| OLD | NEW |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import datetime | 5 import datetime |
| 6 import glob | 6 import glob |
| 7 import heapq | 7 import heapq |
| 8 import logging | 8 import logging |
| 9 import os | 9 import os |
| 10 import os.path | 10 import os.path |
| (...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 107 super(DesktopBrowserBackend, self).__init__( | 107 super(DesktopBrowserBackend, self).__init__( |
| 108 desktop_platform_backend, | 108 desktop_platform_backend, |
| 109 supports_tab_control=not is_content_shell, | 109 supports_tab_control=not is_content_shell, |
| 110 supports_extensions=not is_content_shell, | 110 supports_extensions=not is_content_shell, |
| 111 browser_options=browser_options) | 111 browser_options=browser_options) |
| 112 | 112 |
| 113 # Initialize fields so that an explosion during init doesn't break in Close. | 113 # Initialize fields so that an explosion during init doesn't break in Close. |
| 114 self._proc = None | 114 self._proc = None |
| 115 self._tmp_profile_dir = None | 115 self._tmp_profile_dir = None |
| 116 self._tmp_output_file = None | 116 self._tmp_output_file = None |
| 117 self._most_recent_symbolized_minidump_paths = set([]) |
| 117 | 118 |
| 118 self._executable = executable | 119 self._executable = executable |
| 119 if not self._executable: | 120 if not self._executable: |
| 120 raise Exception('Cannot create browser, no executable found!') | 121 raise Exception('Cannot create browser, no executable found!') |
| 121 | 122 |
| 122 assert not flash_path or os.path.exists(flash_path) | 123 assert not flash_path or os.path.exists(flash_path) |
| 123 self._flash_path = flash_path | 124 self._flash_path = flash_path |
| 124 | 125 |
| 125 self._is_content_shell = is_content_shell | 126 self._is_content_shell = is_content_shell |
| 126 | 127 |
| 127 extensions_to_load = browser_options.extensions_to_load | 128 extensions_to_load = browser_options.extensions_to_load |
| 128 | 129 |
| 129 if len(extensions_to_load) > 0 and is_content_shell: | 130 if len(extensions_to_load) > 0 and is_content_shell: |
| 130 raise browser_backend.ExtensionsNotSupportedException( | 131 raise browser_backend.ExtensionsNotSupportedException( |
| 131 'Content shell does not support extensions.') | 132 'Content shell does not support extensions.') |
| 132 | 133 |
| 133 self._browser_directory = browser_directory | 134 self._browser_directory = browser_directory |
| 134 self._port = None | 135 self._port = None |
| 135 self._tmp_minidump_dir = tempfile.mkdtemp() | 136 self._tmp_minidump_dir = tempfile.mkdtemp() |
| 136 if self.browser_options.enable_logging: | 137 if self.is_logging_enabled: |
| 137 self._log_file_path = os.path.join(tempfile.mkdtemp(), 'chrome.log') | 138 self._log_file_path = os.path.join(tempfile.mkdtemp(), 'chrome.log') |
| 138 else: | 139 else: |
| 139 self._log_file_path = None | 140 self._log_file_path = None |
| 140 | 141 |
| 141 self._SetupProfile() | 142 self._SetupProfile() |
| 142 | 143 |
| 143 @property | 144 @property |
| 145 def is_logging_enabled(self): |
| 146 return self.browser_options.logging_verbosity in [ |
| 147 self.browser_options.NON_VERBOSE_LOGGING, |
| 148 self.browser_options.VERBOSE_LOGGING] |
| 149 |
| 150 @property |
| 144 def log_file_path(self): | 151 def log_file_path(self): |
| 145 return self._log_file_path | 152 return self._log_file_path |
| 146 | 153 |
| 147 @property | 154 @property |
| 148 def supports_uploading_logs(self): | 155 def supports_uploading_logs(self): |
| 149 return (self.browser_options.logs_cloud_bucket and self.log_file_path and | 156 return (self.browser_options.logs_cloud_bucket and self.log_file_path and |
| 150 os.path.isfile(self.log_file_path)) | 157 os.path.isfile(self.log_file_path)) |
| 151 | 158 |
| 152 def _SetupProfile(self): | 159 def _SetupProfile(self): |
| 153 if not self.browser_options.dont_override_profile: | 160 if not self.browser_options.dont_override_profile: |
| (...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 261 def Start(self): | 268 def Start(self): |
| 262 assert not self._proc, 'Must call Close() before Start()' | 269 assert not self._proc, 'Must call Close() before Start()' |
| 263 | 270 |
| 264 args = [self._executable] | 271 args = [self._executable] |
| 265 args.extend(self.GetBrowserStartupArgs()) | 272 args.extend(self.GetBrowserStartupArgs()) |
| 266 if self.browser_options.startup_url: | 273 if self.browser_options.startup_url: |
| 267 args.append(self.browser_options.startup_url) | 274 args.append(self.browser_options.startup_url) |
| 268 env = os.environ.copy() | 275 env = os.environ.copy() |
| 269 env['CHROME_HEADLESS'] = '1' # Don't upload minidumps. | 276 env['CHROME_HEADLESS'] = '1' # Don't upload minidumps. |
| 270 env['BREAKPAD_DUMP_LOCATION'] = self._tmp_minidump_dir | 277 env['BREAKPAD_DUMP_LOCATION'] = self._tmp_minidump_dir |
| 271 if self.browser_options.enable_logging: | 278 if self.is_logging_enabled: |
| 272 sys.stderr.write( | 279 sys.stderr.write( |
| 273 'Chrome log file will be saved in %s\n' % self.log_file_path) | 280 'Chrome log file will be saved in %s\n' % self.log_file_path) |
| 274 env['CHROME_LOG_FILE'] = self.log_file_path | 281 env['CHROME_LOG_FILE'] = self.log_file_path |
| 275 logging.info('Starting Chrome %s', args) | 282 logging.info('Starting Chrome %s', args) |
| 276 if not self.browser_options.show_stdout: | 283 if not self.browser_options.show_stdout: |
| 277 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0) | 284 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0) |
| 278 self._proc = subprocess.Popen( | 285 self._proc = subprocess.Popen( |
| 279 args, stdout=self._tmp_output_file, stderr=subprocess.STDOUT, env=env) | 286 args, stdout=self._tmp_output_file, stderr=subprocess.STDOUT, env=env) |
| 280 else: | 287 else: |
| 281 self._proc = subprocess.Popen(args, env=env) | 288 self._proc = subprocess.Popen(args, env=env) |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 319 # recursive call to this function. | 326 # recursive call to this function. |
| 320 print >> sys.stderr, "Can't get standard output with --show-stdout" | 327 print >> sys.stderr, "Can't get standard output with --show-stdout" |
| 321 return '' | 328 return '' |
| 322 self._tmp_output_file.flush() | 329 self._tmp_output_file.flush() |
| 323 try: | 330 try: |
| 324 with open(self._tmp_output_file.name) as f: | 331 with open(self._tmp_output_file.name) as f: |
| 325 return f.read() | 332 return f.read() |
| 326 except IOError: | 333 except IOError: |
| 327 return '' | 334 return '' |
| 328 | 335 |
| 329 def _GetMostRecentCrashpadMinidump(self): | 336 def _GetAllCrashpadMinidumps(self): |
| 330 os_name = self.browser.platform.GetOSName() | 337 os_name = self.browser.platform.GetOSName() |
| 331 arch_name = self.browser.platform.GetArchName() | 338 arch_name = self.browser.platform.GetArchName() |
| 332 try: | 339 try: |
| 333 crashpad_database_util = binary_manager.FetchPath( | 340 crashpad_database_util = binary_manager.FetchPath( |
| 334 'crashpad_database_util', arch_name, os_name) | 341 'crashpad_database_util', arch_name, os_name) |
| 335 if not crashpad_database_util: | 342 if not crashpad_database_util: |
| 336 logging.warning('No crashpad_database_util found') | 343 logging.warning('No crashpad_database_util found') |
| 337 return None | 344 return None |
| 338 except dependency_manager.NoPathFoundError: | 345 except dependency_manager.NoPathFoundError: |
| 339 logging.warning('No path to crashpad_database_util found') | 346 logging.warning('No path to crashpad_database_util found') |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 378 # Include the last report. | 385 # Include the last report. |
| 379 if report_dict: | 386 if report_dict: |
| 380 try: | 387 try: |
| 381 report_time = ParseCrashpadDateTime(report_dict['Creation time']) | 388 report_time = ParseCrashpadDateTime(report_dict['Creation time']) |
| 382 report_path = report_dict['Path'].strip() | 389 report_path = report_dict['Path'].strip() |
| 383 reports_list.append((report_time, report_path)) | 390 reports_list.append((report_time, report_path)) |
| 384 except (ValueError, KeyError) as e: | 391 except (ValueError, KeyError) as e: |
| 385 logging.warning('Crashpad report expected valid keys' | 392 logging.warning('Crashpad report expected valid keys' |
| 386 ' "Path" and "Creation time": %s', e) | 393 ' "Path" and "Creation time": %s', e) |
| 387 | 394 |
| 395 return reports_list |
| 396 |
| 397 def _GetMostRecentCrashpadMinidump(self): |
| 398 reports_list = self._GetAllCrashpadMinidumps() |
| 388 if reports_list: | 399 if reports_list: |
| 389 _, most_recent_report_path = max(reports_list) | 400 _, most_recent_report_path = max(reports_list) |
| 390 return most_recent_report_path | 401 return most_recent_report_path |
| 391 | 402 |
| 392 return None | 403 return None |
| 393 | 404 |
| 405 def _GetBreakPadMinidumpPaths(self): |
| 406 return glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp')) |
| 407 |
| 394 def _GetMostRecentMinidump(self): | 408 def _GetMostRecentMinidump(self): |
| 395 # Crashpad dump layout will be the standard eventually, check it first. | 409 # Crashpad dump layout will be the standard eventually, check it first. |
| 396 most_recent_dump = self._GetMostRecentCrashpadMinidump() | 410 most_recent_dump = self._GetMostRecentCrashpadMinidump() |
| 397 | 411 |
| 398 # Typical breakpad format is simply dump files in a folder. | 412 # Typical breakpad format is simply dump files in a folder. |
| 399 if not most_recent_dump: | 413 if not most_recent_dump: |
| 400 logging.info('No minidump found via crashpad_database_util') | 414 logging.info('No minidump found via crashpad_database_util') |
| 401 dumps = glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp')) | 415 dumps = self._GetBreakPadMinidumpPaths() |
| 402 if dumps: | 416 if dumps: |
| 403 most_recent_dump = heapq.nlargest(1, dumps, os.path.getmtime)[0] | 417 most_recent_dump = heapq.nlargest(1, dumps, os.path.getmtime)[0] |
| 404 if most_recent_dump: | 418 if most_recent_dump: |
| 405 logging.info('Found minidump via globbing in minidump dir') | 419 logging.info('Found minidump via globbing in minidump dir') |
| 406 | 420 |
| 407 # As a sanity check, make sure the crash dump is recent. | 421 # As a sanity check, make sure the crash dump is recent. |
| 408 if (most_recent_dump and | 422 if (most_recent_dump and |
| 409 os.path.getmtime(most_recent_dump) < (time.time() - (5 * 60))): | 423 os.path.getmtime(most_recent_dump) < (time.time() - (5 * 60))): |
| 410 logging.warning('Crash dump is older than 5 minutes. May not be correct.') | 424 logging.warning('Crash dump is older than 5 minutes. May not be correct.') |
| 411 | 425 |
| (...skipping 16 matching lines...) Expand all Loading... |
| 428 else: | 442 else: |
| 429 return False | 443 return False |
| 430 | 444 |
| 431 def _GetStackFromMinidump(self, minidump): | 445 def _GetStackFromMinidump(self, minidump): |
| 432 os_name = self.browser.platform.GetOSName() | 446 os_name = self.browser.platform.GetOSName() |
| 433 if os_name == 'win': | 447 if os_name == 'win': |
| 434 cdb = self._GetCdbPath() | 448 cdb = self._GetCdbPath() |
| 435 if not cdb: | 449 if not cdb: |
| 436 logging.warning('cdb.exe not found.') | 450 logging.warning('cdb.exe not found.') |
| 437 return None | 451 return None |
| 452 # Include all the threads' stacks ("~*kb30") in addition to the |
| 453 # ostensibly crashed stack associated with the exception context |
| 454 # record (".ecxr;kb30"). Note that stack dumps, including that |
| 455 # for the crashed thread, may not be as precise as the one |
| 456 # starting from the exception context record. |
| 457 # Specify kb instead of k in order to get four arguments listed, for |
| 458 # easier diagnosis from stacks. |
| 438 output = subprocess.check_output([cdb, '-y', self._browser_directory, | 459 output = subprocess.check_output([cdb, '-y', self._browser_directory, |
| 439 '-c', '.ecxr;k30;q', '-z', minidump]) | 460 '-c', '.ecxr;kb30;~*kb30;q', |
| 461 '-z', minidump]) |
| 440 # cdb output can start the stack with "ChildEBP", "Child-SP", and possibly | 462 # cdb output can start the stack with "ChildEBP", "Child-SP", and possibly |
| 441 # other things we haven't seen yet. If we can't find the start of the | 463 # other things we haven't seen yet. If we can't find the start of the |
| 442 # stack, include output from the beginning. | 464 # stack, include output from the beginning. |
| 443 stack_start = 0 | 465 stack_start = 0 |
| 444 stack_start_match = re.search("^Child(?:EBP|-SP)", output, re.MULTILINE) | 466 stack_start_match = re.search("^Child(?:EBP|-SP)", output, re.MULTILINE) |
| 445 if stack_start_match: | 467 if stack_start_match: |
| 446 stack_start = stack_start_match.start() | 468 stack_start = stack_start_match.start() |
| 447 stack_end = output.find('quit:') | 469 stack_end = output.find('quit:') |
| 448 return output[stack_start:stack_end] | 470 return output[stack_start:stack_end] |
| 449 | 471 |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 483 def GetStackTrace(self): | 505 def GetStackTrace(self): |
| 484 """Returns a stack trace if a valid minidump is found, will return a tuple | 506 """Returns a stack trace if a valid minidump is found, will return a tuple |
| 485 (valid, output) where valid will be True if a valid minidump was found | 507 (valid, output) where valid will be True if a valid minidump was found |
| 486 and output will contain either an error message or the attempt to | 508 and output will contain either an error message or the attempt to |
| 487 symbolize the minidump if one was found. | 509 symbolize the minidump if one was found. |
| 488 """ | 510 """ |
| 489 most_recent_dump = self._GetMostRecentMinidump() | 511 most_recent_dump = self._GetMostRecentMinidump() |
| 490 if not most_recent_dump: | 512 if not most_recent_dump: |
| 491 return (False, 'No crash dump found.') | 513 return (False, 'No crash dump found.') |
| 492 logging.info('Minidump found: %s' % most_recent_dump) | 514 logging.info('Minidump found: %s' % most_recent_dump) |
| 493 stack = self._GetStackFromMinidump(most_recent_dump) | 515 return self._InternalSymbolizeMinidump(most_recent_dump) |
| 516 |
| 517 def GetMostRecentMinidumpPath(self): |
| 518 return self._GetMostRecentMinidump() |
| 519 |
| 520 def GetAllMinidumpPaths(self): |
| 521 reports_list = self._GetAllCrashpadMinidumps() |
| 522 if reports_list: |
| 523 return [report[1] for report in reports_list] |
| 524 else: |
| 525 logging.info('No minidump found via crashpad_database_util') |
| 526 dumps = self._GetBreakPadMinidumpPaths() |
| 527 if dumps: |
| 528 logging.info('Found minidump via globbing in minidump dir') |
| 529 return dumps |
| 530 return None |
| 531 |
| 532 def GetAllUnsymbolizedMinidumpPaths(self): |
| 533 minidump_paths = set(self.GetAllMinidumpPaths()) |
| 534 # If we have already symbolized paths remove them from the list |
| 535 unsymbolized_paths = (minidump_paths |
| 536 - self._most_recent_symbolized_minidump_paths) |
| 537 return list(unsymbolized_paths) |
| 538 |
| 539 def SymbolizeMinidump(self, minidump_path): |
| 540 return self._InternalSymbolizeMinidump(minidump_path) |
| 541 |
| 542 def _InternalSymbolizeMinidump(self, minidump_path): |
| 543 stack = self._GetStackFromMinidump(minidump_path) |
| 494 if not stack: | 544 if not stack: |
| 495 cloud_storage_link = self._UploadMinidumpToCloudStorage(most_recent_dump) | 545 cloud_storage_link = self._UploadMinidumpToCloudStorage(minidump_path) |
| 496 error_message = ('Failed to symbolize minidump. Raw stack is uploaded to' | 546 error_message = ('Failed to symbolize minidump. Raw stack is uploaded to' |
| 497 ' cloud storage: %s.' % cloud_storage_link) | 547 ' cloud storage: %s.' % cloud_storage_link) |
| 498 return (False, error_message) | 548 return (False, error_message) |
| 499 | 549 |
| 550 self._most_recent_symbolized_minidump_paths.add(minidump_path) |
| 500 return (True, stack) | 551 return (True, stack) |
| 501 | 552 |
| 502 def __del__(self): | 553 def __del__(self): |
| 503 self.Close() | 554 self.Close() |
| 504 | 555 |
| 505 def _TryCooperativeShutdown(self): | 556 def _TryCooperativeShutdown(self): |
| 506 if self.browser.platform.IsCooperativeShutdownSupported(): | 557 if self.browser.platform.IsCooperativeShutdownSupported(): |
| 507 # Ideally there would be a portable, cooperative shutdown | 558 # Ideally there would be a portable, cooperative shutdown |
| 508 # mechanism for the browser. This seems difficult to do | 559 # mechanism for the browser. This seems difficult to do |
| 509 # correctly for all embedders of the content API. The only known | 560 # correctly for all embedders of the content API. The only known |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 551 shutil.rmtree(self._tmp_profile_dir, ignore_errors=True) | 602 shutil.rmtree(self._tmp_profile_dir, ignore_errors=True) |
| 552 self._tmp_profile_dir = None | 603 self._tmp_profile_dir = None |
| 553 | 604 |
| 554 if self._tmp_output_file: | 605 if self._tmp_output_file: |
| 555 self._tmp_output_file.close() | 606 self._tmp_output_file.close() |
| 556 self._tmp_output_file = None | 607 self._tmp_output_file = None |
| 557 | 608 |
| 558 if self._tmp_minidump_dir: | 609 if self._tmp_minidump_dir: |
| 559 shutil.rmtree(self._tmp_minidump_dir, ignore_errors=True) | 610 shutil.rmtree(self._tmp_minidump_dir, ignore_errors=True) |
| 560 self._tmp_minidump_dir = None | 611 self._tmp_minidump_dir = None |
| OLD | NEW |