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.
9 import subprocess as subprocess
15 from telemetry.core import exceptions
16 from telemetry.core import util
17 from telemetry.core.backends import browser_backend
18 from telemetry.core.backends.chrome import chrome_browser_backend
21 class DesktopBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
22 """The backend for controlling a locally-executed browser instance, on Linux,
25 def __init__(self, browser_options, executable, flash_path, is_content_shell,
26 browser_directory, output_profile_path, extensions_to_load):
27 super(DesktopBrowserBackend, self).__init__(
28 is_content_shell=is_content_shell,
29 supports_extensions=not is_content_shell,
30 browser_options=browser_options,
31 output_profile_path=output_profile_path,
32 extensions_to_load=extensions_to_load)
34 # Initialize fields so that an explosion during init doesn't break in Close.
36 self._tmp_profile_dir = None
37 self._tmp_output_file = None
39 self._executable = executable
40 if not self._executable:
41 raise Exception('Cannot create browser, no executable found!')
43 self._flash_path = flash_path
44 if (browser_options.warn_if_no_flash
45 and self._flash_path and not os.path.exists(self._flash_path)):
46 logging.warning(('Could not find flash at %s. Running without flash.\n\n'
47 'To fix this see http://go/read-src-internal') %
49 self._flash_path = None
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._profile_dir = None
58 self._tmp_minidump_dir = tempfile.mkdtemp()
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._profile_dir or 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)
80 def _LaunchBrowser(self):
81 args = [self._executable]
82 args.extend(self.GetBrowserStartupArgs())
83 if self.browser_options.startup_url:
84 args.append(self.browser_options.startup_url)
85 env = os.environ.copy()
86 env['CHROME_HEADLESS'] = '1' # Don't upload minidumps.
87 env['BREAKPAD_DUMP_LOCATION'] = self._tmp_minidump_dir
88 logging.debug('Starting Chrome %s', args)
89 if not self.browser_options.show_stdout:
90 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
91 self._proc = subprocess.Popen(
92 args, stdout=self._tmp_output_file, stderr=subprocess.STDOUT, env=env)
94 self._proc = subprocess.Popen(args, env=env)
97 self._WaitForBrowserToComeUp()
98 self._PostBrowserStartupInitialization()
103 def HasBrowserFinishedLaunching(self):
104 # In addition to the functional check performed by the base class, quickly
105 # check if the browser process is still alive.
107 if self._proc.returncode:
108 raise exceptions.ProcessGoneException(
109 "Return code: %d" % self._proc.returncode)
110 return super(DesktopBrowserBackend, self).HasBrowserFinishedLaunching()
112 def GetBrowserStartupArgs(self):
113 args = super(DesktopBrowserBackend, self).GetBrowserStartupArgs()
114 self._port = util.GetUnreservedAvailableLocalPort()
115 args.append('--remote-debugging-port=%i' % self._port)
116 args.append('--enable-crash-reporter-for-testing')
117 if not self.is_content_shell:
118 args.append('--window-size=1280,1024')
120 args.append('--ppapi-flash-path=%s' % self._flash_path)
121 if not self.browser_options.dont_override_profile:
122 args.append('--user-data-dir=%s' % self._tmp_profile_dir)
125 def SetProfileDirectory(self, profile_dir):
126 # Make sure _profile_dir hasn't already been set.
127 assert self._profile_dir is None
129 if self.is_content_shell:
130 logging.critical('Profile creation cannot be used with content shell')
133 self._profile_dir = profile_dir
136 self._LaunchBrowser()
141 return self._proc.pid
145 def browser_directory(self):
146 return self._browser_directory
149 def profile_directory(self):
150 return self._tmp_profile_dir
152 def IsBrowserRunning(self):
153 return self._proc.poll() == None
155 def GetStandardOutput(self):
156 if not self._tmp_output_file:
157 if self.browser_options.show_stdout:
158 # This can happen in the case that loading the Chrome binary fails.
159 # We print rather than using logging here, because that makes a
160 # recursive call to this function.
161 print >> sys.stderr, "Can't get standard output with --show_stdout"
163 self._tmp_output_file.flush()
165 with open(self._tmp_output_file.name) as f:
170 def GetStackTrace(self):
171 stackwalk = util.FindSupportBinary('minidump_stackwalk')
173 logging.warning('minidump_stackwalk binary not found. Must build it to '
174 'symbolize crash dumps. Returning browser stdout.')
175 return self.GetStandardOutput()
177 dumps = glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp'))
179 logging.warning('No crash dump found. Returning browser stdout.')
180 return self.GetStandardOutput()
181 most_recent_dump = heapq.nlargest(1, dumps, os.path.getmtime)[0]
182 if os.path.getmtime(most_recent_dump) < (time.time() - (5 * 60)):
183 logging.warning('Crash dump is older than 5 minutes. May not be correct.')
185 symbols = glob.glob(os.path.join(self._browser_directory, '*.breakpad*'))
187 logging.warning('No breakpad symbols found. Returning browser stdout.')
188 return self.GetStandardOutput()
190 minidump = most_recent_dump + '.stripped'
191 with open(most_recent_dump, 'rb') as infile:
192 with open(minidump, 'wb') as outfile:
193 outfile.write(''.join(infile.read().partition('MDMP')[1:]))
195 symbols_path = os.path.join(self._tmp_minidump_dir, 'symbols')
196 for symbol in sorted(symbols, key=os.path.getmtime, reverse=True):
197 if not os.path.isfile(symbol):
199 with open(symbol, 'r') as f:
200 fields = f.readline().split()
204 binary = ' '.join(fields[4:])
205 symbol_path = os.path.join(symbols_path, binary, sha)
206 if os.path.exists(symbol_path):
208 os.makedirs(symbol_path)
209 shutil.copyfile(symbol, os.path.join(symbol_path, binary + '.sym'))
211 error = tempfile.NamedTemporaryFile('w', 0)
212 return subprocess.Popen(
213 [stackwalk, minidump, symbols_path],
214 stdout=subprocess.PIPE, stderr=error).communicate()[0]
220 super(DesktopBrowserBackend, self).Close()
227 return self._proc.poll() != None
229 # Try to politely shutdown, first.
231 self._proc.terminate()
233 util.WaitFor(IsClosed, timeout=5)
235 except util.TimeoutException:
236 logging.warning('Failed to gracefully shutdown. Proceeding to kill.')
242 util.WaitFor(IsClosed, timeout=10)
243 except util.TimeoutException:
244 raise Exception('Could not shutdown the browser.')
248 if self._output_profile_path:
249 # If we need the output then double check that it exists.
250 if not (self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir)):
251 raise Exception("No profile directory generated by Chrome: '%s'." %
252 self._tmp_profile_dir)
254 # If we don't need the profile after the run then cleanup.
255 if self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir):
256 shutil.rmtree(self._tmp_profile_dir, ignore_errors=True)
257 self._tmp_profile_dir = None
259 if self._tmp_output_file:
260 self._tmp_output_file.close()
261 self._tmp_output_file = None