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.

292 lines
9.3 KiB

  1. #!/usr/bin/python3
  2. from http.server import HTTPServer, BaseHTTPRequestHandler
  3. from socketserver import ThreadingMixIn
  4. import argparse
  5. import ctypes
  6. import sys
  7. import threading
  8. import os.path
  9. class BuildHTTPServer(ThreadingMixIn, HTTPServer):
  10. allow_reuse_address = True
  11. advapi32 = ctypes.windll.advapi32
  12. SC_MANAGER_ALL_ACCESS = 0xf003f
  13. SC_MANAGER_CREATE_SERVICE = 0x02
  14. SERVICE_WIN32_OWN_PROCESS = 0x10
  15. SERVICE_AUTO_START = 0x2
  16. SERVICE_ERROR_NORMAL = 0x1
  17. DELETE = 0x00010000
  18. def win_OpenSCManager():
  19. res = advapi32.OpenSCManagerA(None, None, SC_MANAGER_ALL_ACCESS)
  20. if not res:
  21. raise Exception('Opening service manager failed - '
  22. 'are you running this as administrator?')
  23. return res
  24. def win_install_service(service_name, cmdline):
  25. manager = win_OpenSCManager()
  26. try:
  27. h = advapi32.CreateServiceA(
  28. manager, service_name, None,
  29. SC_MANAGER_CREATE_SERVICE, SERVICE_WIN32_OWN_PROCESS,
  30. SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
  31. cmdline, None, None, None, None, None)
  32. if not h:
  33. raise OSError('Service creation failed: %s' % ctypes.FormatError())
  34. advapi32.CloseServiceHandle(h)
  35. finally:
  36. advapi32.CloseServiceHandle(manager)
  37. def win_uninstall_service(service_name):
  38. manager = win_OpenSCManager()
  39. try:
  40. h = advapi32.OpenServiceA(manager, service_name, DELETE)
  41. if not h:
  42. raise OSError('Could not find service %s: %s' % (
  43. service_name, ctypes.FormatError()))
  44. try:
  45. if not advapi32.DeleteService(h):
  46. raise OSError('Deletion failed: %s' % ctypes.FormatError())
  47. finally:
  48. advapi32.CloseServiceHandle(h)
  49. finally:
  50. advapi32.CloseServiceHandle(manager)
  51. def install_service(bind):
  52. fn = os.path.normpath(__file__)
  53. cmdline = '"%s" "%s" -s -b "%s"' % (sys.executable, fn, bind)
  54. win_install_service('youtubedl_builder', cmdline)
  55. def uninstall_service():
  56. win_uninstall_service('youtubedl_builder')
  57. def main(argv):
  58. parser = argparse.ArgumentParser()
  59. parser.add_argument('-i', '--install',
  60. action='store_const', dest='action', const='install',
  61. help='Launch at Windows startup')
  62. parser.add_argument('-u', '--uninstall',
  63. action='store_const', dest='action', const='uninstall',
  64. help='Remove Windows service')
  65. parser.add_argument('-s', '--service',
  66. action='store_const', dest='action', const='servce',
  67. help='Run as a Windows service')
  68. parser.add_argument('-b', '--bind', metavar='<host:port>',
  69. action='store', default='localhost:8142',
  70. help='Bind to host:port (default %default)')
  71. options = parser.parse_args()
  72. if options.action == 'install':
  73. return install_service(options.bind)
  74. if options.action == 'uninstall':
  75. return uninstall_service()
  76. host, port_str = options.bind.split(':')
  77. port = int(port_str)
  78. print('Listening on %s:%d' % (host, port))
  79. srv = BuildHTTPServer((host, port), BuildHTTPRequestHandler)
  80. thr = threading.Thread(target=srv.serve_forever)
  81. thr.start()
  82. input('Press ENTER to shut down')
  83. srv.shutdown()
  84. thr.join()
  85. def rmtree(path):
  86. for name in os.listdir(path):
  87. fname = os.path.join(path, name)
  88. if os.path.isdir(fname):
  89. rmtree(fname)
  90. else:
  91. os.chmod(fname, 0o666)
  92. os.remove(fname)
  93. os.rmdir(path)
  94. #==============================================================================
  95. class BuildError(Exception):
  96. def __init__(self, output, code=500):
  97. self.output = output
  98. self.code = code
  99. def __str__(self):
  100. return self.output
  101. class HTTPError(BuildError):
  102. pass
  103. class PythonBuilder(object):
  104. def __init__(self, **kwargs):
  105. pythonVersion = kwargs.pop('python', '2.7')
  106. try:
  107. key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Python\PythonCore\%s\InstallPath' % pythonVersion)
  108. try:
  109. self.pythonPath, _ = _winreg.QueryValueEx(key, '')
  110. finally:
  111. _winreg.CloseKey(key)
  112. except Exception:
  113. raise BuildError('No such Python version: %s' % pythonVersion)
  114. super(PythonBuilder, self).__init__(**kwargs)
  115. class GITInfoBuilder(object):
  116. def __init__(self, **kwargs):
  117. try:
  118. self.user, self.repoName = kwargs['path'][:2]
  119. self.rev = kwargs.pop('rev')
  120. except ValueError:
  121. raise BuildError('Invalid path')
  122. except KeyError as e:
  123. raise BuildError('Missing mandatory parameter "%s"' % e.args[0])
  124. path = os.path.join(os.environ['APPDATA'], 'Build archive', self.repoName, self.user)
  125. if not os.path.exists(path):
  126. os.makedirs(path)
  127. self.basePath = tempfile.mkdtemp(dir=path)
  128. self.buildPath = os.path.join(self.basePath, 'build')
  129. super(GITInfoBuilder, self).__init__(**kwargs)
  130. class GITBuilder(GITInfoBuilder):
  131. def build(self):
  132. try:
  133. subprocess.check_output(['git', 'clone', 'git://github.com/%s/%s.git' % (self.user, self.repoName), self.buildPath])
  134. subprocess.check_output(['git', 'checkout', self.rev], cwd=self.buildPath)
  135. except subprocess.CalledProcessError as e:
  136. raise BuildError(e.output)
  137. super(GITBuilder, self).build()
  138. class YoutubeDLBuilder(object):
  139. authorizedUsers = ['fraca7', 'phihag', 'rg3', 'FiloSottile']
  140. def __init__(self, **kwargs):
  141. if self.repoName != 'youtube-dl':
  142. raise BuildError('Invalid repository "%s"' % self.repoName)
  143. if self.user not in self.authorizedUsers:
  144. raise HTTPError('Unauthorized user "%s"' % self.user, 401)
  145. super(YoutubeDLBuilder, self).__init__(**kwargs)
  146. def build(self):
  147. try:
  148. subprocess.check_output([os.path.join(self.pythonPath, 'python.exe'), 'setup.py', 'py2exe'],
  149. cwd=self.buildPath)
  150. except subprocess.CalledProcessError as e:
  151. raise BuildError(e.output)
  152. super(YoutubeDLBuilder, self).build()
  153. class DownloadBuilder(object):
  154. def __init__(self, **kwargs):
  155. self.handler = kwargs.pop('handler')
  156. self.srcPath = os.path.join(self.buildPath, *tuple(kwargs['path'][2:]))
  157. self.srcPath = os.path.abspath(os.path.normpath(self.srcPath))
  158. if not self.srcPath.startswith(self.buildPath):
  159. raise HTTPError(self.srcPath, 401)
  160. super(DownloadBuilder, self).__init__(**kwargs)
  161. def build(self):
  162. if not os.path.exists(self.srcPath):
  163. raise HTTPError('No such file', 404)
  164. if os.path.isdir(self.srcPath):
  165. raise HTTPError('Is a directory: %s' % self.srcPath, 401)
  166. self.handler.send_response(200)
  167. self.handler.send_header('Content-Type', 'application/octet-stream')
  168. self.handler.send_header('Content-Disposition', 'attachment; filename=%s' % os.path.split(self.srcPath)[-1])
  169. self.handler.send_header('Content-Length', str(os.stat(self.srcPath).st_size))
  170. self.handler.end_headers()
  171. with open(self.srcPath, 'rb') as src:
  172. shutil.copyfileobj(src, self.handler.wfile)
  173. super(DownloadBuilder, self).build()
  174. class CleanupTempDir(object):
  175. def build(self):
  176. try:
  177. rmtree(self.basePath)
  178. except Exception as e:
  179. print('WARNING deleting "%s": %s' % (self.basePath, e))
  180. super(CleanupTempDir, self).build()
  181. class Null(object):
  182. def __init__(self, **kwargs):
  183. pass
  184. def start(self):
  185. pass
  186. def close(self):
  187. pass
  188. def build(self):
  189. pass
  190. class Builder(PythonBuilder, GITBuilder, YoutubeDLBuilder, DownloadBuilder, CleanupTempDir, Null):
  191. pass
  192. class BuildHTTPRequestHandler(BaseHTTPRequestHandler):
  193. actionDict = { 'build': Builder, 'download': Builder } # They're the same, no more caching.
  194. def do_GET(self):
  195. path = urlparse.urlparse(self.path)
  196. paramDict = dict([(key, value[0]) for key, value in urlparse.parse_qs(path.query).items()])
  197. action, _, path = path.path.strip('/').partition('/')
  198. if path:
  199. path = path.split('/')
  200. if action in self.actionDict:
  201. try:
  202. builder = self.actionDict[action](path=path, handler=self, **paramDict)
  203. builder.start()
  204. try:
  205. builder.build()
  206. finally:
  207. builder.close()
  208. except BuildError as e:
  209. self.send_response(e.code)
  210. msg = unicode(e).encode('UTF-8')
  211. self.send_header('Content-Type', 'text/plain; charset=UTF-8')
  212. self.send_header('Content-Length', len(msg))
  213. self.end_headers()
  214. self.wfile.write(msg)
  215. except HTTPError as e:
  216. self.send_response(e.code, str(e))
  217. else:
  218. self.send_response(500, 'Unknown build method "%s"' % action)
  219. else:
  220. self.send_response(500, 'Malformed URL')
  221. #==============================================================================
  222. if __name__ == '__main__':
  223. main(sys.argv[1:])