buildserver.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. # UNUSED
  2. #!/usr/bin/python3
  3. import argparse
  4. import ctypes
  5. import functools
  6. import shutil
  7. import subprocess
  8. import sys
  9. import tempfile
  10. import threading
  11. import traceback
  12. import os.path
  13. sys.path.insert(0, os.path.dirname(os.path.dirname((os.path.abspath(__file__)))))
  14. from yt_dlp.compat import (
  15. compat_input,
  16. compat_http_server,
  17. compat_str,
  18. compat_urlparse,
  19. )
  20. # These are not used outside of buildserver.py thus not in compat.py
  21. try:
  22. import winreg as compat_winreg
  23. except ImportError: # Python 2
  24. import _winreg as compat_winreg
  25. try:
  26. import socketserver as compat_socketserver
  27. except ImportError: # Python 2
  28. import SocketServer as compat_socketserver
  29. class BuildHTTPServer(compat_socketserver.ThreadingMixIn, compat_http_server.HTTPServer):
  30. allow_reuse_address = True
  31. advapi32 = ctypes.windll.advapi32
  32. SC_MANAGER_ALL_ACCESS = 0xf003f
  33. SC_MANAGER_CREATE_SERVICE = 0x02
  34. SERVICE_WIN32_OWN_PROCESS = 0x10
  35. SERVICE_AUTO_START = 0x2
  36. SERVICE_ERROR_NORMAL = 0x1
  37. DELETE = 0x00010000
  38. SERVICE_STATUS_START_PENDING = 0x00000002
  39. SERVICE_STATUS_RUNNING = 0x00000004
  40. SERVICE_ACCEPT_STOP = 0x1
  41. SVCNAME = 'youtubedl_builder'
  42. LPTSTR = ctypes.c_wchar_p
  43. START_CALLBACK = ctypes.WINFUNCTYPE(None, ctypes.c_int, ctypes.POINTER(LPTSTR))
  44. class SERVICE_TABLE_ENTRY(ctypes.Structure):
  45. _fields_ = [
  46. ('lpServiceName', LPTSTR),
  47. ('lpServiceProc', START_CALLBACK)
  48. ]
  49. HandlerEx = ctypes.WINFUNCTYPE(
  50. ctypes.c_int, # return
  51. ctypes.c_int, # dwControl
  52. ctypes.c_int, # dwEventType
  53. ctypes.c_void_p, # lpEventData,
  54. ctypes.c_void_p, # lpContext,
  55. )
  56. def _ctypes_array(c_type, py_array):
  57. ar = (c_type * len(py_array))()
  58. ar[:] = py_array
  59. return ar
  60. def win_OpenSCManager():
  61. res = advapi32.OpenSCManagerW(None, None, SC_MANAGER_ALL_ACCESS)
  62. if not res:
  63. raise Exception('Opening service manager failed - '
  64. 'are you running this as administrator?')
  65. return res
  66. def win_install_service(service_name, cmdline):
  67. manager = win_OpenSCManager()
  68. try:
  69. h = advapi32.CreateServiceW(
  70. manager, service_name, None,
  71. SC_MANAGER_CREATE_SERVICE, SERVICE_WIN32_OWN_PROCESS,
  72. SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
  73. cmdline, None, None, None, None, None)
  74. if not h:
  75. raise OSError('Service creation failed: %s' % ctypes.FormatError())
  76. advapi32.CloseServiceHandle(h)
  77. finally:
  78. advapi32.CloseServiceHandle(manager)
  79. def win_uninstall_service(service_name):
  80. manager = win_OpenSCManager()
  81. try:
  82. h = advapi32.OpenServiceW(manager, service_name, DELETE)
  83. if not h:
  84. raise OSError('Could not find service %s: %s' % (
  85. service_name, ctypes.FormatError()))
  86. try:
  87. if not advapi32.DeleteService(h):
  88. raise OSError('Deletion failed: %s' % ctypes.FormatError())
  89. finally:
  90. advapi32.CloseServiceHandle(h)
  91. finally:
  92. advapi32.CloseServiceHandle(manager)
  93. def win_service_report_event(service_name, msg, is_error=True):
  94. with open('C:/sshkeys/log', 'a', encoding='utf-8') as f:
  95. f.write(msg + '\n')
  96. event_log = advapi32.RegisterEventSourceW(None, service_name)
  97. if not event_log:
  98. raise OSError('Could not report event: %s' % ctypes.FormatError())
  99. try:
  100. type_id = 0x0001 if is_error else 0x0004
  101. event_id = 0xc0000000 if is_error else 0x40000000
  102. lines = _ctypes_array(LPTSTR, [msg])
  103. if not advapi32.ReportEventW(
  104. event_log, type_id, 0, event_id, None, len(lines), 0,
  105. lines, None):
  106. raise OSError('Event reporting failed: %s' % ctypes.FormatError())
  107. finally:
  108. advapi32.DeregisterEventSource(event_log)
  109. def win_service_handler(stop_event, *args):
  110. try:
  111. raise ValueError('Handler called with args ' + repr(args))
  112. TODO
  113. except Exception as e:
  114. tb = traceback.format_exc()
  115. msg = str(e) + '\n' + tb
  116. win_service_report_event(service_name, msg, is_error=True)
  117. raise
  118. def win_service_set_status(handle, status_code):
  119. svcStatus = SERVICE_STATUS()
  120. svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS
  121. svcStatus.dwCurrentState = status_code
  122. svcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP
  123. svcStatus.dwServiceSpecificExitCode = 0
  124. if not advapi32.SetServiceStatus(handle, ctypes.byref(svcStatus)):
  125. raise OSError('SetServiceStatus failed: %r' % ctypes.FormatError())
  126. def win_service_main(service_name, real_main, argc, argv_raw):
  127. try:
  128. # args = [argv_raw[i].value for i in range(argc)]
  129. stop_event = threading.Event()
  130. handler = HandlerEx(functools.partial(stop_event, win_service_handler))
  131. h = advapi32.RegisterServiceCtrlHandlerExW(service_name, handler, None)
  132. if not h:
  133. raise OSError('Handler registration failed: %s' %
  134. ctypes.FormatError())
  135. TODO
  136. except Exception as e:
  137. tb = traceback.format_exc()
  138. msg = str(e) + '\n' + tb
  139. win_service_report_event(service_name, msg, is_error=True)
  140. raise
  141. def win_service_start(service_name, real_main):
  142. try:
  143. cb = START_CALLBACK(
  144. functools.partial(win_service_main, service_name, real_main))
  145. dispatch_table = _ctypes_array(SERVICE_TABLE_ENTRY, [
  146. SERVICE_TABLE_ENTRY(
  147. service_name,
  148. cb
  149. ),
  150. SERVICE_TABLE_ENTRY(None, ctypes.cast(None, START_CALLBACK))
  151. ])
  152. if not advapi32.StartServiceCtrlDispatcherW(dispatch_table):
  153. raise OSError('ctypes start failed: %s' % ctypes.FormatError())
  154. except Exception as e:
  155. tb = traceback.format_exc()
  156. msg = str(e) + '\n' + tb
  157. win_service_report_event(service_name, msg, is_error=True)
  158. raise
  159. def main(args=None):
  160. parser = argparse.ArgumentParser()
  161. parser.add_argument('-i', '--install',
  162. action='store_const', dest='action', const='install',
  163. help='Launch at Windows startup')
  164. parser.add_argument('-u', '--uninstall',
  165. action='store_const', dest='action', const='uninstall',
  166. help='Remove Windows service')
  167. parser.add_argument('-s', '--service',
  168. action='store_const', dest='action', const='service',
  169. help='Run as a Windows service')
  170. parser.add_argument('-b', '--bind', metavar='<host:port>',
  171. action='store', default='0.0.0.0:8142',
  172. help='Bind to host:port (default %default)')
  173. options = parser.parse_args(args=args)
  174. if options.action == 'install':
  175. fn = os.path.abspath(__file__).replace('v:', '\\\\vboxsrv\\vbox')
  176. cmdline = '%s %s -s -b %s' % (sys.executable, fn, options.bind)
  177. win_install_service(SVCNAME, cmdline)
  178. return
  179. if options.action == 'uninstall':
  180. win_uninstall_service(SVCNAME)
  181. return
  182. if options.action == 'service':
  183. win_service_start(SVCNAME, main)
  184. return
  185. host, port_str = options.bind.split(':')
  186. port = int(port_str)
  187. print('Listening on %s:%d' % (host, port))
  188. srv = BuildHTTPServer((host, port), BuildHTTPRequestHandler)
  189. thr = threading.Thread(target=srv.serve_forever)
  190. thr.start()
  191. compat_input('Press ENTER to shut down')
  192. srv.shutdown()
  193. thr.join()
  194. def rmtree(path):
  195. for name in os.listdir(path):
  196. fname = os.path.join(path, name)
  197. if os.path.isdir(fname):
  198. rmtree(fname)
  199. else:
  200. os.chmod(fname, 0o666)
  201. os.remove(fname)
  202. os.rmdir(path)
  203. class BuildError(Exception):
  204. def __init__(self, output, code=500):
  205. self.output = output
  206. self.code = code
  207. def __str__(self):
  208. return self.output
  209. class HTTPError(BuildError):
  210. pass
  211. class PythonBuilder(object):
  212. def __init__(self, **kwargs):
  213. python_version = kwargs.pop('python', '3.4')
  214. python_path = None
  215. for node in ('Wow6432Node\\', ''):
  216. try:
  217. key = compat_winreg.OpenKey(
  218. compat_winreg.HKEY_LOCAL_MACHINE,
  219. r'SOFTWARE\%sPython\PythonCore\%s\InstallPath' % (node, python_version))
  220. try:
  221. python_path, _ = compat_winreg.QueryValueEx(key, '')
  222. finally:
  223. compat_winreg.CloseKey(key)
  224. break
  225. except Exception:
  226. pass
  227. if not python_path:
  228. raise BuildError('No such Python version: %s' % python_version)
  229. self.pythonPath = python_path
  230. super(PythonBuilder, self).__init__(**kwargs)
  231. class GITInfoBuilder(object):
  232. def __init__(self, **kwargs):
  233. try:
  234. self.user, self.repoName = kwargs['path'][:2]
  235. self.rev = kwargs.pop('rev')
  236. except ValueError:
  237. raise BuildError('Invalid path')
  238. except KeyError as e:
  239. raise BuildError('Missing mandatory parameter "%s"' % e.args[0])
  240. path = os.path.join(os.environ['APPDATA'], 'Build archive', self.repoName, self.user)
  241. if not os.path.exists(path):
  242. os.makedirs(path)
  243. self.basePath = tempfile.mkdtemp(dir=path)
  244. self.buildPath = os.path.join(self.basePath, 'build')
  245. super(GITInfoBuilder, self).__init__(**kwargs)
  246. class GITBuilder(GITInfoBuilder):
  247. def build(self):
  248. try:
  249. subprocess.check_output(['git', 'clone', 'git://github.com/%s/%s.git' % (self.user, self.repoName), self.buildPath])
  250. subprocess.check_output(['git', 'checkout', self.rev], cwd=self.buildPath)
  251. except subprocess.CalledProcessError as e:
  252. raise BuildError(e.output)
  253. super(GITBuilder, self).build()
  254. class YoutubeDLBuilder(object):
  255. authorizedUsers = ['fraca7', 'phihag', 'rg3', 'FiloSottile', 'ytdl-org']
  256. def __init__(self, **kwargs):
  257. if self.repoName != 'yt-dlp':
  258. raise BuildError('Invalid repository "%s"' % self.repoName)
  259. if self.user not in self.authorizedUsers:
  260. raise HTTPError('Unauthorized user "%s"' % self.user, 401)
  261. super(YoutubeDLBuilder, self).__init__(**kwargs)
  262. def build(self):
  263. try:
  264. proc = subprocess.Popen([os.path.join(self.pythonPath, 'python.exe'), 'setup.py', 'py2exe'], stdin=subprocess.PIPE, cwd=self.buildPath)
  265. proc.wait()
  266. #subprocess.check_output([os.path.join(self.pythonPath, 'python.exe'), 'setup.py', 'py2exe'],
  267. # cwd=self.buildPath)
  268. except subprocess.CalledProcessError as e:
  269. raise BuildError(e.output)
  270. super(YoutubeDLBuilder, self).build()
  271. class DownloadBuilder(object):
  272. def __init__(self, **kwargs):
  273. self.handler = kwargs.pop('handler')
  274. self.srcPath = os.path.join(self.buildPath, *tuple(kwargs['path'][2:]))
  275. self.srcPath = os.path.abspath(os.path.normpath(self.srcPath))
  276. if not self.srcPath.startswith(self.buildPath):
  277. raise HTTPError(self.srcPath, 401)
  278. super(DownloadBuilder, self).__init__(**kwargs)
  279. def build(self):
  280. if not os.path.exists(self.srcPath):
  281. raise HTTPError('No such file', 404)
  282. if os.path.isdir(self.srcPath):
  283. raise HTTPError('Is a directory: %s' % self.srcPath, 401)
  284. self.handler.send_response(200)
  285. self.handler.send_header('Content-Type', 'application/octet-stream')
  286. self.handler.send_header('Content-Disposition', 'attachment; filename=%s' % os.path.split(self.srcPath)[-1])
  287. self.handler.send_header('Content-Length', str(os.stat(self.srcPath).st_size))
  288. self.handler.end_headers()
  289. with open(self.srcPath, 'rb') as src:
  290. shutil.copyfileobj(src, self.handler.wfile)
  291. super(DownloadBuilder, self).build()
  292. class CleanupTempDir(object):
  293. def build(self):
  294. try:
  295. rmtree(self.basePath)
  296. except Exception as e:
  297. print('WARNING deleting "%s": %s' % (self.basePath, e))
  298. super(CleanupTempDir, self).build()
  299. class Null(object):
  300. def __init__(self, **kwargs):
  301. pass
  302. def start(self):
  303. pass
  304. def close(self):
  305. pass
  306. def build(self):
  307. pass
  308. class Builder(PythonBuilder, GITBuilder, YoutubeDLBuilder, DownloadBuilder, CleanupTempDir, Null):
  309. pass
  310. class BuildHTTPRequestHandler(compat_http_server.BaseHTTPRequestHandler):
  311. actionDict = {'build': Builder, 'download': Builder} # They're the same, no more caching.
  312. def do_GET(self):
  313. path = compat_urlparse.urlparse(self.path)
  314. paramDict = dict([(key, value[0]) for key, value in compat_urlparse.parse_qs(path.query).items()])
  315. action, _, path = path.path.strip('/').partition('/')
  316. if path:
  317. path = path.split('/')
  318. if action in self.actionDict:
  319. try:
  320. builder = self.actionDict[action](path=path, handler=self, **paramDict)
  321. builder.start()
  322. try:
  323. builder.build()
  324. finally:
  325. builder.close()
  326. except BuildError as e:
  327. self.send_response(e.code)
  328. msg = compat_str(e).encode('UTF-8')
  329. self.send_header('Content-Type', 'text/plain; charset=UTF-8')
  330. self.send_header('Content-Length', len(msg))
  331. self.end_headers()
  332. self.wfile.write(msg)
  333. else:
  334. self.send_response(500, 'Unknown build method "%s"' % action)
  335. else:
  336. self.send_response(500, 'Malformed URL')
  337. if __name__ == '__main__':
  338. main()