OLD | NEW |
| (Empty) |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 import base64 | |
5 import hashlib | |
6 import heapq | |
7 import json | |
8 import logging | |
9 import os | |
10 import re | |
11 import select | |
12 import socket | |
13 import subprocess | |
14 import sys | |
15 import time | |
16 import urlparse | |
17 | |
18 import BaseHTTPServer | |
19 | |
20 _unittests_running = False | |
21 | |
22 class ChromeNotFoundException(Exception): | |
23 pass | |
24 | |
25 class _PossibleDesktopBrowser(object): | |
26 def __init__(self, browser_type, executable): | |
27 self.browser_type = browser_type | |
28 self.local_executable = executable | |
29 | |
30 def __repr__(self): | |
31 return '_PossibleDesktopBrowser(browser_type=%s)' % self.browser_type | |
32 | |
33 def _samefile(a, b): | |
34 if sys.platform != 'win32': | |
35 return os.path.samefile(a, b) | |
36 return os.path.abspath(a) == os.path.abspath(b) | |
37 | |
38 def _FindAllAvailableBrowsers(): | |
39 """Finds all the desktop browsers available on this machine.""" | |
40 browsers = [] | |
41 | |
42 has_display = True | |
43 if (sys.platform.startswith('linux') and | |
44 os.getenv('DISPLAY') == None): | |
45 has_display = False | |
46 | |
47 def AddIfFound(browser_type, browser_dir, app_name): | |
48 app = os.path.join(browser_dir, app_name) | |
49 if os.path.exists(app): | |
50 browsers.append(_PossibleDesktopBrowser(browser_type, | |
51 app)) | |
52 return True | |
53 return False | |
54 | |
55 # Look for a browser in the standard chrome build locations. | |
56 if sys.platform == 'darwin': | |
57 chromium_app_name = 'Chromium.app/Contents/MacOS/Chromium' | |
58 elif sys.platform.startswith('linux'): | |
59 chromium_app_name = 'chrome' | |
60 elif sys.platform.startswith('win'): | |
61 chromium_app_name = 'chrome.exe' | |
62 else: | |
63 raise Exception('Platform not recognized') | |
64 | |
65 # Mac-specific options. | |
66 if sys.platform == 'darwin': | |
67 mac_canary = ('/Applications/Google Chrome Canary.app/' | |
68 'Contents/MacOS/Google Chrome Canary') | |
69 mac_system = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' | |
70 if os.path.exists(mac_canary): | |
71 browsers.append(_PossibleDesktopBrowser('canary', | |
72 mac_canary)) | |
73 | |
74 if os.path.exists(mac_system): | |
75 browsers.append(_PossibleDesktopBrowser('system', | |
76 mac_system)) | |
77 | |
78 # Linux specific options. | |
79 if sys.platform.startswith('linux'): | |
80 # Look for a google-chrome instance. | |
81 found = False | |
82 try: | |
83 with open(os.devnull, 'w') as devnull: | |
84 found = subprocess.call(['google-chrome', '--version'], | |
85 stdout=devnull, stderr=devnull) == 0 | |
86 except OSError: | |
87 pass | |
88 if found: | |
89 browsers.append( | |
90 _PossibleDesktopBrowser('system', | |
91 'google-chrome')) | |
92 | |
93 # Win32-specific options. | |
94 if sys.platform.startswith('win'): | |
95 system_path = os.path.join('Google', 'Chrome', 'Application') | |
96 canary_path = os.path.join('Google', 'Chrome SxS', 'Application') | |
97 | |
98 win_search_paths = [os.getenv('PROGRAMFILES(X86)'), | |
99 os.getenv('PROGRAMFILES'), | |
100 os.getenv('LOCALAPPDATA')] | |
101 | |
102 for path in win_search_paths: | |
103 if not path: | |
104 continue | |
105 if AddIfFound('canary', os.path.join(path, canary_path), | |
106 chromium_app_name): | |
107 break | |
108 | |
109 for path in win_search_paths: | |
110 if not path: | |
111 continue | |
112 if AddIfFound('system', os.path.join(path, system_path), | |
113 chromium_app_name): | |
114 break | |
115 | |
116 if len(browsers) and not has_display: | |
117 logging.warning( | |
118 'Found (%s), but you do not have a DISPLAY environment set.' % | |
119 ','.join([b.browser_type for b in browsers])) | |
120 return [] | |
121 | |
122 return browsers | |
123 | |
124 | |
125 | |
126 _ExceptionNames = { | |
127 404: 'Not found', | |
128 500: 'Internal exception', | |
129 } | |
130 | |
131 class _RequestException(Exception): | |
132 def __init__(self, number): | |
133 super(_RequestException, self).__init__('_RequestException') | |
134 self.number = number | |
135 | |
136 class _RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): | |
137 def __init__(self, request, client_address, server): | |
138 self._server = server | |
139 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address
, server) | |
140 | |
141 def _SendJSON(self, obj, resp_code=200, resp_code_str='OK'): | |
142 text = json.dumps(obj) | |
143 try: | |
144 self.send_response(resp_code, resp_code_str) | |
145 self.send_header('Cache-Control', 'no-cache') | |
146 self.send_header('Content-Type', 'application/json') | |
147 self.send_header('Content-Length', len(text)) | |
148 self.end_headers() | |
149 self.wfile.write(text) | |
150 except IOError: | |
151 return | |
152 | |
153 def log_message(self, format, *args): | |
154 pass | |
155 | |
156 def do_POST(self): | |
157 self._HandleRequest('POST') | |
158 | |
159 def do_GET(self): | |
160 self._HandleRequest('GET') | |
161 | |
162 def _HandleRequest(self, method): | |
163 if 'Content-Length' in self.headers: | |
164 cl = int(self.headers['Content-Length']) | |
165 text = self.rfile.read(cl).encode('utf8') | |
166 try: | |
167 if text != '': | |
168 content = json.loads(text) | |
169 else: | |
170 content = '' | |
171 except ValueError: | |
172 raise Exception('Payload was unparseable: [%s]' % text) | |
173 else: | |
174 content = None | |
175 | |
176 if self.path == '/ping': | |
177 self._SendJSON('pong') | |
178 return | |
179 | |
180 try: | |
181 res = self._server.HandleRequest(method, self.path, content) | |
182 self._SendJSON(res) | |
183 except _RequestException, ex: | |
184 self.send_response(ex.number, _ExceptionNames[ex.number]) | |
185 self.send_header('Content-Length', 0) | |
186 self.end_headers() | |
187 except: | |
188 import traceback | |
189 traceback.print_exc() | |
190 self.send_response(500, 'ServerError') | |
191 self.send_header('Content-Length', 0) | |
192 self.end_headers() | |
193 | |
194 class _TimeoutTask(object): | |
195 def __init__(self, cb, deadline, args): | |
196 self.cb = cb | |
197 self.deadline = deadline | |
198 self.args = args | |
199 | |
200 def __cmp__(self, that): | |
201 return cmp(self.deadline, that.deadline) | |
202 | |
203 class _Daemon(BaseHTTPServer.HTTPServer): | |
204 def __init__(self, server_address): | |
205 BaseHTTPServer.HTTPServer.__init__(self, server_address, _RequestHandler) | |
206 self._port = server_address[1] | |
207 self._is_running = False | |
208 self._pending_timeout_heap = [] | |
209 | |
210 @property | |
211 def port(self): | |
212 return self._port | |
213 | |
214 @property | |
215 def is_running(self): | |
216 return self._is_running | |
217 | |
218 def AddDelayedTask(self, cb, delay, *args): | |
219 deadline = time.time() + delay | |
220 to = _TimeoutTask(cb, deadline, args) | |
221 heapq.heappush(self._pending_timeout_heap, to) | |
222 | |
223 def serve_forever(self): | |
224 self._is_running = True | |
225 try: | |
226 while self._is_running: | |
227 now = time.time() | |
228 while True: | |
229 if len(self._pending_timeout_heap): | |
230 deadline = self._pending_timeout_heap[0].deadline | |
231 if now > deadline: | |
232 item = heapq.heappop(self._pending_timeout_heap) | |
233 item.cb(*item.args) | |
234 else: | |
235 next_deadline = deadline | |
236 break | |
237 else: | |
238 next_deadline = now + 0.2 | |
239 break | |
240 | |
241 now = time.time() | |
242 delay = max(0.0,next_deadline - now) | |
243 delay = min(0.25,delay) | |
244 r, w, e = select.select([self], [], [], delay) | |
245 if r: | |
246 self.handle_request() | |
247 finally: | |
248 self._is_running = False | |
249 | |
250 def HandleRequest(self, method, path, content): | |
251 if self.handler: | |
252 return self.handler(method, path, content) | |
253 raise _RequestException('404') | |
254 | |
255 def Stop(self): | |
256 assert self._is_running | |
257 self._is_running = False | |
258 | |
259 def Run(self): | |
260 logging.debug('Starting chromeapp._Daemon on port %d', self._port) | |
261 self.serve_forever() | |
262 logging.debug('Shut down chromeapp._Daemon on port %d', self._port) | |
263 | |
264 class _TimeoutException(Exception): | |
265 pass | |
266 | |
267 def _WaitFor(condition, | |
268 timeout, poll_interval=0.1, | |
269 pass_time_left_to_func=False): | |
270 assert isinstance(condition, type(lambda: None)) # is function | |
271 start_time = time.time() | |
272 while True: | |
273 if pass_time_left_to_func: | |
274 res = condition(max((start_time + timeout) - time.time(), 0.0)) | |
275 else: | |
276 res = condition() | |
277 if res: | |
278 break | |
279 if time.time() - start_time > timeout: | |
280 if condition.__name__ == '<lambda>': | |
281 try: | |
282 condition_string = inspect.getsource(condition).strip() | |
283 except IOError: | |
284 condition_string = condition.__name__ | |
285 else: | |
286 condition_string = condition.__name__ | |
287 raise _TimeoutException('Timed out while waiting %ds for %s.' % | |
288 (timeout, condition_string)) | |
289 time.sleep(poll_interval) | |
290 | |
291 def IsChromeInstalled(): | |
292 """Returns whether chromeapp works on this system.""" | |
293 browsers = _FindAllAvailableBrowsers() | |
294 return len(browsers) > 0 | |
295 | |
296 def _HexToMPDecimal(hex_chars): | |
297 """ Convert bytes to an MPDecimal string. Example \x00 -> "aa" | |
298 This gives us the AppID for a chrome extension. | |
299 """ | |
300 result = '' | |
301 base = ord('a') | |
302 for i in xrange(len(hex_chars)): | |
303 value = ord(hex_chars[i]) | |
304 dig1 = value / 16 | |
305 dig2 = value % 16 | |
306 result += chr(dig1 + base) | |
307 result += chr(dig2 + base) | |
308 return result | |
309 | |
310 def _GetPublicKeyFromPath(filepath): | |
311 # Normalize the path for windows to have capital drive letters. | |
312 # We intentionally don't check if sys.platform == 'win32' and just | |
313 # check if this looks like drive letter so that we can test this | |
314 # even on posix systems. | |
315 if (len(filepath) >= 2 and | |
316 filepath[0].islower() and | |
317 filepath[1] == ':'): | |
318 return filepath[0].upper() + filepath[1:] | |
319 return filepath | |
320 | |
321 def _GetPublicKeyUnpacked(filepath): | |
322 assert os.path.isdir(filepath) | |
323 f = open(os.path.join(filepath, 'manifest.json'), 'rb') | |
324 manifest = json.load(f) | |
325 if 'key' not in manifest: | |
326 # Use the path as the public key. | |
327 # See Extension::GenerateIdForPath in extension.cc | |
328 return _GetPublicKeyFromPath(os.path.abspath(filepath)) | |
329 else: | |
330 return base64.standard_b64decode(manifest['key']) | |
331 | |
332 def _GetCRXAppID(filepath): | |
333 pub_key = _GetPublicKeyUnpacked(filepath) | |
334 pub_key_hash = hashlib.sha256(pub_key).digest() | |
335 # AppID is the MPDecimal of only the first 128 bits of the hash. | |
336 return _HexToMPDecimal(pub_key_hash[:128/8]) | |
337 | |
338 class AppInstance(object): | |
339 def __init__(self, app, args=None): | |
340 self._app = app | |
341 self._proc = None | |
342 self._devnull = None | |
343 self._cur_chromeapp_js = None | |
344 self._event_listeners = {} | |
345 if args: | |
346 self._args = args | |
347 else: | |
348 self._args = [] | |
349 | |
350 self._exit_code = None | |
351 self._exiting_run_loop = False | |
352 | |
353 def __enter__(self): | |
354 if not self.is_started: | |
355 self.Start() | |
356 return self | |
357 | |
358 def __exit__(self, *args): | |
359 if self.is_started: | |
360 self._CloseBrowserProcess() | |
361 | |
362 @property | |
363 def is_started(self): | |
364 return self._proc != None | |
365 | |
366 def Start(self): | |
367 tmp = socket.socket() | |
368 tmp.bind(('', 0)) | |
369 port = tmp.getsockname()[1] | |
370 tmp.close() | |
371 | |
372 self._daemon = _Daemon(('localhost', port)) | |
373 self._daemon.handler = self._HandleRequest | |
374 | |
375 browsers = _FindAllAvailableBrowsers() | |
376 if len(browsers) == 0: | |
377 raise ChromeNotFoundException('Could not find Chrome. Cannot start app.') | |
378 browser = browsers[0] | |
379 | |
380 | |
381 if self._GetAppID() == None: | |
382 if not _unittests_running: | |
383 sys.stderr.write(""" | |
384 Installing Chrome App for %s. | |
385 | |
386 You will see chrome appear as this happens. | |
387 | |
388 ***DO NOT CLOSE IT*** | |
389 """ % self._app.stable_app_name) | |
390 sys.stderr.flush() | |
391 self._Install(browser) | |
392 if not _unittests_running: | |
393 sys.stderr.write("\nApp installed. Thanks for waiting.\n") | |
394 | |
395 app_id = self._GetAppID() | |
396 | |
397 # Temporary staging: we now know how to compute crx ids by hand. | |
398 # Verify that we are getting it right. | |
399 raw_id = _GetCRXAppID(self._app.manifest_dirname) | |
400 assert raw_id == app_id | |
401 | |
402 if self._app.debug_mode: | |
403 print "chromeapp: app_id is %s" % app_id | |
404 | |
405 browser_args = [browser.local_executable] | |
406 browser_args.extend(self._app._GetBrowserStartupArgs()) | |
407 browser_args.append('--app-id=%s' % app_id) | |
408 logging.info('Launching %s as %s', self._app.stable_app_name, app_id) | |
409 self._CreateLaunchJS() | |
410 try: | |
411 self._Launch(browser_args) | |
412 except: | |
413 if self.is_started: | |
414 self._CloseBrowserProcess() | |
415 | |
416 def _Install(self, browser): | |
417 logging.info('Installing %s for first time...', self._app.stable_app_name) | |
418 browser_args = [browser.local_executable] | |
419 browser_args.extend(self._app._GetBrowserStartupArgs()) | |
420 browser_args.append( | |
421 '--load-extension=%s' % self._app.manifest_dirname | |
422 ) | |
423 try: | |
424 self._Launch(browser_args) | |
425 def IsAppInstalled(): | |
426 return self._GetAppID() != None | |
427 # We may have to a wait for a while, it seems like chrome takes a while | |
428 # to flush its preferences. | |
429 _WaitFor(IsAppInstalled, 60, poll_interval=0.5) | |
430 logging.info('Installed %s', self._app.stable_app_name) | |
431 finally: | |
432 if self.is_started: | |
433 self._CloseBrowserProcess() | |
434 | |
435 def _GetAppID(self): | |
436 prefs = self._app._ReadPreferences() | |
437 if 'extensions' not in prefs: | |
438 return None | |
439 if 'settings' not in prefs['extensions']: | |
440 return None | |
441 settings = prefs['extensions']['settings'] | |
442 for app_id, app_settings in settings.iteritems(): | |
443 if 'path' not in app_settings: | |
444 continue | |
445 if not os.path.exists(app_settings['path']): | |
446 continue | |
447 if not _samefile(app_settings['path'], | |
448 self._app.manifest_dirname): | |
449 continue | |
450 | |
451 if 'events' not in app_settings: | |
452 return None | |
453 | |
454 return app_id | |
455 | |
456 return None | |
457 | |
458 def _CreateLaunchJS(self): | |
459 js_template = """ | |
460 'use strict'; | |
461 | |
462 // DO NOT COMMIT! This template is automatically created and updated by | |
463 // py-chrome-ui just before launch, with the necessary | |
464 // parameters for communicating back to the hosting python code. | |
465 (function() { | |
466 var BASE_URL = "__CHROMEAPP_REPLY_URL__"; // Note: chromeapp will set this up
during launch. | |
467 var DEBUG_MODE = __CHROMEAPP_DEBUG_MODE__; // Note: chromeapp will set this u
p during launch.lj | |
468 | |
469 function reqAsync(method, path, data, opt_response_cb, opt_err_cb) { | |
470 if (path[0] != '/') | |
471 throw new Error('Must start with /'); | |
472 var req = new XMLHttpRequest(); | |
473 req.open(method, BASE_URL + path, true); | |
474 req.addEventListener('load', function() { | |
475 if (req.status == 200) { | |
476 if (opt_response_cb) | |
477 opt_response_cb(JSON.parse(req.responseText)); | |
478 return; | |
479 } | |
480 if (opt_err_cb) | |
481 opt_err_cb(); | |
482 else | |
483 console.log('reqAsync ' + path, req); | |
484 }); | |
485 req.addEventListener('error', function() { | |
486 if (opt_err_cb) | |
487 opt_err_cb(); | |
488 else | |
489 console.log('reqAsync ' + path, req); | |
490 }); | |
491 if (data) | |
492 req.send(JSON.stringify(data)); | |
493 else | |
494 req.send(null); | |
495 } | |
496 | |
497 function Event(type, opt_bubbles, opt_preventable) { | |
498 var e = document.createEvent('Event'); | |
499 e.initEvent(type, !!opt_bubbles, !!opt_preventable); | |
500 e.__proto__ = window.Event.prototype; | |
501 return e; | |
502 }; | |
503 | |
504 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { | |
505 var e = new Event(type, opt_bubbles, opt_cancelable); | |
506 return target.dispatchEvent(e); | |
507 } | |
508 | |
509 // chromeapp derives from a div in order to get basic dispatchEvent | |
510 // capabilities. Lame but effective. | |
511 var chromeapp = document.createElement('div'); | |
512 | |
513 // Ask the server for startup args. | |
514 // TODO(nduca): crbug.com/168085 causes breakpoints to be ineffective | |
515 // during the early stages of page load. In debug mode, we delay the launch | |
516 // event for a bit so that breakpoints work. | |
517 if (!DEBUG_MODE) { | |
518 reqAsync('GET', '/launch_args', null, gotLaunchArgs); | |
519 } else { | |
520 reqAsync('GET', '/launch_args', null, function(args) { | |
521 setTimeout(function() { | |
522 gotLaunchArgs(args); | |
523 }, 1000); | |
524 }); | |
525 } | |
526 function gotLaunchArgs(args) { | |
527 var e = new Event('launch', false, false); | |
528 e.args = args; | |
529 chromeapp.launch_args = args; | |
530 chromeapp.dispatchEvent(e); | |
531 } | |
532 | |
533 var oldOnError = window.onerror; | |
534 function onUncaughtError(error, url, line_number) { | |
535 reqAsync('POST', '/uncaught_error', { | |
536 error: error, | |
537 url: url, | |
538 line_number: line_number}); | |
539 if (oldOnError) oldOnError(error, url, line_number); | |
540 } | |
541 window.onerror = onUncaughtError; | |
542 | |
543 function print() { | |
544 var messages = []; | |
545 for (var i = 0; i < arguments.length; i++) { | |
546 try { | |
547 // See if it even stringifies. | |
548 JSON.stringify(arguments[i]); | |
549 messages.push(arguments[i]); | |
550 } catch(ex) { | |
551 messages.push('Argument ' + i + ' not convertible to JSON'); | |
552 } | |
553 } | |
554 reqAsync('POST', '/print', messages); | |
555 } | |
556 | |
557 function sendEvent(event_name, args, opt_callback, opt_err_callback) { | |
558 if (args === undefined) | |
559 throw new Error('args is required'); | |
560 reqAsync('POST', '/send_event', { | |
561 event_name: event_name, | |
562 args: args}, | |
563 opt_callback, | |
564 opt_err_callback); | |
565 } | |
566 | |
567 var exiting = false; | |
568 function exit(opt_exitCode) { | |
569 var exitCode = opt_exitCode; | |
570 if (opt_exitCode === undefined) | |
571 exitCode = 0; | |
572 if (typeof(exitCode) != 'number') | |
573 throw new Error('exit code must be a number or undefined'); | |
574 if (exiting) | |
575 throw new Error('chromeapp.exit() was a already called'); | |
576 exiting = true; | |
577 | |
578 reqAsync('POST', '/exit', {exitCode: exitCode}, function() { }); | |
579 | |
580 // Busy wait for a bit to try to give the xhr a chance to hit | |
581 // python. | |
582 var start = Date.now(); | |
583 while(Date.now() < start + 150); | |
584 } | |
585 | |
586 window.chromeapp = chromeapp; | |
587 window.chromeapp.launch_args = undefined; | |
588 window.chromeapp.sendEvent = sendEvent; | |
589 window.chromeapp.print = print; | |
590 window.chromeapp.exit = exit; | |
591 })(); | |
592 """ | |
593 | |
594 assert self._daemon | |
595 self._cur_chromeapp_js = os.path.join( | |
596 self._app.manifest_dirname, | |
597 'chromeapp.js') | |
598 js = js_template | |
599 js = js.replace('__CHROMEAPP_REPLY_URL__', | |
600 'http://localhost:%i' % self._daemon.port) | |
601 js = js.replace('__CHROMEAPP_DEBUG_MODE__', | |
602 '%s' % json.dumps(self._app.debug_mode)) | |
603 with open(self._cur_chromeapp_js, 'w') as f: | |
604 f.write(js) | |
605 | |
606 def _CleanupLaunchJS(self): | |
607 if self._cur_chromeapp_js == None: | |
608 return | |
609 assert os.path.exists(self._cur_chromeapp_js) | |
610 os.unlink(self._cur_chromeapp_js) | |
611 self._cur_chromeapp_js = None | |
612 | |
613 def _Launch(self, browser_args): | |
614 if not self._app.debug_mode: | |
615 self._devnull = open(os.devnull, 'w') | |
616 self._proc = subprocess.Popen( | |
617 browser_args, stdout=self._devnull, stderr=self._devnull) | |
618 else: | |
619 self._devnull = None | |
620 self._proc = subprocess.Popen(browser_args) | |
621 | |
622 def _StartCheckingForBrowserAliveness(self): | |
623 self._daemon.AddDelayedTask(self._CheckForBrowserAliveness, 0.25) | |
624 | |
625 def _CheckForBrowserAliveness(self): | |
626 if not self._proc: | |
627 return | |
628 def IsStopped(): | |
629 return self._proc.poll() != None | |
630 if IsStopped(): | |
631 if not self._exiting_run_loop: | |
632 sys.stderr.write("Browser closed without notifying us. Exiting...\n") | |
633 self.ExitRunLoop(1) | |
634 return | |
635 self._StartCheckingForBrowserAliveness() | |
636 | |
637 def Run(self): | |
638 assert self._exit_code == None | |
639 assert self.is_started | |
640 self._StartCheckingForBrowserAliveness() | |
641 try: | |
642 self._daemon.Run() | |
643 finally: | |
644 self._exiting_run_loop = False | |
645 exit_code = self._exit_code | |
646 self._exit_code = None | |
647 return exit_code | |
648 | |
649 def _OnUncaughtError(self, error): | |
650 m = re.match('chrome-extension:\/\/(.+)\/(.*)', error['url'], re.DOTALL) | |
651 assert m | |
652 sys.stderr.write("Uncaught error: %s:%i: %s\n" % ( | |
653 m.group(2), error['line_number'], | |
654 error['error'])) | |
655 | |
656 def _OnPrint(self, content): | |
657 print "%s" % ' '.join([str(x) for x in content]) | |
658 | |
659 def AddListener(self, event_name, callback): | |
660 if event_name in self._event_listeners: | |
661 raise Exception('Event listener already registered') | |
662 self._event_listeners[event_name] = callback | |
663 | |
664 def RemoveListener(self, event_name, callback): | |
665 if event_name not in self._event_listeners: | |
666 raise Exception("Not found") | |
667 del self._event_listeners[event_name] | |
668 | |
669 def HasListener(self, event_name, callback): | |
670 return event_name in self._event_listeners | |
671 | |
672 def _OnSendEvent(self, content): | |
673 event_name = content["event_name"] | |
674 args = content["args"] | |
675 listener = self._event_listeners.get(event_name, None) | |
676 if not listener: | |
677 sys.stderr.write('No listener for %s\n' % event_name) | |
678 raise _RequestException(500) | |
679 | |
680 try: | |
681 return listener(args) | |
682 except: | |
683 import traceback | |
684 traceback.print_exc() | |
685 raise _RequestException(500) | |
686 | |
687 def _HandleRequest(self, method, path, content): | |
688 parsed_result = urlparse.urlparse(path) | |
689 if path == '/launch_args': | |
690 return self._args | |
691 | |
692 if path == '/uncaught_error': | |
693 self._OnUncaughtError(content) | |
694 return | |
695 | |
696 if path == '/print': | |
697 self._OnPrint(content) | |
698 return | |
699 | |
700 if path == '/send_event': | |
701 return self._OnSendEvent(content) | |
702 | |
703 if path == '/exit': | |
704 self.ExitRunLoop(content['exitCode']) | |
705 return True | |
706 | |
707 raise _RequestException(404) | |
708 | |
709 def ExitRunLoop(self, exit_code): | |
710 """Forces the app out of its run loop.""" | |
711 assert self._daemon.is_running | |
712 if self._exiting_run_loop: | |
713 logging.warning("Multiple calls to exit. First return value will be chosen
.") | |
714 return | |
715 self._exiting_run_loop = True | |
716 self._exit_code = exit_code | |
717 self._daemon.Stop() | |
718 | |
719 def _CloseBrowserProcess(self): | |
720 assert self.is_started | |
721 assert not self._daemon.is_running | |
722 | |
723 if self._proc: | |
724 | |
725 def IsStopped(): | |
726 if not self._proc: | |
727 return True | |
728 return self._proc.poll() != None | |
729 | |
730 # Try to politely shutdown, first. | |
731 if not IsStopped(): | |
732 try: | |
733 self._proc.terminate() | |
734 try: | |
735 _WaitFor(IsStopped, timeout=5) | |
736 self._proc = None | |
737 except _TimeoutException: | |
738 pass | |
739 except: | |
740 pass | |
741 | |
742 # Kill it. | |
743 if not IsStopped(): | |
744 self._proc.kill() | |
745 try: | |
746 _WaitFor(IsStopped, timeout=5) | |
747 self._proc = None | |
748 except _TimeoutException: | |
749 self._proc = None | |
750 raise Exception('Could not shutdown the browser.') | |
751 | |
752 if self._devnull: | |
753 self._devnull.close() | |
754 self._devnull = None | |
755 | |
756 self._CleanupLaunchJS() | |
757 | |
758 class ManifestError(Exception): | |
759 pass | |
760 | |
761 class App(object): | |
762 def __init__(self, stable_app_name, manifest_filename, | |
763 debug_mode=False, | |
764 chromeapp_profiles_dir=False): | |
765 self._stable_app_name = stable_app_name | |
766 self._profile_dir = None | |
767 | |
768 self._manifest_filename = manifest_filename | |
769 self._debug_mode = debug_mode | |
770 | |
771 with open(self._manifest_filename, 'r') as f: | |
772 manifest_text = f.read() | |
773 self._manifest = json.loads(manifest_text) | |
774 | |
775 if 'permissions' not in self._manifest: | |
776 raise ManifestError('You need to have permissions: "http://localhost:*/" i
n your manifest.') | |
777 if 'http://localhost:*/' not in self._manifest['permissions']: | |
778 raise ManifestError('You need to have permissions: "http://localhost:*/" i
n your manifest.') | |
779 | |
780 if not chromeapp_profiles_dir: | |
781 chromeapp_profiles_dir = os.path.expanduser('~/.chromeapp') | |
782 if not os.path.exists(chromeapp_profiles_dir): | |
783 os.mkdir(chromeapp_profiles_dir) | |
784 | |
785 self._profile_dir = os.path.join(chromeapp_profiles_dir, | |
786 stable_app_name) | |
787 | |
788 def Run(self, args=None): | |
789 """Launches and runs instance of the application. Returns its exit code. | |
790 | |
791 This is shorthand for creating an AppInstance against this app and running i
t: | |
792 with AppInstance(app, args) as instance: | |
793 ret_val = instance.Run() | |
794 """ | |
795 with AppInstance(self, args) as instance: | |
796 return instance.Run() | |
797 | |
798 @property | |
799 def stable_app_name(self): | |
800 return self._stable_app_name | |
801 | |
802 @property | |
803 def debug_mode(self): | |
804 return self._debug_mode | |
805 | |
806 @property | |
807 def manifest_filename(self): | |
808 return self._manifest_filename | |
809 | |
810 @property | |
811 def manifest_dirname(self): | |
812 return os.path.abspath(os.path.dirname(self._manifest_filename)) | |
813 | |
814 def _GetBrowserStartupArgs(self): | |
815 args = [] | |
816 args.append('--user-data-dir=%s' % self._profile_dir) | |
817 args.append('--no-first-run') | |
818 args.append('--noerrdialogs') | |
819 args.append('--enable-experimental-extension-apis') | |
820 return args | |
821 | |
822 def _ReadPreferences(self): | |
823 prefs_file = os.path.join(self._profile_dir, | |
824 'Default', 'Preferences') | |
825 try: | |
826 with open(prefs_file, 'r') as f: | |
827 contents = f.read() | |
828 except: | |
829 contents = """{ | |
830 "extensions": { | |
831 "settings": { | |
832 } | |
833 } | |
834 }""" | |
835 return json.loads(contents) | |
OLD | NEW |