win.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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. import atexit
  30. import os
  31. import logging
  32. import re
  33. import sys
  34. import time
  35. from webkitpy.common.system.crashlogs import CrashLogs
  36. from webkitpy.common.system.systemhost import SystemHost
  37. from webkitpy.common.system.executive import ScriptError, Executive
  38. from webkitpy.common.system.path import abspath_to_uri, cygpath
  39. from webkitpy.port.apple import ApplePort
  40. _log = logging.getLogger(__name__)
  41. class WinPort(ApplePort):
  42. port_name = "win"
  43. VERSION_FALLBACK_ORDER = ["win-xp", "win-vista", "win-7sp0", "win"]
  44. ARCHITECTURES = ['x86']
  45. CRASH_LOG_PREFIX = "CrashLog"
  46. POST_MORTEM_DEBUGGER_KEY = "/HKLM/SOFTWARE/Microsoft/Windows NT/CurrentVersion/AeDebug/%s"
  47. previous_debugger_values = {}
  48. def do_text_results_differ(self, expected_text, actual_text):
  49. # Sanity was restored in WK2, so we don't need this hack there.
  50. if self.get_option('webkit_test_runner'):
  51. return ApplePort.do_text_results_differ(self, expected_text, actual_text)
  52. # This is a hack (which dates back to ORWT).
  53. # Windows does not have an EDITING DELEGATE, so we strip any EDITING DELEGATE
  54. # messages to make more of the tests pass.
  55. # It's possible more of the ports might want this and this could move down into WebKitPort.
  56. delegate_regexp = re.compile("^EDITING DELEGATE: .*?\n", re.MULTILINE)
  57. expected_text = delegate_regexp.sub("", expected_text)
  58. actual_text = delegate_regexp.sub("", actual_text)
  59. return expected_text != actual_text
  60. def default_baseline_search_path(self):
  61. name = self._name.replace('-wk2', '')
  62. if name.endswith(self.FUTURE_VERSION):
  63. fallback_names = [self.port_name]
  64. else:
  65. fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(name):-1] + [self.port_name]
  66. # FIXME: The AppleWin port falls back to AppleMac for some results. Eventually we'll have a shared 'apple' port.
  67. if self.get_option('webkit_test_runner'):
  68. fallback_names.insert(0, 'win-wk2')
  69. fallback_names.append('mac-wk2')
  70. # Note we do not add 'wk2' here, even though it's included in _skipped_search_paths().
  71. # FIXME: Perhaps we should get this list from MacPort?
  72. fallback_names.extend(['mac-lion', 'mac'])
  73. return map(self._webkit_baseline_path, fallback_names)
  74. def operating_system(self):
  75. return 'win'
  76. def show_results_html_file(self, results_filename):
  77. self._run_script('run-safari', [abspath_to_uri(SystemHost().platform, results_filename)])
  78. # FIXME: webkitperl/httpd.pm installs /usr/lib/apache/libphp4.dll on cycwin automatically
  79. # as part of running old-run-webkit-tests. That's bad design, but we may need some similar hack.
  80. # We might use setup_environ_for_server for such a hack (or modify apache_http_server.py).
  81. def _runtime_feature_list(self):
  82. supported_features_command = [self._path_to_driver(), '--print-supported-features']
  83. try:
  84. output = self._executive.run_command(supported_features_command, error_handler=Executive.ignore_error)
  85. except OSError, e:
  86. _log.warn("Exception running driver: %s, %s. Driver must be built before calling WebKitPort.test_expectations()." % (supported_features_command, e))
  87. return None
  88. # Note: win/DumpRenderTree.cpp does not print a leading space before the features_string.
  89. match_object = re.match("SupportedFeatures:\s*(?P<features_string>.*)\s*", output)
  90. if not match_object:
  91. return None
  92. return match_object.group('features_string').split(' ')
  93. # Note: These are based on the stock Cygwin locations for these files.
  94. def _uses_apache(self):
  95. return False
  96. def _path_to_lighttpd(self):
  97. return "/usr/sbin/lighttpd"
  98. def _path_to_lighttpd_modules(self):
  99. return "/usr/lib/lighttpd"
  100. def _path_to_lighttpd_php(self):
  101. return "/usr/bin/php-cgi"
  102. # Remove this implementation when we are confident that DumpRenderTree on Windows works properly in parallel.
  103. def default_child_processes(self):
  104. return 1
  105. def _driver_tempdir_for_environment(self):
  106. return cygpath(self._driver_tempdir())
  107. def test_search_path(self):
  108. test_fallback_names = [path for path in self.baseline_search_path() if not path.startswith(self._webkit_baseline_path('mac'))]
  109. return map(self._webkit_baseline_path, test_fallback_names)
  110. def _ntsd_location(self):
  111. possible_paths = [self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.0", "Debuggers", "x86", "ntsd.exe"),
  112. self._filesystem.join(os.environ['PROGRAMFILES'], "Windows Kits", "8.0", "Debuggers", "x64", "ntsd.exe"),
  113. self._filesystem.join(os.environ['PROGRAMFILES'], "Debugging Tools for Windows (x86)", "ntsd.exe"),
  114. self._filesystem.join(os.environ['ProgramW6432'], "Debugging Tools for Windows (x64)", "ntsd.exe"),
  115. self._filesystem.join(os.environ['SYSTEMROOT'], "system32", "ntsd.exe")]
  116. for path in possible_paths:
  117. expanded_path = self._filesystem.expanduser(path)
  118. if self._filesystem.exists(expanded_path):
  119. _log.debug("Using ntsd located in '%s'" % path)
  120. return expanded_path
  121. return None
  122. def create_debugger_command_file(self):
  123. debugger_temp_directory = str(self._filesystem.mkdtemp())
  124. command_file = self._filesystem.join(debugger_temp_directory, "debugger-commands.txt")
  125. commands = ''.join(['.logopen /t "%s\\%s.txt"\n' % (cygpath(self.results_directory()), self.CRASH_LOG_PREFIX),
  126. '.srcpath "%s"\n' % cygpath(self._webkit_finder.webkit_base()),
  127. '!analyze -vv\n',
  128. '~*kpn\n',
  129. 'q\n'])
  130. self._filesystem.write_text_file(command_file, commands)
  131. return command_file
  132. def read_registry_string(self, key):
  133. registry_key = self.POST_MORTEM_DEBUGGER_KEY % key
  134. read_registry_command = ["regtool", "--wow32", "get", registry_key]
  135. value = self._executive.run_command(read_registry_command, error_handler=Executive.ignore_error)
  136. return value.rstrip()
  137. def write_registry_string(self, key, value):
  138. registry_key = self.POST_MORTEM_DEBUGGER_KEY % key
  139. set_reg_value_command = ["regtool", "--wow32", "set", "-s", str(registry_key), str(value)]
  140. rc = self._executive.run_command(set_reg_value_command, return_exit_code=True)
  141. if rc == 2:
  142. add_reg_value_command = ["regtool", "--wow32", "add", "-s", str(registry_key)]
  143. rc = self._executive.run_command(add_reg_value_command, return_exit_code=True)
  144. if rc == 0:
  145. rc = self._executive.run_command(set_reg_value_command, return_exit_code=True)
  146. if rc:
  147. _log.warn("Error setting key: %s to value %s. Error=%ld." % (key, value, rc))
  148. return False
  149. # On Windows Vista/7 with UAC enabled, regtool will fail to modify the registry, but will still
  150. # return a successful exit code. So we double-check here that the value we tried to write to the
  151. # registry was really written.
  152. if self.read_registry_string(key) != value:
  153. _log.warn("Regtool reported success, but value of key %s did not change." % key)
  154. return False
  155. return True
  156. def setup_crash_log_saving(self):
  157. if '_NT_SYMBOL_PATH' not in os.environ:
  158. _log.warning("The _NT_SYMBOL_PATH environment variable is not set. Crash logs will not be saved.")
  159. return None
  160. ntsd_path = self._ntsd_location()
  161. if not ntsd_path:
  162. _log.warning("Can't find ntsd.exe. Crash logs will not be saved.")
  163. return None
  164. # If we used -c (instead of -cf) we could pass the commands directly on the command line. But
  165. # when the commands include multiple quoted paths (e.g., for .logopen and .srcpath), Windows
  166. # fails to invoke the post-mortem debugger at all (perhaps due to a bug in Windows's command
  167. # line parsing). So we save the commands to a file instead and tell the debugger to execute them
  168. # using -cf.
  169. command_file = self.create_debugger_command_file()
  170. if not command_file:
  171. return None
  172. debugger_options = '"{0}" -p %ld -e %ld -g -noio -lines -cf "{1}"'.format(cygpath(ntsd_path), cygpath(command_file))
  173. registry_settings = {'Debugger': debugger_options, 'Auto': "1"}
  174. for key in registry_settings:
  175. self.previous_debugger_values[key] = self.read_registry_string(key)
  176. self.write_registry_string(key, registry_settings[key])
  177. def restore_crash_log_saving(self):
  178. for key in self.previous_debugger_values:
  179. self.write_registry_string(key, self.previous_debugger_values[key])
  180. def setup_test_run(self):
  181. atexit.register(self.restore_crash_log_saving)
  182. self.setup_crash_log_saving()
  183. super(WinPort, self).setup_test_run()
  184. def clean_up_test_run(self):
  185. self.restore_crash_log_saving()
  186. super(WinPort, self).clean_up_test_run()
  187. def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
  188. # Note that we do slow-spin here and wait, since it appears the time
  189. # ReportCrash takes to actually write and flush the file varies when there are
  190. # lots of simultaneous crashes going on.
  191. # FIXME: Should most of this be moved into CrashLogs()?
  192. time_fn = time_fn or time.time
  193. sleep_fn = sleep_fn or time.sleep
  194. crash_log = ''
  195. crash_logs = CrashLogs(self.host, self.results_directory())
  196. now = time_fn()
  197. # FIXME: delete this after we're sure this code is working ...
  198. _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
  199. deadline = now + 5 * int(self.get_option('child_processes', 1))
  200. while not crash_log and now <= deadline:
  201. # If the system_pid hasn't been determined yet, just try with the passed in pid. We'll be checking again later
  202. system_pid = self._executive.pid_to_system_pid.get(pid)
  203. if system_pid == None:
  204. break # We haven't mapped cygwin pid->win pid yet
  205. crash_log = crash_logs.find_newest_log(name, system_pid, include_errors=True, newer_than=newer_than)
  206. if not wait_for_log:
  207. break
  208. if not crash_log or not [line for line in crash_log.splitlines() if line.startswith('quit:')]:
  209. sleep_fn(0.1)
  210. now = time_fn()
  211. if not crash_log:
  212. return (stderr, None)
  213. return (stderr, crash_log)
  214. def look_for_new_crash_logs(self, crashed_processes, start_time):
  215. """Since crash logs can take a long time to be written out if the system is
  216. under stress do a second pass at the end of the test run.
  217. crashes: test_name -> pid, process_name tuple of crashed process
  218. start_time: time the tests started at. We're looking for crash
  219. logs after that time.
  220. """
  221. crash_logs = {}
  222. for (test_name, process_name, pid) in crashed_processes:
  223. # Passing None for output. This is a second pass after the test finished so
  224. # if the output had any logging we would have already collected it.
  225. crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
  226. if crash_log:
  227. crash_logs[test_name] = crash_log
  228. return crash_logs
  229. def find_system_pid(self, name, pid):
  230. system_pid = int(pid)
  231. # Windows and Cygwin PIDs are not the same. We need to find the Windows
  232. # PID for our Cygwin process so we can match it later to any crash
  233. # files we end up creating (which will be tagged with the Windows PID)
  234. ps_process = self._executive.run_command(['ps', '-e'], error_handler=Executive.ignore_error)
  235. for line in ps_process.splitlines():
  236. tokens = line.strip().split()
  237. try:
  238. cpid, ppid, pgid, winpid, tty, uid, stime, process_name = tokens
  239. if process_name.endswith(name):
  240. self._executive.pid_to_system_pid[int(cpid)] = int(winpid)
  241. if int(pid) == int(cpid):
  242. system_pid = int(winpid)
  243. break
  244. except ValueError, e:
  245. pass
  246. return system_pid