buildserver.py 14 KB

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