f84e9c9890f374ea9fade7e217d073a26fdc770d
[WebKit-https.git] / Tools / Scripts / webkitpy / port / win.py
1 # Copyright (C) 2010 Google Inc. All rights reserved.
2 # Copyright (C) 2013 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"]
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 operating_system(self):
94         return 'win'
95
96     def default_child_processes(self):
97         return 1
98
99     def show_results_html_file(self, results_filename):
100         self._run_script('run-safari', [abspath_to_uri(SystemHost().platform, results_filename)])
101
102     def _runtime_feature_list(self):
103         supported_features_command = [self._path_to_driver(), '--print-supported-features']
104         try:
105             output = self._executive.run_command(supported_features_command, error_handler=Executive.ignore_error)
106         except OSError, e:
107             _log.warn("Exception running driver: %s, %s.  Driver must be built before calling WebKitPort.test_expectations()." % (supported_features_command, e))
108             return None
109
110         # Note: win/DumpRenderTree.cpp does not print a leading space before the features_string.
111         match_object = re.match("SupportedFeatures:\s*(?P<features_string>.*)\s*", output)
112         if not match_object:
113             return None
114         return match_object.group('features_string').split(' ')
115
116     # Note: These are based on the stock XAMPP locations for these files.
117     def _uses_apache(self):
118         return True
119
120     def _path_to_apache(self):
121         httpdPath = "C:/xampp/apache/bin/httpd.exe"
122         if self._filesystem.exists(httpdPath):
123             return httpdPath
124         _log.error("Could not find apache. Not installed or unknown path.")
125         return None
126
127     def _path_to_lighttpd(self):
128         return "/usr/sbin/lighttpd"
129
130     def _path_to_lighttpd_modules(self):
131         return "/usr/lib/lighttpd"
132
133     def _path_to_lighttpd_php(self):
134         return "/usr/bin/php-cgi"
135
136     def _driver_tempdir_for_environment(self):
137         return cygpath(self._driver_tempdir())
138
139     def test_search_path(self):
140         test_fallback_names = [path for path in self.baseline_search_path() if not path.startswith(self._webkit_baseline_path('mac'))]
141         return map(self._webkit_baseline_path, test_fallback_names)
142
143     def _ntsd_location(self):
144         if 'PROGRAMFILES' not in os.environ:
145             return None
146         possible_paths = [self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.1", "Debuggers", "x86", "ntsd.exe"),
147             self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.1", "Debuggers", "x64", "ntsd.exe"),
148             self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.0", "Debuggers", "x86", "ntsd.exe"),
149             self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.0", "Debuggers", "x64", "ntsd.exe"),
150             self._filesystem.join(os.environ['PROGRAMFILES'], "Debugging Tools for Windows (x86)", "ntsd.exe"),
151             self._filesystem.join(os.environ['ProgramW6432'], "Debugging Tools for Windows (x64)", "ntsd.exe"),
152             self._filesystem.join(os.environ['SYSTEMROOT'], "system32", "ntsd.exe")]
153         for path in possible_paths:
154             expanded_path = self._filesystem.expanduser(path)
155             if self._filesystem.exists(expanded_path):
156                 _log.debug("Using ntsd located in '%s'" % path)
157                 return expanded_path
158         return None
159
160     def create_debugger_command_file(self):
161         debugger_temp_directory = str(self._filesystem.mkdtemp())
162         command_file = self._filesystem.join(debugger_temp_directory, "debugger-commands.txt")
163         commands = ''.join(['.logopen /t "%s\\%s.txt"\n' % (cygpath(self.results_directory()), self.CRASH_LOG_PREFIX),
164             '.srcpath "%s"\n' % cygpath(self._webkit_finder.webkit_base()),
165             '!analyze -vv\n',
166             '~*kpn\n',
167             'q\n'])
168         self._filesystem.write_text_file(command_file, commands)
169         return command_file
170
171     def read_registry_string(self, reg_path, arch, root, key):
172         registry_key = reg_path % (root, key)
173         read_registry_command = ["regtool", arch, "get", registry_key]
174         value = self._executive.run_command(read_registry_command, error_handler=Executive.ignore_error)
175         return value.rstrip()
176
177     def write_registry_value(self, reg_path, arch, root, key, regType, value):
178         registry_key = reg_path % (root, key)
179
180         _log.debug("Writing to %s" % registry_key)
181
182         set_reg_value_command = ["regtool", arch, "set", regType, str(registry_key), str(value)]
183         rc = self._executive.run_command(set_reg_value_command, return_exit_code=True)
184         if rc == 2:
185             add_reg_value_command = ["regtool", arch, "add", regType, str(registry_key)]
186             rc = self._executive.run_command(add_reg_value_command, return_exit_code=True)
187             if rc == 0:
188                 rc = self._executive.run_command(set_reg_value_command, return_exit_code=True)
189         if rc:
190             _log.warn("Error setting (%s) %s\key: %s to value: %s.  Error=%s." % (arch, root, key, value, str(rc)))
191             _log.warn("You many need to adjust permissions on the %s key." % registry_key)
192             return False
193
194         # On Windows Vista/7 with UAC enabled, regtool will fail to modify the registry, but will still
195         # return a successful exit code. So we double-check here that the value we tried to write to the
196         # registry was really written.
197         if self.read_registry_string(reg_path, arch, root, key) != str(value):
198             _log.warn("Regtool reported success, but value of key %s did not change." % key)
199             _log.warn("You many need to adjust permissions on the %s key." % registry_key)
200             return False
201
202         return True
203
204     def write_registry_string(self, reg_path, arch, root, key, value):
205         return self.write_registry_value(reg_path, arch, root, key, "-s", value)
206
207     def setup_crash_log_saving(self):
208         if '_NT_SYMBOL_PATH' not in os.environ:
209             _log.warning("The _NT_SYMBOL_PATH environment variable is not set. Using Microsoft Symbol Server.")
210             os.environ['_NT_SYMBOL_PATH'] = 'SRV*http://msdl.microsoft.com/download/symbols'
211         ntsd_path = self._ntsd_location()
212         if not ntsd_path:
213             _log.warning("Can't find ntsd.exe. Crash logs will not be saved.")
214             return None
215         # If we used -c (instead of -cf) we could pass the commands directly on the command line. But
216         # when the commands include multiple quoted paths (e.g., for .logopen and .srcpath), Windows
217         # fails to invoke the post-mortem debugger at all (perhaps due to a bug in Windows's command
218         # line parsing). So we save the commands to a file instead and tell the debugger to execute them
219         # using -cf.
220         command_file = self.create_debugger_command_file()
221         if not command_file:
222             return None
223         debugger_options = '"{0}" -p %ld -e %ld -g -noio -lines -cf "{1}"'.format(cygpath(ntsd_path), cygpath(command_file))
224         registry_settings = {'Debugger': debugger_options, 'Auto': "1"}
225         for key in registry_settings:
226             for arch in ["--wow32", "--wow64"]:
227                 self.previous_debugger_values[(arch, "HKLM", key)] = self.read_registry_string(self.POST_MORTEM_DEBUGGER_KEY, arch, "HKLM", key)
228                 self.write_registry_string(self.POST_MORTEM_DEBUGGER_KEY, arch, "HKLM", key, registry_settings[key])
229
230     def restore_crash_log_saving(self):
231         for key in self.previous_debugger_values:
232             self.write_registry_string(self.POST_MORTEM_DEBUGGER_KEY, key[0], key[1], key[2], self.previous_debugger_values[key])
233
234     def prevent_error_dialogs(self):
235         registry_settings = {'DontShowUI': 1, 'Disabled': 1}
236         for key in registry_settings:
237             for root in ["HKLM", "HKCU"]:
238                 for arch in ["--wow32", "--wow64"]:
239                     self.previous_error_reporting_values[(arch, root, key)] = self.read_registry_string(self.WINDOWS_ERROR_REPORTING_KEY, arch, root, key)
240                     self.write_registry_value(self.WINDOWS_ERROR_REPORTING_KEY, arch, root, key, "-d", registry_settings[key])
241
242     def allow_error_dialogs(self):
243         for key in self.previous_error_reporting_values:
244             self.write_registry_value(self.WINDOWS_ERROR_REPORTING_KEY, key[0], key[1], key[2], "-d", self.previous_error_reporting_values[key])
245
246     def delete_sem_locks(self):
247         os.system("rm -rf /dev/shm/sem.*")
248
249     def setup_test_run(self):
250         atexit.register(self.restore_crash_log_saving)
251         self.setup_crash_log_saving()
252         self.prevent_error_dialogs()
253         self.delete_sem_locks()
254         super(WinPort, self).setup_test_run()
255
256     def clean_up_test_run(self):
257         self.allow_error_dialogs()
258         self.restore_crash_log_saving()
259         super(WinPort, self).clean_up_test_run()
260
261     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
262         # Note that we do slow-spin here and wait, since it appears the time
263         # ReportCrash takes to actually write and flush the file varies when there are
264         # lots of simultaneous crashes going on.
265         # FIXME: Should most of this be moved into CrashLogs()?
266         time_fn = time_fn or time.time
267         sleep_fn = sleep_fn or time.sleep
268         crash_log = ''
269         crash_logs = CrashLogs(self.host, self.results_directory())
270         now = time_fn()
271         # FIXME: delete this after we're sure this code is working ...
272         _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
273         deadline = now + 5 * int(self.get_option('child_processes', 1))
274         while not crash_log and now <= deadline:
275             # If the system_pid hasn't been determined yet, just try with the passed in pid.  We'll be checking again later
276             system_pid = self._executive.pid_to_system_pid.get(pid)
277             if system_pid == None:
278                 break  # We haven't mapped cygwin pid->win pid yet
279             crash_log = crash_logs.find_newest_log(name, system_pid, include_errors=True, newer_than=newer_than)
280             if not wait_for_log:
281                 break
282             if not crash_log or not [line for line in crash_log.splitlines() if line.startswith('quit:')]:
283                 sleep_fn(0.1)
284                 now = time_fn()
285
286         if not crash_log:
287             return (stderr, None)
288         return (stderr, crash_log)
289
290     def look_for_new_crash_logs(self, crashed_processes, start_time):
291         """Since crash logs can take a long time to be written out if the system is
292            under stress do a second pass at the end of the test run.
293
294            crashes: test_name -> pid, process_name tuple of crashed process
295            start_time: time the tests started at.  We're looking for crash
296                logs after that time.
297         """
298         crash_logs = {}
299         for (test_name, process_name, pid) in crashed_processes:
300             # Passing None for output.  This is a second pass after the test finished so
301             # if the output had any logging we would have already collected it.
302             crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
303             if crash_log:
304                 crash_logs[test_name] = crash_log
305         return crash_logs
306
307     def find_system_pid(self, name, pid):
308         system_pid = int(pid)
309         # Windows and Cygwin PIDs are not the same.  We need to find the Windows
310         # PID for our Cygwin process so we can match it later to any crash
311         # files we end up creating (which will be tagged with the Windows PID)
312         ps_process = self._executive.run_command(['ps', '-e'], error_handler=Executive.ignore_error)
313         for line in ps_process.splitlines():
314             tokens = line.strip().split()
315             try:
316                 cpid, ppid, pgid, winpid, tty, uid, stime, process_name = tokens
317                 if process_name.endswith(name):
318                     self._executive.pid_to_system_pid[int(cpid)] = int(winpid)
319                     if int(pid) == int(cpid):
320                         system_pid = int(winpid)
321                     break
322             except ValueError, e:
323                 pass
324
325         return system_pid