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
3 # found in the LICENSE file.
11 import subprocess as subprocess
16 from telemetry.core import exceptions
17 from telemetry.core import util
18 from telemetry.core.backends import browser_backend
19 from telemetry.core.backends.chrome import chrome_browser_backend
20 from telemetry.util import path
21 from telemetry.util import support_binaries
24 class DesktopBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
25 """The backend for controlling a locally-executed browser instance, on Linux,
28 def __init__(self, browser_options, executable, flash_path, is_content_shell,
29 browser_directory, output_profile_path, extensions_to_load):
30 super(DesktopBrowserBackend, self).__init__(
31 supports_tab_control=not is_content_shell,
32 supports_extensions=not is_content_shell,
33 browser_options=browser_options,
34 output_profile_path=output_profile_path,
35 extensions_to_load=extensions_to_load)
37 # Initialize fields so that an explosion during init doesn't break in Close.
39 self._tmp_profile_dir = None
40 self._tmp_output_file = None
42 self._executable = executable
43 if not self._executable:
44 raise Exception('Cannot create browser, no executable found!')
46 assert not flash_path or os.path.exists(flash_path)
47 self._flash_path = flash_path
49 self._is_content_shell = is_content_shell
51 if len(extensions_to_load) > 0 and is_content_shell:
52 raise browser_backend.ExtensionsNotSupportedException(
53 'Content shell does not support extensions.')
55 self._browser_directory = browser_directory
57 self._tmp_minidump_dir = tempfile.mkdtemp()
58 self._crash_service = None
62 def _SetupProfile(self):
63 if not self.browser_options.dont_override_profile:
64 if self._output_profile_path:
65 # If both |_output_profile_path| and |profile_dir| are specified then
66 # the calling code will throw an exception, so we don't need to worry
67 # about that case here.
68 self._tmp_profile_dir = self._output_profile_path
70 self._tmp_profile_dir = tempfile.mkdtemp()
71 profile_dir = self.browser_options.profile_dir
73 if self._is_content_shell:
74 logging.critical('Profiles cannot be used with content shell')
76 logging.info("Using profile directory:'%s'." % profile_dir)
77 shutil.rmtree(self._tmp_profile_dir)
78 shutil.copytree(profile_dir, self._tmp_profile_dir)
79 if self.browser_options.use_devtools_active_port:
80 # No matter whether we're using an existing profile directory or
81 # creating a new one, always delete the well-known file containing
82 # the active DevTools port number.
83 port_file = self._GetDevToolsActivePortPath()
84 if os.path.isfile(port_file):
87 except Exception as e:
88 logging.critical('Unable to remove DevToolsActivePort file: %s' % e)
91 def _GetDevToolsActivePortPath(self):
92 return os.path.join(self.profile_directory, 'DevToolsActivePort')
94 def _GetCrashServicePipeName(self):
95 # Ensure a unique pipe name by using the name of the temp dir.
96 return r'\\.\pipe\%s_service' % os.path.basename(self._tmp_minidump_dir)
98 def _StartCrashService(self):
99 os_name = self._browser.platform.GetOSName()
102 return subprocess.Popen([
103 support_binaries.FindPath('crash_service', os_name),
105 '--dumps-dir=%s' % self._tmp_minidump_dir,
106 '--pipe-name=%s' % self._GetCrashServicePipeName()])
108 def _GetCdbPath(self):
110 'Debugging Tools For Windows',
111 'Debugging Tools For Windows (x86)',
112 'Debugging Tools For Windows (x64)',
113 os.path.join('Windows Kits', '8.0', 'Debuggers', 'x86'),
114 os.path.join('Windows Kits', '8.0', 'Debuggers', 'x64'),
115 os.path.join('win_toolchain', 'vs2013_files', 'win8sdk', 'Debuggers',
117 os.path.join('win_toolchain', 'vs2013_files', 'win8sdk', 'Debuggers',
120 for possible_path in possible_paths:
121 app_path = os.path.join(possible_path, 'cdb.exe')
122 app_path = path.FindInstalledWindowsApplication(app_path)
127 def HasBrowserFinishedLaunching(self):
128 # In addition to the functional check performed by the base class, quickly
129 # check if the browser process is still alive.
130 if not self.IsBrowserRunning():
131 raise exceptions.ProcessGoneException(
132 "Return code: %d" % self._proc.returncode)
133 if self.browser_options.use_devtools_active_port:
134 # The Telemetry user selected the new code path to start DevTools on
135 # an ephemeral port. Wait for the well-known file containing the port
137 port_file = self._GetDevToolsActivePortPath()
138 if not os.path.isfile(port_file):
139 # File isn't ready yet. Return false. Will retry.
141 # Attempt to avoid reading the file until it's populated.
144 if os.stat(port_file).st_size > 0:
145 with open(port_file) as f:
146 port_string = f.read()
147 self._port = int(port_string)
148 logging.info('Discovered ephemeral port %s' % self._port)
151 # Both stat and open can throw exceptions.
154 # File isn't ready yet. Return false. Will retry.
156 return super(DesktopBrowserBackend, self).HasBrowserFinishedLaunching()
158 def GetBrowserStartupArgs(self):
159 args = super(DesktopBrowserBackend, self).GetBrowserStartupArgs()
160 if self.browser_options.use_devtools_active_port:
163 self._port = util.GetUnreservedAvailableLocalPort()
164 logging.info('Requested remote debugging port: %d' % self._port)
165 args.append('--remote-debugging-port=%i' % self._port)
166 args.append('--enable-crash-reporter-for-testing')
167 args.append('--use-mock-keychain')
168 if not self._is_content_shell:
169 args.append('--window-size=1280,1024')
171 args.append('--ppapi-flash-path=%s' % self._flash_path)
172 if not self.browser_options.dont_override_profile:
173 args.append('--user-data-dir=%s' % self._tmp_profile_dir)
177 assert not self._proc, 'Must call Close() before Start()'
179 args = [self._executable]
180 args.extend(self.GetBrowserStartupArgs())
181 if self.browser_options.startup_url:
182 args.append(self.browser_options.startup_url)
183 env = os.environ.copy()
184 env['CHROME_HEADLESS'] = '1' # Don't upload minidumps.
185 env['BREAKPAD_DUMP_LOCATION'] = self._tmp_minidump_dir
186 env['CHROME_BREAKPAD_PIPE_NAME'] = self._GetCrashServicePipeName()
187 self._crash_service = self._StartCrashService()
188 logging.debug('Starting Chrome %s', args)
189 if not self.browser_options.show_stdout:
190 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
191 self._proc = subprocess.Popen(
192 args, stdout=self._tmp_output_file, stderr=subprocess.STDOUT, env=env)
194 self._proc = subprocess.Popen(args, env=env)
197 self._WaitForBrowserToComeUp()
205 return self._proc.pid
209 def browser_directory(self):
210 return self._browser_directory
213 def profile_directory(self):
214 return self._tmp_profile_dir
216 def IsBrowserRunning(self):
217 return self._proc and self._proc.poll() == None
219 def GetStandardOutput(self):
220 if not self._tmp_output_file:
221 if self.browser_options.show_stdout:
222 # This can happen in the case that loading the Chrome binary fails.
223 # We print rather than using logging here, because that makes a
224 # recursive call to this function.
225 print >> sys.stderr, "Can't get standard output with --show-stdout"
227 self._tmp_output_file.flush()
229 with open(self._tmp_output_file.name) as f:
234 def _GetMostRecentMinidump(self):
235 dumps = glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp'))
238 most_recent_dump = heapq.nlargest(1, dumps, os.path.getmtime)[0]
239 if os.path.getmtime(most_recent_dump) < (time.time() - (5 * 60)):
240 logging.warning('Crash dump is older than 5 minutes. May not be correct.')
241 return most_recent_dump
243 def _GetStackFromMinidump(self, minidump):
244 os_name = self._browser.platform.GetOSName()
246 cdb = self._GetCdbPath()
248 logging.warning('cdb.exe not found.')
250 output = subprocess.check_output([cdb, '-y', self._browser_directory,
251 '-c', '.ecxr;k30;q', '-z', minidump])
252 stack_start = output.find('ChildEBP')
253 stack_end = output.find('quit:')
254 return output[stack_start:stack_end]
256 stackwalk = support_binaries.FindPath('minidump_stackwalk', os_name)
258 logging.warning('minidump_stackwalk binary not found.')
261 with open(minidump, 'rb') as infile:
262 minidump += '.stripped'
263 with open(minidump, 'wb') as outfile:
264 outfile.write(''.join(infile.read().partition('MDMP')[1:]))
266 symbols_path = os.path.join(self._tmp_minidump_dir, 'symbols')
268 symbols = glob.glob(os.path.join(self._browser_directory, '*.breakpad*'))
270 for symbol in sorted(symbols, key=os.path.getmtime, reverse=True):
271 if not os.path.isfile(symbol):
273 with open(symbol, 'r') as f:
274 fields = f.readline().split()
278 binary = ' '.join(fields[4:])
279 symbol_path = os.path.join(symbols_path, binary, sha)
280 if os.path.exists(symbol_path):
282 os.makedirs(symbol_path)
283 shutil.copyfile(symbol, os.path.join(symbol_path, binary + '.sym'))
285 logging.info('Dumping breakpad symbols')
286 generate_breakpad_symbols_path = os.path.join(
287 util.GetChromiumSrcDir(), "components", "breakpad",
288 "tools", "generate_breakpad_symbols.py")
291 generate_breakpad_symbols_path,
292 '--binary=%s' % self._executable,
293 '--symbols-dir=%s' % symbols_path,
294 '--build-dir=%s' % self._browser_directory,
298 subprocess.check_output(cmd, stderr=open(os.devnull, 'w'))
299 except subprocess.CalledProcessError:
300 logging.warning('Failed to execute "%s"' % ' '.join(cmd))
303 return subprocess.check_output([stackwalk, minidump, symbols_path],
304 stderr=open(os.devnull, 'w'))
306 def GetStackTrace(self):
307 most_recent_dump = self._GetMostRecentMinidump()
308 if not most_recent_dump:
309 logging.warning('No crash dump found. Returning browser stdout.')
310 return self.GetStandardOutput()
312 stack = self._GetStackFromMinidump(most_recent_dump)
314 logging.warning('Failed to symbolize minidump. Returning browser stdout.')
315 return self.GetStandardOutput()
323 super(DesktopBrowserBackend, self).Close()
325 # Shutdown politely if the profile may be used again.
326 if self._output_profile_path and self.IsBrowserRunning():
327 self._proc.terminate()
329 util.WaitFor(lambda: not self.IsBrowserRunning(), timeout=5)
331 except util.TimeoutException:
332 logging.warning('Failed to gracefully shutdown. Proceeding to kill.')
334 # Shutdown aggressively if the above failed or if the profile is temporary.
335 if self.IsBrowserRunning():
339 if self._crash_service:
340 self._crash_service.kill()
341 self._crash_service = None
343 if self._output_profile_path:
344 # If we need the output then double check that it exists.
345 if not (self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir)):
346 raise Exception("No profile directory generated by Chrome: '%s'." %
347 self._tmp_profile_dir)
349 # If we don't need the profile after the run then cleanup.
350 if self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir):
351 shutil.rmtree(self._tmp_profile_dir, ignore_errors=True)
352 self._tmp_profile_dir = None
354 if self._tmp_output_file:
355 self._tmp_output_file.close()
356 self._tmp_output_file = None