Spurious output on Windows tests: AQMEIOManager::FindIOUnit: error -1
[WebKit-https.git] / Tools / Scripts / webkitpy / port / win.py
1 # Copyright (C) 2010 Google Inc. All rights reserved.
2 # Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the Google name nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 import atexit
31 import os
32 import logging
33 import re
34 import sys
35 import time
36
37 from webkitpy.common.system.crashlogs import CrashLogs
38 from webkitpy.common.system.systemhost import SystemHost
39 from webkitpy.common.system.executive import ScriptError, Executive
40 from webkitpy.common.system.path import abspath_to_uri, cygpath
41 from webkitpy.port.apple import ApplePort
42
43
44 _log = logging.getLogger(__name__)
45
46
47 class WinPort(ApplePort):
48     port_name = "win"
49
50     VERSION_FALLBACK_ORDER = ["win-xp", "win-vista", "win-7sp0", "win-win10", "win"]
51
52     ARCHITECTURES = ['x86', 'x86_64']
53
54     CRASH_LOG_PREFIX = "CrashLog"
55
56     POST_MORTEM_DEBUGGER_KEY = "/%s/SOFTWARE/Microsoft/Windows NT/CurrentVersion/AeDebug/%s"
57
58     WINDOWS_ERROR_REPORTING_KEY = "/%s/SOFTWARE/Microsoft/Windows/Windows Error Reporting/%s"
59
60     previous_debugger_values = {}
61
62     previous_error_reporting_values = {}
63
64     def do_text_results_differ(self, expected_text, actual_text):
65         # Sanity was restored in WK2, so we don't need this hack there.
66         if self.get_option('webkit_test_runner'):
67             return ApplePort.do_text_results_differ(self, expected_text, actual_text)
68
69         # This is a hack (which dates back to ORWT).
70         # Windows does not have an EDITING DELEGATE, so we strip any EDITING DELEGATE
71         # messages to make more of the tests pass.
72         # It's possible more of the ports might want this and this could move down into WebKitPort.
73         delegate_regexp = re.compile("^EDITING DELEGATE: .*?\n", re.MULTILINE)
74         expected_text = delegate_regexp.sub("", expected_text)
75         actual_text = delegate_regexp.sub("", actual_text)
76         return expected_text != actual_text
77
78     def default_baseline_search_path(self):
79         name = self._name.replace('-wk2', '')
80         if name.endswith(self.FUTURE_VERSION):
81             fallback_names = [self.port_name]
82         else:
83             fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(name):-1] + [self.port_name]
84         # FIXME: The AppleWin port falls back to AppleMac for some results.  Eventually we'll have a shared 'apple' port.
85         if self.get_option('webkit_test_runner'):
86             fallback_names.insert(0, 'win-wk2')
87             fallback_names.append('mac-wk2')
88             # Note we do not add 'wk2' here, even though it's included in _skipped_search_paths().
89         # FIXME: Perhaps we should get this list from MacPort?
90         fallback_names.append('mac')
91         return map(self._webkit_baseline_path, fallback_names)
92
93     def setup_environ_for_server(self, server_name=None):
94         env = super(WinPort, self).setup_environ_for_server(server_name)
95         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
96         return env
97
98     def operating_system(self):
99         return 'win'
100
101     def default_child_processes(self):
102         return 1
103
104     def show_results_html_file(self, results_filename):
105         self._run_script('run-safari', [abspath_to_uri(SystemHost().platform, results_filename)])
106
107     def _runtime_feature_list(self):
108         supported_features_command = [self._path_to_driver(), '--print-supported-features']
109         try:
110             output = self._executive.run_command(supported_features_command, error_handler=Executive.ignore_error)
111         except OSError, e:
112             _log.warn("Exception running driver: %s, %s.  Driver must be built before calling WebKitPort.test_expectations()." % (supported_features_command, e))
113             return None
114
115         # Note: win/DumpRenderTree.cpp does not print a leading space before the features_string.
116         match_object = re.match("SupportedFeatures:\s*(?P<features_string>.*)\s*", output)
117         if not match_object:
118             return None
119         return match_object.group('features_string').split(' ')
120
121     # Note: These are based on the stock XAMPP locations for these files.
122     def _uses_apache(self):
123         return True
124
125     def _path_to_apache(self):
126         httpdPath = "C:/xampp/apache/bin/httpd.exe"
127         if self._filesystem.exists(httpdPath):
128             return httpdPath
129         _log.error("Could not find apache. Not installed or unknown path.")
130         return None
131
132     def _path_to_lighttpd(self):
133         return "/usr/sbin/lighttpd"
134
135     def _path_to_lighttpd_modules(self):
136         return "/usr/lib/lighttpd"
137
138     def _path_to_lighttpd_php(self):
139         return "/usr/bin/php-cgi"
140
141     def _driver_tempdir_for_environment(self):
142         return cygpath(self._driver_tempdir())
143
144     def test_search_path(self):
145         test_fallback_names = [path for path in self.baseline_search_path() if not path.startswith(self._webkit_baseline_path('mac'))]
146         return map(self._webkit_baseline_path, test_fallback_names)
147
148     def _ntsd_location(self):
149         if 'PROGRAMFILES' not in os.environ:
150             return None
151         possible_paths = [self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.1", "Debuggers", "x86", "ntsd.exe"),
152             self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.1", "Debuggers", "x64", "ntsd.exe"),
153             self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.0", "Debuggers", "x86", "ntsd.exe"),
154             self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.0", "Debuggers", "x64", "ntsd.exe"),
155             self._filesystem.join(os.environ['PROGRAMFILES'], "Debugging Tools for Windows (x86)", "ntsd.exe"),
156             self._filesystem.join(os.environ['ProgramW6432'], "Debugging Tools for Windows (x64)", "ntsd.exe"),
157             self._filesystem.join(os.environ['SYSTEMROOT'], "system32", "ntsd.exe")]
158         for path in possible_paths:
159             expanded_path = self._filesystem.expanduser(path)
160             if self._filesystem.exists(expanded_path):
161                 _log.debug("Using ntsd located in '%s'" % path)
162                 return expanded_path
163         return None
164
165     def create_debugger_command_file(self):
166         debugger_temp_directory = str(self._filesystem.mkdtemp())
167         command_file = self._filesystem.join(debugger_temp_directory, "debugger-commands.txt")
168         commands = ''.join(['.logopen /t "%s\\%s.txt"\n' % (cygpath(self.results_directory()), self.CRASH_LOG_PREFIX),
169             '.srcpath "%s"\n' % cygpath(self._webkit_finder.webkit_base()),
170             '!analyze -vv\n',
171             '~*kpn\n',
172             'q\n'])
173         self._filesystem.write_text_file(command_file, commands)
174         return command_file
175
176     def read_registry_string(self, reg_path, arch, root, key):
177         registry_key = reg_path % (root, key)
178         read_registry_command = ["regtool", arch, "get", registry_key]
179         value = self._executive.run_command(read_registry_command, error_handler=Executive.ignore_error)
180         return value.rstrip()
181
182     def write_registry_value(self, reg_path, arch, root, key, regType, value):
183         registry_key = reg_path % (root, key)
184
185         _log.debug("Writing to %s" % registry_key)
186
187         set_reg_value_command = ["regtool", arch, "set", regType, str(registry_key), str(value)]
188         rc = self._executive.run_command(set_reg_value_command, return_exit_code=True)
189         if rc == 2:
190             add_reg_value_command = ["regtool", arch, "add", regType, str(registry_key)]
191             rc = self._executive.run_command(add_reg_value_command, return_exit_code=True)
192             if rc == 0:
193                 rc = self._executive.run_command(set_reg_value_command, return_exit_code=True)
194         if rc:
195             _log.warn("Error setting (%s) %s\key: %s to value: %s.  Error=%s." % (arch, root, key, value, str(rc)))
196             _log.warn("You many need to adjust permissions on the %s key." % registry_key)
197             return False
198
199         # On Windows Vista/7 with UAC enabled, regtool will fail to modify the registry, but will still
200         # return a successful exit code. So we double-check here that the value we tried to write to the
201         # registry was really written.
202         if self.read_registry_string(reg_path, arch, root, key) != str(value):
203             _log.warn("Regtool reported success, but value of key %s did not change." % key)
204             _log.warn("You many need to adjust permissions on the %s key." % registry_key)
205             return False
206
207         return True
208
209     def write_registry_string(self, reg_path, arch, root, key, value):
210         return self.write_registry_value(reg_path, arch, root, key, "-s", value)
211
212     def setup_crash_log_saving(self):
213         if '_NT_SYMBOL_PATH' not in os.environ:
214             _log.warning("The _NT_SYMBOL_PATH environment variable is not set. Using Microsoft Symbol Server.")
215             os.environ['_NT_SYMBOL_PATH'] = 'SRV*http://msdl.microsoft.com/download/symbols'
216         ntsd_path = self._ntsd_location()
217         if not ntsd_path:
218             _log.warning("Can't find ntsd.exe. Crash logs will not be saved.")
219             return None
220         # If we used -c (instead of -cf) we could pass the commands directly on the command line. But
221         # when the commands include multiple quoted paths (e.g., for .logopen and .srcpath), Windows
222         # fails to invoke the post-mortem debugger at all (perhaps due to a bug in Windows's command
223         # line parsing). So we save the commands to a file instead and tell the debugger to execute them
224         # using -cf.
225         command_file = self.create_debugger_command_file()
226         if not command_file:
227             return None
228         debugger_options = '"{0}" -p %ld -e %ld -g -noio -lines -cf "{1}"'.format(cygpath(ntsd_path), cygpath(command_file))
229         registry_settings = {'Debugger': debugger_options, 'Auto': "1"}
230         for key in registry_settings:
231             for arch in ["--wow32", "--wow64"]:
232                 self.previous_debugger_values[(arch, "HKLM", key)] = self.read_registry_string(self.POST_MORTEM_DEBUGGER_KEY, arch, "HKLM", key)
233                 self.write_registry_string(self.POST_MORTEM_DEBUGGER_KEY, arch, "HKLM", key, registry_settings[key])
234
235     def restore_crash_log_saving(self):
236         for key in self.previous_debugger_values:
237             self.write_registry_string(self.POST_MORTEM_DEBUGGER_KEY, key[0], key[1], key[2], self.previous_debugger_values[key])
238
239     def prevent_error_dialogs(self):
240         registry_settings = {'DontShowUI': 1, 'Disabled': 1}
241         for key in registry_settings:
242             for root in ["HKLM", "HKCU"]:
243                 for arch in ["--wow32", "--wow64"]:
244                     self.previous_error_reporting_values[(arch, root, key)] = self.read_registry_string(self.WINDOWS_ERROR_REPORTING_KEY, arch, root, key)
245                     self.write_registry_value(self.WINDOWS_ERROR_REPORTING_KEY, arch, root, key, "-d", registry_settings[key])
246
247     def allow_error_dialogs(self):
248         for key in self.previous_error_reporting_values:
249             self.write_registry_value(self.WINDOWS_ERROR_REPORTING_KEY, key[0], key[1], key[2], "-d", self.previous_error_reporting_values[key])
250
251     def delete_sem_locks(self):
252         os.system("rm -rf /dev/shm/sem.*")
253
254     def setup_test_run(self):
255         atexit.register(self.restore_crash_log_saving)
256         self.setup_crash_log_saving()
257         self.prevent_error_dialogs()
258         self.delete_sem_locks()
259         super(WinPort, self).setup_test_run()
260
261     def clean_up_test_run(self):
262         self.allow_error_dialogs()
263         self.restore_crash_log_saving()
264         super(WinPort, self).clean_up_test_run()
265
266     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
267         # Note that we do slow-spin here and wait, since it appears the time
268         # ReportCrash takes to actually write and flush the file varies when there are
269         # lots of simultaneous crashes going on.
270         # FIXME: Should most of this be moved into CrashLogs()?
271         time_fn = time_fn or time.time
272         sleep_fn = sleep_fn or time.sleep
273         crash_log = ''
274         crash_logs = CrashLogs(self.host, self.results_directory())
275         now = time_fn()
276         # FIXME: delete this after we're sure this code is working ...
277         _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
278         deadline = now + 5 * int(self.get_option('child_processes', 1))
279         while not crash_log and now <= deadline:
280             # If the system_pid hasn't been determined yet, just try with the passed in pid.  We'll be checking again later
281             system_pid = self._executive.pid_to_system_pid.get(pid)
282             if system_pid == None:
283                 break  # We haven't mapped cygwin pid->win pid yet
284             crash_log = crash_logs.find_newest_log(name, system_pid, include_errors=True, newer_than=newer_than)
285             if not wait_for_log:
286                 break
287             if not crash_log or not [line for line in crash_log.splitlines() if line.startswith('quit:')]:
288                 sleep_fn(0.1)
289                 now = time_fn()
290
291         if not crash_log:
292             return (stderr, None)
293         return (stderr, crash_log)
294
295     def look_for_new_crash_logs(self, crashed_processes, start_time):
296         """Since crash logs can take a long time to be written out if the system is
297            under stress do a second pass at the end of the test run.
298
299            crashes: test_name -> pid, process_name tuple of crashed process
300            start_time: time the tests started at.  We're looking for crash
301                logs after that time.
302         """
303         crash_logs = {}
304         for (test_name, process_name, pid) in crashed_processes:
305             # Passing None for output.  This is a second pass after the test finished so
306             # if the output had any logging we would have already collected it.
307             crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
308             if crash_log:
309                 crash_logs[test_name] = crash_log
310         return crash_logs
311
312     def find_system_pid(self, name, pid):
313         system_pid = int(pid)
314         # Windows and Cygwin PIDs are not the same.  We need to find the Windows
315         # PID for our Cygwin process so we can match it later to any crash
316         # files we end up creating (which will be tagged with the Windows PID)
317         ps_process = self._executive.run_command(['ps', '-e'], error_handler=Executive.ignore_error)
318         for line in ps_process.splitlines():
319             tokens = line.strip().split()
320             try:
321                 cpid, ppid, pgid, winpid, tty, uid, stime, process_name = tokens
322                 if process_name.endswith(name):
323                     self._executive.pid_to_system_pid[int(cpid)] = int(winpid)
324                     if int(pid) == int(cpid):
325                         system_pid = int(winpid)
326                     break
327             except ValueError, e:
328                 pass
329
330         return system_pid
331
332     def logging_patterns_to_strip(self):
333         # rdar://problem/18681688
334         return [(re.compile('AQMEIOManager::FindIOUnit: error \-1\n'), '')]