| #!src/build/run_python |
| |
| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import atexit |
| import logging |
| import os |
| import re |
| import signal |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| from src.build import build_common |
| from src.build import launch_chrome_options |
| from src.build import prep_launch_chrome |
| from src.build import toolchain |
| from src.build.build_options import OPTIONS |
| from src.build.util import chrome_process |
| from src.build.util import concurrent_subprocess |
| from src.build.util import file_util |
| from src.build.util import gdb_util |
| from src.build.util import jdb_util |
| from src.build.util import launch_chrome_util |
| from src.build.util import logging_util |
| from src.build.util import minidump_filter |
| from src.build.util import output_handler |
| from src.build.util import platform_util |
| from src.build.util import remote_executor |
| from src.build.util import signal_util |
| from src.build.util import startup_stats |
| |
| |
| _ROOT_DIR = build_common.get_arc_root() |
| |
| _CHROME_KILL_DELAY = 0.1 |
| |
| _CHROME_KILL_TIMEOUT = 10 |
| |
| _CHROME_PID_PATH = None |
| |
| _PERF_TOOL = 'perf' |
| |
| _USER_DATA_DIR = None # Will be set after we parse the commandline flags. |
| |
| # List of lines for stdout/stderr Chrome output to suppress. |
| _SUPPRESS_LIST = [ |
| # When debugging with gdb, NaCl is emitting many of these messages. |
| 'NaClAppThreadSetSuspendedRegisters: Registers not modified', |
| ] |
| |
| |
| # Caution: The feature to kill the running chrome has race condition, that this |
| # may kill unrelated process. The file can be rewritten at anytime, so there |
| # is no simpler way to guarantee that the pid read from the file is reliable. |
| # Note that technically we should be able to lock the file, but python does |
| # not provide portable implementation. |
| def _read_chrome_pid_file(): |
| if not os.path.exists(_CHROME_PID_PATH): |
| return None |
| |
| with open(_CHROME_PID_PATH) as pid_file: |
| lines = pid_file.readlines() |
| if not lines: |
| logging.error('chrome.pid is empty.') |
| return None |
| try: |
| return int(lines[0]) |
| except ValueError: |
| logging.error('Invalid content of chrome.pid: ' + lines[0]) |
| return None |
| |
| |
| def _kill_running_chrome(): |
| pid = _read_chrome_pid_file() |
| if pid is None: |
| return |
| |
| try: |
| # Use SIGKILL instead of more graceful signals, as Chrome's |
| # behavior for other signals are not well defined nor |
| # tested. Actually, we cannot kill Chrome with NaCl's debug stub |
| # enabled by SIGTERM. Also, there was a similar issue in Bare |
| # Metal mode with M39 Chrome. See crbug.com/433967. |
| os.kill(pid, signal.SIGKILL) |
| |
| # Unfortunately, there is no convenient API to wait subprocess's |
| # termination with timeout. So, here we just poll it. |
| wait_time_limit = time.time() + _CHROME_KILL_TIMEOUT |
| while True: |
| retpid, status = os.waitpid(pid, os.WNOHANG) |
| if retpid: |
| break |
| now = time.time() |
| if now > wait_time_limit: |
| logging.error('Terminating Chrome is timed out: %d', pid) |
| break |
| time.sleep(min(_CHROME_KILL_DELAY, wait_time_limit - now)) |
| except OSError: |
| # Here we ignore the OS error. The process may have been terminated somehow |
| # by external reason, while the file still exists on the file system. |
| pass |
| |
| _remove_chrome_pid_file(pid) |
| |
| |
| def _remove_chrome_pid_file(pid): |
| read_pid = _read_chrome_pid_file() |
| if read_pid == pid: |
| try: |
| os.remove(_CHROME_PID_PATH) |
| except OSError: |
| # The file may already be removed due to timing issue. Ignore the error. |
| pass |
| |
| |
| def _prepare_chrome_user_data_dir(parsed_args): |
| global _USER_DATA_DIR |
| if parsed_args.use_temporary_data_dirs: |
| _USER_DATA_DIR = tempfile.mkdtemp( |
| prefix=build_common.CHROME_USER_DATA_DIR_PREFIX + '-') |
| atexit.register(lambda: file_util.rmtree_with_retries(_USER_DATA_DIR)) |
| elif parsed_args.user_data_dir: |
| _USER_DATA_DIR = parsed_args.user_data_dir |
| else: |
| _USER_DATA_DIR = build_common.get_chrome_default_user_data_dir() |
| |
| |
| def _maybe_wait_iteration_lock(parsed_args): |
| if not parsed_args.iteration_lock_file: |
| return |
| with open(parsed_args.iteration_lock_file, 'w'): |
| pass |
| # This message is hard-coded in interleaved_perftest.py. Don't change it. |
| sys.stderr.write('waiting for next iteration\n') |
| while os.path.exists(parsed_args.iteration_lock_file): |
| time.sleep(0.1) |
| |
| |
| def _run_chrome_iterations(parsed_args): |
| if not parsed_args.no_cache_warming: |
| _maybe_wait_iteration_lock(parsed_args) |
| stats = _run_chrome(parsed_args, cache_warming=True) |
| if parsed_args.mode == 'perftest': |
| total = (stats.pre_embed_time_ms + stats.plugin_load_time_ms + |
| stats.on_resume_time_ms) |
| print 'WARM-UP %d %d %d %d' % (total, |
| stats.pre_embed_time_ms, |
| stats.plugin_load_time_ms, |
| stats.on_resume_time_ms) |
| startup_stats.print_raw_stats(stats) |
| sys.stdout.flush() |
| |
| if parsed_args.iterations > 0: |
| stat_list = [] |
| for i in xrange(parsed_args.iterations): |
| _maybe_wait_iteration_lock(parsed_args) |
| sys.stderr.write('\nStarting Chrome\n') |
| stats = _run_chrome(parsed_args) |
| startup_stats.print_raw_stats(stats) |
| sys.stdout.flush() |
| stat_list.append(stats) |
| |
| startup_stats.print_aggregated_stats(stat_list) |
| sys.stdout.flush() |
| |
| |
| def _check_apk_existence(parsed_args): |
| for apk_path in parsed_args.apk_path_list: |
| if not os.path.exists(apk_path): |
| raise Exception('APK does not exist:' + apk_path) |
| |
| |
| def _check_crx_existence(parsed_args): |
| if (not parsed_args.build_crx and |
| not os.path.exists(parsed_args.arc_data_dir)): |
| raise Exception('--nocrxbuild is used but CRX does not exist in %s.\n' |
| 'Try launching chrome without --nocrxbuild in order to ' |
| 'rebuild the CRX.' % parsed_args.arc_data_dir) |
| |
| |
| def _get_chrome_path(parsed_args): |
| if parsed_args.chrome_binary: |
| return parsed_args.chrome_binary |
| else: |
| return remote_executor.get_chrome_exe_path() |
| |
| |
| def _get_nacl_helper_nonsfi_path(parsed_args): |
| if parsed_args.nacl_helper_nonsfi_binary: |
| return parsed_args.nacl_helper_nonsfi_binary |
| chrome_path = _get_chrome_path(parsed_args) |
| return os.path.join(os.path.dirname(chrome_path), 'nacl_helper_nonsfi') |
| |
| |
| def _get_nacl_irt_path(parsed_args): |
| if not OPTIONS.is_nacl_build(): |
| return None |
| chrome_path = _get_chrome_path(parsed_args) |
| irt = toolchain.get_tool(OPTIONS.target(), 'irt') |
| nacl_irt_path = os.path.join(os.path.dirname(chrome_path), irt) |
| nacl_irt_debug_path = nacl_irt_path + '.debug' |
| # Use debug version nacl_irt if it exists. |
| if os.path.exists(nacl_irt_debug_path): |
| return nacl_irt_debug_path |
| else: |
| return nacl_irt_path |
| |
| |
| def main(): |
| signal_util.setup() |
| |
| OPTIONS.parse_configure_file() |
| |
| parsed_args = launch_chrome_options.parse_args(sys.argv) |
| logging_util.setup( |
| level=logging.DEBUG if parsed_args.verbose else logging.INFO) |
| |
| _prepare_chrome_user_data_dir(parsed_args) |
| global _CHROME_PID_PATH |
| _CHROME_PID_PATH = os.path.join(_USER_DATA_DIR, 'chrome.pid') |
| |
| # If there is an X server at :0.0 and GPU is enabled, set it as the |
| # current display. |
| if parsed_args.display: |
| os.environ['DISPLAY'] = parsed_args.display |
| |
| os.chdir(_ROOT_DIR) |
| |
| if not parsed_args.remote: |
| _kill_running_chrome() |
| |
| if parsed_args.run_ninja: |
| build_common.run_ninja() |
| |
| ld_library_path = os.environ.get('LD_LIBRARY_PATH') |
| lib_paths = ld_library_path.split(':') if ld_library_path else [] |
| lib_paths.append(build_common.get_load_library_path()) |
| # Add the directory of the chrome binary so that .so files in the directory |
| # can be loaded. This is needed for loading libudev.so.0. |
| # TODO(crbug.com/375609): Remove the hack once it becomes no longer needed. |
| lib_paths.append(os.path.dirname(_get_chrome_path(parsed_args))) |
| os.environ['LD_LIBRARY_PATH'] = ':'.join(lib_paths) |
| launch_chrome_util.set_environment_for_chrome() |
| |
| if not platform_util.is_running_on_remote_host(): |
| _check_apk_existence(parsed_args) |
| |
| if not platform_util.is_running_on_remote_host(): |
| prep_launch_chrome.prepare_crx(parsed_args) |
| prep_launch_chrome.remove_crx_at_exit_if_needed(parsed_args) |
| |
| if parsed_args.remote: |
| remote_executor.launch_remote_chrome(parsed_args, sys.argv[1:]) |
| else: |
| platform_util.assert_machine(OPTIONS.target()) |
| _check_crx_existence(parsed_args) |
| _run_chrome_iterations(parsed_args) |
| |
| return 0 |
| |
| |
| def _compute_chrome_plugin_params(parsed_args): |
| params = [] |
| params.append( |
| '--load-extension=' + |
| remote_executor.resolve_path(build_common.get_runtime_out_dir())) |
| |
| # Do not use user defined data directory if user name for remote host is |
| # provided. The mounted cryptohome directory is used instead. |
| if not parsed_args.login_user: |
| params.append( |
| '--user-data-dir=' + remote_executor.resolve_path(_USER_DATA_DIR)) |
| |
| # Force-enable nonsfi mode on targets that do not allow it by default |
| # (for example, non-ChromeOS Linux) for testing purposes. |
| if (OPTIONS.is_bare_metal_build() and |
| not platform_util.is_running_on_chromeos()): |
| params.append('--enable-nacl-nonsfi-mode') |
| |
| return params |
| |
| |
| def _is_no_sandbox_needed(parsed_args): |
| if parsed_args.disable_nacl_sandbox: |
| return True |
| |
| # Official Chrome needs setuid + root ownership to run. --no-sandbox |
| # bypasses that. |
| if OPTIONS.is_official_chrome(): |
| return True |
| |
| # In some cases, --no-sandbox is needed to work gdb properly. |
| if gdb_util.is_no_sandbox_needed(parsed_args.gdb): |
| return True |
| |
| # Set --no-sandbox on Mac for now because ARC apps crash on Mac Chromium |
| # without the flag. |
| # TODO(crbug.com/332785): Investigate the cause of crash and remove the flag |
| # if possible. |
| if platform_util.is_running_on_mac(): |
| return True |
| |
| return False |
| |
| |
| def _compute_chrome_sandbox_params(parsed_args): |
| params = [] |
| if _is_no_sandbox_needed(parsed_args): |
| params.append('--no-sandbox') |
| if OPTIONS.is_bare_metal_build(): |
| # Non-SFI NaCl helper, which heavily depends on seccomp-bpf, |
| # does not start without seccomp sandbox initialized unless we |
| # specify this flag explicitly. |
| params.append('--nacl-dangerous-no-sandbox-nonsfi') |
| |
| # Environment variables to pass through to nacl_helper. |
| passthrough_env_vars = [] |
| |
| if OPTIONS.is_nacl_build() and parsed_args.disable_nacl_sandbox: |
| os.environ['NACL_DANGEROUS_ENABLE_FILE_ACCESS'] = '1' |
| passthrough_env_vars.append('NACL_DANGEROUS_ENABLE_FILE_ACCESS') |
| if OPTIONS.is_nacl_build() and parsed_args.enable_nacl_list_mappings: |
| os.environ['NACL_DANGEROUS_ENABLE_LIST_MAPPINGS'] = '1' |
| passthrough_env_vars.append('NACL_DANGEROUS_ENABLE_LIST_MAPPINGS') |
| if passthrough_env_vars: |
| os.environ['NACL_ENV_PASSTHROUGH'] = ','.join(passthrough_env_vars) |
| return params |
| |
| |
| def _compute_chrome_graphics_params(parsed_args): |
| params = [] |
| params.append('--disable-gl-error-limit') |
| |
| # Always use the compositor thread. All desktop Chrome except Linux already |
| # use it. |
| params.append('--enable-threaded-compositing') |
| |
| if parsed_args.enable_osmesa: |
| params.append('--use-gl=osmesa') |
| |
| # The NVidia GPU on buildbot is blacklisted due to unstableness of graphic |
| # driver even there is secondary Matrox GPU(http://crbug.com/145600). It |
| # happens with low memory but seems safe for buildbot. So passing |
| # ignore-gpu-blacklist to be able to use hardware acceleration. |
| params.append('--ignore-gpu-blacklist') |
| |
| return params |
| |
| |
| def _compute_chrome_debugging_params(parsed_args): |
| params = [] |
| |
| # This reduce one step necessary to enable filesystem inspector. |
| params.append('--enable-devtools-experiments') |
| |
| if OPTIONS.is_nacl_build() and 'plugin' in parsed_args.gdb: |
| params.append('--enable-nacl-debug') |
| |
| if len(parsed_args.gdb): |
| params.append('--disable-hang-monitor') |
| |
| if 'gpu' in parsed_args.gdb: |
| params.append('--gpu-startup-dialog') |
| params.append('--disable-gpu-watchdog') |
| |
| if 'renderer' in parsed_args.gdb: |
| params.append('--renderer-startup-dialog') |
| |
| if parsed_args.enable_fake_video_source: |
| params.append('--use-fake-device-for-media-stream') |
| |
| return params |
| |
| |
| def _compute_chrome_diagnostic_params(parsed_args): |
| params_startwith = [] |
| params = [] |
| params.append('--enable-logging=stderr') |
| params.append('--log-level=0') |
| |
| if parsed_args.tracestartup > 0: |
| params.append('--trace-startup') |
| params.append('--trace-startup-duration=%d' % parsed_args.tracestartup) |
| |
| if parsed_args.perfstartup: |
| params_startwith.extend([ |
| 'timeout', '-s', 'INT', str(parsed_args.perfstartup), |
| _PERF_TOOL, 'record', '-g', '-o', 'out/perf.data']) |
| |
| return (params, params_startwith) |
| |
| |
| def _compute_chrome_performance_test_params(unused_parsed_args): |
| """Add params that are necessary for stable perftest result.""" |
| params = [] |
| |
| # Skip First Run tasks, whether or not it's actually the First Run. |
| params.append('--no-first-run') |
| |
| # Disable default component extensions with background pages - useful for |
| # performance tests where these pages may interfere with perf results. |
| params.append('--disable-component-extensions-with-background-pages') |
| |
| # Enable the recording of metrics reports but disable reporting. In contrast |
| # to kDisableMetrics, this executes all the code that a normal client would |
| # use for reporting, except the report is dropped rather than sent to the |
| # server. This is useful for finding issues in the metrics code during UI and |
| # performance tests. |
| params.append('--metrics-recording-only') |
| |
| # Disable several subsystems which run network requests in the background. |
| # This is for use when doing network performance testing to avoid noise in the |
| # measurements. |
| params.append('--disable-background-networking') |
| |
| # They are copied from |
| # ppapi/native_client/tools/browser_tester/browsertester/browserlauncher.py |
| # These features could be a source of non-determinism too. |
| params.append('--disable-default-apps') |
| params.append('--disable-preconnect') |
| params.append('--disable-sync') |
| params.append('--disable-web-resources') |
| params.append('--dns-prefetch-disable') |
| params.append('--no-default-browser-check') |
| params.append('--safebrowsing-disable-auto-update') |
| |
| return params |
| |
| |
| def _compute_chrome_params(parsed_args): |
| chrome_path = _get_chrome_path(parsed_args) |
| params = [chrome_path] |
| |
| if parsed_args.mode in ('perftest', 'atftest'): |
| # Do not show the New Tab Page because showing NTP during perftest makes the |
| # benchmark score look unnecessarily bad. |
| # TODO(crbug.com/315356): Remove the IF once 315356 is fixed. |
| params.append('about:blank') |
| # Append flags for performance measurement in test modes to stabilize |
| # integration tests and perf score. Do not append these flags in run mode |
| # because apps that depend on component extensions (e.g. Files.app) will not |
| # work with these flags. |
| params.extend(_compute_chrome_performance_test_params(parsed_args)) |
| # Make the window size small on Goobuntu so that it does not cover the whole |
| # desktop during perftest/integration_test. |
| params.append('--window-size=500,500') |
| |
| if parsed_args.lang: |
| params.append('--lang=' + parsed_args.lang) |
| # LANGUAGE takes priority over --lang option in Linux. |
| os.environ['LANGUAGE'] = parsed_args.lang |
| # In Mac, there is no handy way to change the locale. |
| if platform_util.is_running_on_mac(): |
| print '\nWARNING: --lang is not supported in Mac.' |
| |
| if (parsed_args.mode == 'atftest' and |
| not platform_util.is_running_on_chromeos() and |
| not platform_util.is_running_on_mac()): |
| # This launches ARC without creating a browser window. We only do it for |
| # automated tests, in case the user wants to do something like examine the |
| # Chromium settings ("about:flags" for example), which requires using the |
| # browser window. Note that this does not work on Mac, and should be |
| # unnecessary on a remote Chromebook target. |
| params.append('--silent-launch') |
| |
| params.extend(_compute_chrome_plugin_params(parsed_args)) |
| params.extend(_compute_chrome_sandbox_params(parsed_args)) |
| params.extend(_compute_chrome_graphics_params(parsed_args)) |
| params.extend(_compute_chrome_debugging_params(parsed_args)) |
| diagnostic_params, diagnostic_params_startwith = ( |
| _compute_chrome_diagnostic_params(parsed_args)) |
| params.extend(diagnostic_params) |
| params = diagnostic_params_startwith + params |
| remote_executor.maybe_extend_remote_host_chrome_params(parsed_args, params) |
| |
| params.append( |
| '--load-and-launch-app=' + |
| remote_executor.resolve_path(parsed_args.arc_data_dir)) |
| |
| # This prevents Chrome to add icon to Gnome panel, which current leaks memory. |
| # See http://crbug.com/341724 for details. |
| params.append('--disable-background-mode') |
| |
| if parsed_args.chrome_args: |
| params.extend(parsed_args.chrome_args) |
| |
| return params |
| |
| |
| def _should_timeouts_be_used(parsed_args): |
| if parsed_args.jdb_port or parsed_args.gdb: |
| # Do not apply a timeout if debugging |
| return False |
| |
| if parsed_args.mode not in ('atftest', 'perftest'): |
| return False |
| |
| return True |
| |
| |
| def _select_chrome_timeout(parsed_args): |
| if not _should_timeouts_be_used(parsed_args): |
| return None |
| return parsed_args.timeout |
| |
| |
| def _select_output_handler(parsed_args, stats, chrome_process, **kwargs): |
| if parsed_args.mode == 'atftest': |
| handler = output_handler.AtfTestHandler() |
| elif parsed_args.mode == 'perftest': |
| handler = output_handler.PerfTestHandler( |
| parsed_args, stats, chrome_process, **kwargs) |
| else: |
| handler = concurrent_subprocess.RedirectOutputHandler( |
| *['.*' + re.escape(suppress) for suppress in _SUPPRESS_LIST]) |
| |
| if 'gpu' in parsed_args.gdb or 'renderer' in parsed_args.gdb: |
| handler = gdb_util.GdbHandlerAdapter( |
| handler, parsed_args.gdb, parsed_args.gdb_type) |
| |
| if 'plugin' in parsed_args.gdb: |
| if OPTIONS.is_nacl_build(): |
| handler = gdb_util.NaClGdbHandlerAdapter( |
| handler, _get_nacl_irt_path(parsed_args), parsed_args.gdb_type) |
| elif OPTIONS.is_bare_metal_build(): |
| handler = gdb_util.BareMetalGdbHandlerAdapter( |
| handler, _get_nacl_helper_nonsfi_path(parsed_args), |
| parsed_args.gdb_type) |
| |
| if (parsed_args.enable_arc_strace and |
| parsed_args.arc_strace_output != 'stderr'): |
| handler = output_handler.ArcStraceFilter( |
| handler, parsed_args.arc_strace_output) |
| |
| handler = output_handler.CrashAddressFilter(handler) |
| |
| if not platform_util.is_running_on_remote_host(): |
| # Filters that need to run on local host. |
| # See remote_executor_util.run_with_filter |
| handler = minidump_filter.MinidumpFilter(handler) |
| |
| if parsed_args.jdb_port: |
| handler = jdb_util.JdbHandlerAdapter( |
| handler, parsed_args.jdb_port, parsed_args.jdb_type) |
| |
| if parsed_args.chrome_flakiness_retry: |
| handler = output_handler.ChromeFlakinessHandler(handler, chrome_process) |
| |
| return output_handler.ChromeStatusCodeHandler(handler) |
| |
| |
| def _terminate_chrome(chrome): |
| _remove_chrome_pid_file(chrome.pid) |
| |
| if OPTIONS.is_nacl_build(): |
| # For now use sigkill, as NaCl's debug stub seems to cause sigterm to |
| # be ignored. |
| chrome.kill() |
| else: |
| chrome.terminate() |
| |
| chrome.wait(_CHROME_KILL_TIMEOUT) |
| |
| |
| def _run_chrome(parsed_args, **kwargs): |
| if parsed_args.logcat is not None: |
| # adb process will be terminated in the atexit handler, registered |
| # in the signal_util.setup(). |
| subprocess.Popen( |
| [toolchain.get_tool('host', 'adb'), 'logcat'] + parsed_args.logcat) |
| |
| params = _compute_chrome_params(parsed_args) |
| gdb_util.create_or_remove_bare_metal_gdb_lock_dir(parsed_args.gdb) |
| |
| # Similar to adb subprocess, using atexit has timing issue. See above comment |
| # for the details. |
| chrome_timeout = _select_chrome_timeout(parsed_args) |
| for i in xrange(parsed_args.chrome_flakiness_retry + 1): |
| if i > 0: |
| logging.error('Chrome is flaky. Retrying...: %d', i) |
| |
| p = chrome_process.ChromeProcess(params, timeout=chrome_timeout) |
| atexit.register(_terminate_chrome, p) |
| |
| gdb_util.maybe_launch_gdb(parsed_args.gdb, parsed_args.gdb_type, p.pid) |
| jdb_util.maybe_launch_jdb(parsed_args.jdb_port, parsed_args.jdb_type) |
| |
| # Write the PID to a file, so that other launch_chrome process sharing the |
| # same user data can find the process. In common case, the file will be |
| # removed by _terminate_chrome() defined above. |
| file_util.makedirs_safely(_USER_DATA_DIR) |
| with open(_CHROME_PID_PATH, 'w') as pid_file: |
| pid_file.write('%d\n' % p.pid) |
| |
| stats = startup_stats.StartupStats() |
| handler = _select_output_handler(parsed_args, stats, p, **kwargs) |
| |
| # Wait for the process to finish or us to be interrupted. |
| try: |
| returncode = p.handle_output(handler) |
| except output_handler.ChromeFlakinessError: |
| # Chrome is terminated due to its flakiness. Retry. |
| continue |
| |
| if returncode: |
| sys.exit(returncode) |
| return stats |
| |
| # Here, the Chrome flakiness failure has continued too many times. |
| # Terminate the script. |
| logging.error('Chrome is too flaky so that it hits retry limit.') |
| sys.exit(1) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |