]> jfr.im git - yt-dlp.git/blob - devscripts/buildserver.py
[buildserver] Service installation and uninstallation
[yt-dlp.git] / 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:])