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.

439 lines
14 KiB

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