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