You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

406 lines
13 KiB

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