webkitpy: Missing PID in crashlog name should not be fatal
[WebKit-https.git] / Tools / Scripts / webkitpy / port / darwin.py
1 # Copyright (C) 2014-2016 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1.  Redistributions of source code must retain the above copyright
7 #     notice, this list of conditions and the following disclaimer.
8 # 2.  Redistributions in binary form must reproduce the above copyright
9 #     notice, this list of conditions and the following disclaimer in the
10 #     documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
13 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
16 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
20 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23 import logging
24 import os
25 import time
26
27 from webkitpy.common.memoized import memoized
28 from webkitpy.common.system.crashlogs import CrashLogs
29 from webkitpy.common.system.executive import ScriptError
30 from webkitpy.port.apple import ApplePort
31 from webkitpy.port.leakdetector import LeakDetector
32
33
34 _log = logging.getLogger(__name__)
35
36
37 class DarwinPort(ApplePort):
38
39     CURRENT_VERSION = None
40     SDK = None
41
42     def __init__(self, host, port_name, **kwargs):
43         ApplePort.__init__(self, host, port_name, **kwargs)
44
45         self._leak_detector = LeakDetector(self)
46         if self.get_option("leaks"):
47             # DumpRenderTree slows down noticably if we run more than about 1000 tests in a batch
48             # with MallocStackLogging enabled.
49             self.set_option_default("batch_size", 1000)
50
51     def default_timeout_ms(self):
52         if self.get_option('guard_malloc'):
53             return 350 * 1000
54         return super(DarwinPort, self).default_timeout_ms()
55
56     def _port_specific_expectations_files(self, device_type=None):
57         return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self.baseline_search_path(device_type=device_type)]))
58
59     def check_for_leaks(self, process_name, process_pid):
60         if not self.get_option('leaks'):
61             return
62         # We could use http://code.google.com/p/psutil/ to get the process_name from the pid.
63         self._leak_detector.check_for_leaks(process_name, process_pid)
64
65     def print_leaks_summary(self):
66         if not self.get_option('leaks'):
67             return
68         # We're in the manager process, so the leak detector will not have a valid list of leak files.
69         # FIXME: This is a hack, but we don't have a better way to get this information from the workers yet.
70         # FIXME: This will include too many leaks in subsequent runs until the results directory is cleared!
71         leaks_files = self._leak_detector.leaks_files_in_directory(self.results_directory())
72         if not leaks_files:
73             return
74         total_bytes_string, unique_leaks = self._leak_detector.count_total_bytes_and_unique_leaks(leaks_files)
75         total_leaks = self._leak_detector.count_total_leaks(leaks_files)
76         _log.info("%s total leaks found for a total of %s." % (total_leaks, total_bytes_string))
77         _log.info("%s unique leaks found." % unique_leaks)
78
79     def _path_to_webcore_library(self):
80         return self._build_path('WebCore.framework/Versions/A/WebCore')
81
82     def show_results_html_file(self, results_filename):
83         # We don't use self._run_script() because we don't want to wait for the script
84         # to exit and we want the output to show up on stdout in case there are errors
85         # launching the browser.
86         self._executive.popen([self.path_to_script('run-safari')] + self._arguments_for_configuration() + ['--no-saved-state', '-NSOpen', results_filename],
87             cwd=self.webkit_base(), stdout=file(os.devnull), stderr=file(os.devnull))
88
89     @memoized
90     def path_to_crash_logs(self):
91         log_directory = self.host.filesystem.expanduser('~')
92         log_directory = self.host.filesystem.join(log_directory, 'Library', 'Logs')
93         diagnositc_reports_directory = self.host.filesystem.join(log_directory, 'DiagnosticReports')
94         if self.host.filesystem.exists(diagnositc_reports_directory):
95             return diagnositc_reports_directory
96         return self.host.filesystem.join(log_directory, 'CrashReporter')
97
98     def _merge_crash_logs(self, logs, new_logs, crashed_processes):
99         for test, crash_log in new_logs.iteritems():
100             try:
101                 if test.split('-')[0] == 'Sandbox':
102                     process_name = test.split('-')[1]
103                     pid = int(test.split('-')[2])
104                 else:
105                     process_name = test.split('-')[0]
106                     pid = int(test.split('-')[1])
107             except IndexError, ValueError:
108                 continue
109             if not any(entry[1] == process_name and entry[2] == pid for entry in crashed_processes):
110                 # if this is a new crash, then append the logs
111                 logs[test] = crash_log
112         return logs
113
114     def _look_for_all_crash_logs_in_log_dir(self, newer_than):
115         crash_log = CrashLogs(self.host, self.path_to_crash_logs(), crash_logs_to_skip=self._crash_logs_to_skip_for_host.get(self.host, []))
116         return crash_log.find_all_logs(include_errors=True, newer_than=newer_than)
117
118     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True, target_host=None):
119         # Note that we do slow-spin here and wait, since it appears the time
120         # ReportCrash takes to actually write and flush the file varies when there are
121         # lots of simultaneous crashes going on.
122         time_fn = time_fn or time.time
123         sleep_fn = sleep_fn or time.sleep
124         crash_log = ''
125         crash_logs = CrashLogs(target_host or self.host, self.path_to_crash_logs(), crash_logs_to_skip=self._crash_logs_to_skip_for_host.get(target_host or self.host, []))
126         now = time_fn()
127         deadline = now + 5 * int(self.get_option('child_processes', 1))
128         while not crash_log and now <= deadline:
129             crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
130             if not wait_for_log:
131                 break
132             if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
133                 sleep_fn(0.1)
134                 now = time_fn()
135
136         if not crash_log:
137             return (stderr, None)
138         return (stderr, crash_log)
139
140     def look_for_new_crash_logs(self, crashed_processes, start_time):
141         """Since crash logs can take a long time to be written out if the system is
142            under stress do a second pass at the end of the test run.
143
144            crashes: test_name -> pid, process_name tuple of crashed process
145            start_time: time the tests started at.  We're looking for crash
146                logs after that time.
147         """
148         crash_logs = {}
149         for (test_name, process_name, pid) in crashed_processes:
150             # Passing None for output.  This is a second pass after the test finished so
151             # if the output had any logging we would have already collected it.
152             crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
153             if not crash_log:
154                 continue
155             crash_logs[test_name] = crash_log
156         all_crash_log = self._look_for_all_crash_logs_in_log_dir(start_time)
157         return self._merge_crash_logs(crash_logs, all_crash_log, crashed_processes)
158
159     def sample_process(self, name, pid, target_host=None):
160         host = target_host or self.host
161         tempdir = host.filesystem.mkdtemp()
162         command = [
163             '/usr/sbin/spindump',
164             pid,
165             10,
166             10,
167             '-file',
168             DarwinPort.spindump_file_path(host, name, pid, str(tempdir)),
169         ]
170         if self.host.platform.is_mac():
171             command = ['/usr/bin/sudo', '-n'] + command
172         exit_status = host.executive.run_command(command, return_exit_code=True)
173         if exit_status:
174             try:
175                 host.executive.run_command([
176                     '/usr/bin/sample',
177                     pid,
178                     10,
179                     10,
180                     '-file',
181                     DarwinPort.sample_file_path(host, name, pid, str(tempdir)),
182                 ])
183                 host.filesystem.move_to_base_host(DarwinPort.sample_file_path(host, name, pid, str(tempdir)),
184                                                   DarwinPort.sample_file_path(self.host, name, pid, self.results_directory()))
185             except ScriptError as e:
186                 _log.warning('Unable to sample process:' + str(e))
187         else:
188             host.filesystem.move_to_base_host(DarwinPort.spindump_file_path(host, name, pid, str(tempdir)),
189                                               DarwinPort.spindump_file_path(self.host, name, pid, self.results_directory()))
190         host.filesystem.rmtree(str(tempdir))
191
192     @staticmethod
193     def sample_file_path(host, name, pid, directory):
194         return host.filesystem.join(directory, "{0}-{1}-sample.txt".format(name, pid))
195
196     @staticmethod
197     def spindump_file_path(host, name, pid, directory):
198         return host.filesystem.join(directory, "{0}-{1}-spindump.txt".format(name, pid))
199
200     def look_for_new_samples(self, unresponsive_processes, start_time):
201         sample_files = {}
202         for (test_name, process_name, pid) in unresponsive_processes:
203             sample_file = DarwinPort.sample_file_path(self.host, process_name, pid, self.results_directory())
204             if self._filesystem.isfile(sample_file):
205                 sample_files[test_name] = sample_file
206             else:
207                 spindump_file = DarwinPort.spindump_file_path(self.host, process_name, pid, self.results_directory())
208                 if self._filesystem.isfile(spindump_file):
209                     sample_files[test_name] = spindump_file
210         return sample_files
211
212     def _path_to_image_diff(self):
213         # ImageDiff for DarwinPorts is a little complicated. It will either be in
214         # a directory named ../mac relative to the port build directory, in a directory
215         # named ../<build-type> relative to the port build directory or in the port build directory
216         _image_diff_in_build_path = super(DarwinPort, self)._path_to_image_diff()
217         _port_build_dir = self.host.filesystem.dirname(_image_diff_in_build_path)
218
219         # Test ../mac
220         _path_to_test = self.host.filesystem.join(_port_build_dir, '..', 'mac', 'ImageDiff')
221         if self.host.filesystem.exists(_path_to_test):
222             return _path_to_test
223
224         # Test ../<build-type>
225         _build_type = self.host.filesystem.basename(_port_build_dir).split('-')[0]
226         _path_to_test = self.host.filesystem.join(_port_build_dir, '..', _build_type, 'ImageDiff')
227         if self.host.filesystem.exists(_path_to_test):
228             return _path_to_test
229
230         return _image_diff_in_build_path
231
232     def make_command(self):
233         return self.xcrun_find('make', '/usr/bin/make')
234
235     def nm_command(self):
236         return self.xcrun_find('nm', 'nm')
237
238     def xcrun_find(self, command, fallback=None):
239         fallback = fallback or command
240         try:
241             return self._executive.run_command(['xcrun', '--sdk', self.SDK, '-find', command]).rstrip()
242         except ScriptError:
243             _log.warn("xcrun failed; falling back to '%s'." % fallback)
244             return fallback
245
246     @memoized
247     def _plist_data_from_bundle(self, app_bundle, entry):
248         plist_path = self._filesystem.join(app_bundle, 'Info.plist')
249         if not self._filesystem.exists(plist_path):
250             plist_path = self._filesystem.join(app_bundle, 'Contents', 'Info.plist')
251         if not self._filesystem.exists(plist_path):
252             return None
253         return self._executive.run_command(['/usr/libexec/PlistBuddy', '-c', 'Print {}'.format(entry), plist_path]).rstrip()
254
255     def app_identifier_from_bundle(self, app_bundle):
256         return self._plist_data_from_bundle(app_bundle, 'CFBundleIdentifier')
257
258     def app_executable_from_bundle(self, app_bundle):
259         return self._plist_data_from_bundle(app_bundle, 'CFBundleExecutable')