Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / backends / chrome / desktop_browser_backend.py
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.
4
5 import glob
6 import heapq
7 import logging
8 import os
9 import os.path
10 import shutil
11 import subprocess as subprocess
12 import sys
13 import tempfile
14 import time
15
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
22
23
24 class DesktopBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
25   """The backend for controlling a locally-executed browser instance, on Linux,
26   Mac or Windows.
27   """
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)
36
37     # Initialize fields so that an explosion during init doesn't break in Close.
38     self._proc = None
39     self._tmp_profile_dir = None
40     self._tmp_output_file = None
41
42     self._executable = executable
43     if not self._executable:
44       raise Exception('Cannot create browser, no executable found!')
45
46     assert not flash_path or os.path.exists(flash_path)
47     self._flash_path = flash_path
48
49     self._is_content_shell = is_content_shell
50
51     if len(extensions_to_load) > 0 and is_content_shell:
52       raise browser_backend.ExtensionsNotSupportedException(
53           'Content shell does not support extensions.')
54
55     self._browser_directory = browser_directory
56     self._port = None
57     self._tmp_minidump_dir = tempfile.mkdtemp()
58     self._crash_service = None
59
60     self._SetupProfile()
61
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
69       else:
70         self._tmp_profile_dir = tempfile.mkdtemp()
71       profile_dir = self.browser_options.profile_dir
72       if profile_dir:
73         if self._is_content_shell:
74           logging.critical('Profiles cannot be used with content shell')
75           sys.exit(1)
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):
85         try:
86           os.remove(port_file)
87         except Exception as e:
88           logging.critical('Unable to remove DevToolsActivePort file: %s' % e)
89           sys.exit(1)
90
91   def _GetDevToolsActivePortPath(self):
92     return os.path.join(self.profile_directory, 'DevToolsActivePort')
93
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)
97
98   def _StartCrashService(self):
99     os_name = self._browser.platform.GetOSName()
100     if os_name != 'win':
101       return None
102     return subprocess.Popen([
103         support_binaries.FindPath('crash_service', os_name),
104         '--no-window',
105         '--dumps-dir=%s' % self._tmp_minidump_dir,
106         '--pipe-name=%s' % self._GetCrashServicePipeName()])
107
108   def _GetCdbPath(self):
109     possible_paths = (
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',
116                      'x86'),
117         os.path.join('win_toolchain', 'vs2013_files', 'win8sdk', 'Debuggers',
118                      'x64'),
119     )
120     for possible_path in possible_paths:
121       app_path = os.path.join(possible_path, 'cdb.exe')
122       app_path = path.FindInstalledWindowsApplication(app_path)
123       if app_path:
124         return app_path
125     return None
126
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
136       # number to exist.
137       port_file = self._GetDevToolsActivePortPath()
138       if not os.path.isfile(port_file):
139         # File isn't ready yet. Return false. Will retry.
140         return False
141       # Attempt to avoid reading the file until it's populated.
142       got_port = False
143       try:
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)
149             got_port = True
150       except Exception:
151         # Both stat and open can throw exceptions.
152         pass
153       if not got_port:
154         # File isn't ready yet. Return false. Will retry.
155         return False
156     return super(DesktopBrowserBackend, self).HasBrowserFinishedLaunching()
157
158   def GetBrowserStartupArgs(self):
159     args = super(DesktopBrowserBackend, self).GetBrowserStartupArgs()
160     if self.browser_options.use_devtools_active_port:
161       self._port = 0
162     else:
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')
170       if self._flash_path:
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)
174     return args
175
176   def Start(self):
177     assert not self._proc, 'Must call Close() before Start()'
178
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)
193     else:
194       self._proc = subprocess.Popen(args, env=env)
195
196     try:
197       self._WaitForBrowserToComeUp()
198     except:
199       self.Close()
200       raise
201
202   @property
203   def pid(self):
204     if self._proc:
205       return self._proc.pid
206     return None
207
208   @property
209   def browser_directory(self):
210     return self._browser_directory
211
212   @property
213   def profile_directory(self):
214     return self._tmp_profile_dir
215
216   def IsBrowserRunning(self):
217     return self._proc and self._proc.poll() == None
218
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"
226       return ''
227     self._tmp_output_file.flush()
228     try:
229       with open(self._tmp_output_file.name) as f:
230         return f.read()
231     except IOError:
232       return ''
233
234   def _GetMostRecentMinidump(self):
235     dumps = glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp'))
236     if not dumps:
237       return None
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
242
243   def _GetStackFromMinidump(self, minidump):
244     os_name = self._browser.platform.GetOSName()
245     if os_name == 'win':
246       cdb = self._GetCdbPath()
247       if not cdb:
248         logging.warning('cdb.exe not found.')
249         return None
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]
255
256     stackwalk = support_binaries.FindPath('minidump_stackwalk', os_name)
257     if not stackwalk:
258       logging.warning('minidump_stackwalk binary not found.')
259       return None
260
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:]))
265
266     symbols_path = os.path.join(self._tmp_minidump_dir, 'symbols')
267
268     symbols = glob.glob(os.path.join(self._browser_directory, '*.breakpad*'))
269     if symbols:
270       for symbol in sorted(symbols, key=os.path.getmtime, reverse=True):
271         if not os.path.isfile(symbol):
272           continue
273         with open(symbol, 'r') as f:
274           fields = f.readline().split()
275           if not fields:
276             continue
277           sha = fields[3]
278           binary = ' '.join(fields[4:])
279         symbol_path = os.path.join(symbols_path, binary, sha)
280         if os.path.exists(symbol_path):
281           continue
282         os.makedirs(symbol_path)
283         shutil.copyfile(symbol, os.path.join(symbol_path, binary + '.sym'))
284     else:
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")
289       cmd = [
290           sys.executable,
291           generate_breakpad_symbols_path,
292           '--binary=%s' % self._executable,
293           '--symbols-dir=%s' % symbols_path,
294           '--build-dir=%s' % self._browser_directory,
295           ]
296
297       try:
298         subprocess.check_output(cmd, stderr=open(os.devnull, 'w'))
299       except subprocess.CalledProcessError:
300         logging.warning('Failed to execute "%s"' % ' '.join(cmd))
301         return None
302
303     return subprocess.check_output([stackwalk, minidump, symbols_path],
304                                    stderr=open(os.devnull, 'w'))
305
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()
311
312     stack = self._GetStackFromMinidump(most_recent_dump)
313     if not stack:
314       logging.warning('Failed to symbolize minidump. Returning browser stdout.')
315       return self.GetStandardOutput()
316
317     return stack
318
319   def __del__(self):
320     self.Close()
321
322   def Close(self):
323     super(DesktopBrowserBackend, self).Close()
324
325     # Shutdown politely if the profile may be used again.
326     if self._output_profile_path and self.IsBrowserRunning():
327       self._proc.terminate()
328       try:
329         util.WaitFor(lambda: not self.IsBrowserRunning(), timeout=5)
330         self._proc = None
331       except util.TimeoutException:
332         logging.warning('Failed to gracefully shutdown. Proceeding to kill.')
333
334     # Shutdown aggressively if the above failed or if the profile is temporary.
335     if self.IsBrowserRunning():
336       self._proc.kill()
337     self._proc = None
338
339     if self._crash_service:
340       self._crash_service.kill()
341       self._crash_service = None
342
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)
348     else:
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
353
354     if self._tmp_output_file:
355       self._tmp_output_file.close()
356       self._tmp_output_file = None