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 |