]>
Commit | Line | Data |
---|---|---|
71cedb3c | 1 | #!/usr/bin/python3 |
83de7942 | 2 | |
71cedb3c PH |
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 | |
83de7942 PH |
10 | |
11 | ||
12 | class BuildHTTPServer(ThreadingMixIn, HTTPServer): | |
13 | allow_reuse_address = True | |
14 | ||
15 | ||
71cedb3c | 16 | advapi32 = ctypes.windll.advapi32 |
83de7942 | 17 | |
71cedb3c PH |
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 | |
83de7942 | 24 | |
83de7942 | 25 | |
71cedb3c PH |
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)) | |
83de7942 PH |
103 | srv = BuildHTTPServer((host, port), BuildHTTPRequestHandler) |
104 | thr = threading.Thread(target=srv.serve_forever) | |
105 | thr.start() | |
71cedb3c | 106 | input('Press ENTER to shut down') |
83de7942 PH |
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: | |
71cedb3c | 117 | os.chmod(fname, 0o666) |
83de7942 PH |
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: | |
71cedb3c | 235 | print('WARNING deleting "%s": %s' % (self.basePath, e)) |
83de7942 PH |
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:]) |