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 |