OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Utilities for interacting with processes on a win32 system.""" |
| 7 |
| 8 import logging |
| 9 import os |
| 10 import pywintypes |
| 11 import re |
| 12 import subprocess |
| 13 import sys |
| 14 import time |
| 15 import win32api |
| 16 import win32com.client |
| 17 import win32con |
| 18 import win32event |
| 19 import win32gui |
| 20 import win32process |
| 21 |
| 22 from kasko.util import ScopedStartStop |
| 23 |
| 24 |
| 25 _DEFAULT_TIMEOUT = 10 # Seconds. |
| 26 _LOGGER = logging.getLogger(os.path.basename(__file__)) |
| 27 |
| 28 |
| 29 def ImportSelenium(webdriver_dir=None): |
| 30 """Imports Selenium from the given path.""" |
| 31 global webdriver |
| 32 global service |
| 33 if webdriver_dir: |
| 34 sys.path.append(webdriver_dir) |
| 35 from selenium import webdriver |
| 36 import selenium.webdriver.chrome.service as service |
| 37 |
| 38 |
| 39 def FindChromeProcessId(user_data_dir, timeout=_DEFAULT_TIMEOUT): |
| 40 """Finds the process ID of a given Chrome instance.""" |
| 41 udd = os.path.abspath(user_data_dir) |
| 42 |
| 43 # Find the message window. |
| 44 started = time.time() |
| 45 elapsed = 0 |
| 46 msg_win = None |
| 47 while msg_win is None: |
| 48 try: |
| 49 win = win32gui.FindWindowEx(None, None, 'Chrome_MessageWindow', udd) |
| 50 if win != 0: |
| 51 msg_win = win |
| 52 break |
| 53 except pywintypes.error: |
| 54 continue |
| 55 |
| 56 time.sleep(0.1) |
| 57 elapsed = time.time() - started |
| 58 if elapsed >= timeout: |
| 59 raise TimeoutException() |
| 60 |
| 61 # Get the process ID associated with the message window. |
| 62 tid, pid = win32process.GetWindowThreadProcessId(msg_win) |
| 63 |
| 64 return pid |
| 65 |
| 66 |
| 67 def ShutdownProcess(process_id, timeout, force=False): |
| 68 """Attempts to nicely close the specified process. |
| 69 |
| 70 Returns the exit code on success. Raises an error on failure. |
| 71 """ |
| 72 |
| 73 # Open the process in question, so we can wait for it to exit. |
| 74 permissions = win32con.SYNCHRONIZE | win32con.PROCESS_QUERY_INFORMATION |
| 75 process_handle = win32api.OpenProcess(permissions, False, process_id) |
| 76 |
| 77 # Loop around to periodically retry to close Chrome. |
| 78 started = time.time() |
| 79 elapsed = 0 |
| 80 while True: |
| 81 _LOGGER.debug('Shutting down process with PID=%d.', process_id) |
| 82 |
| 83 with open(os.devnull, 'w') as f: |
| 84 cmd = ['taskkill.exe', '/PID', str(process_id)] |
| 85 if force: |
| 86 cmd.append('/F') |
| 87 subprocess.call(cmd, shell=True, stdout=f, stderr=f) |
| 88 |
| 89 # Wait at most 2 seconds after each call to taskkill. |
| 90 curr_timeout_ms = int(max(2, timeout - elapsed) * 1000) |
| 91 |
| 92 _LOGGER.debug('Waiting for process with PID=%d to exit.', process_id) |
| 93 result = win32event.WaitForSingleObject(process_handle, curr_timeout_ms) |
| 94 # Exit the loop on successful wait. |
| 95 if result == win32event.WAIT_OBJECT_0: |
| 96 break |
| 97 |
| 98 elapsed = time.time() - started |
| 99 if elapsed > timeout: |
| 100 _LOGGER.debug('Timeout waiting for process to exit.') |
| 101 raise TimeoutException() |
| 102 |
| 103 exit_status = win32process.GetExitCodeProcess(process_handle) |
| 104 process_handle.Close() |
| 105 _LOGGER.debug('Process exited with status %d.', exit_status) |
| 106 |
| 107 return exit_status |
| 108 |
| 109 |
| 110 def _WmiTimeToLocalEpoch(wmitime): |
| 111 """Converts a WMI time string to a Unix epoch time.""" |
| 112 # The format of WMI times is: yyyymmddHHMMSS.xxxxxx[+-]UUU, where |
| 113 # UUU is the number of minutes between local time and UTC. |
| 114 m = re.match('^(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})' |
| 115 '(?P<hour>\d{2})(?P<minutes>\d{2})(?P<seconds>\d{2}\.\d+)' |
| 116 '(?P<offset>[+-]\d{3})$', wmitime) |
| 117 if not m: |
| 118 raise Exception('Invalid WMI time string.') |
| 119 |
| 120 # This parses the time as a local time. |
| 121 t = time.mktime(time.strptime(wmitime[0:14], '%Y%m%d%H%M%S')) |
| 122 |
| 123 # Add the fractional part of the seconds that wasn't parsed by strptime. |
| 124 s = float(m.group('seconds')) |
| 125 t += s - int(s) |
| 126 |
| 127 return t |
| 128 |
| 129 |
| 130 def GetProcessCreationDate(pid): |
| 131 """Returns the process creation date as local unix epoch time.""" |
| 132 wmi = win32com.client.GetObject('winmgmts:') |
| 133 procs = wmi.ExecQuery( |
| 134 'select CreationDate from Win32_Process where ProcessId = %s' % pid) |
| 135 for proc in procs: |
| 136 return _WmiTimeToLocalEpoch(proc.Properties_('CreationDate').Value) |
| 137 raise Exception('Unable to find process with PID %d.' % pid) |
| 138 |
| 139 |
| 140 def ShutdownChildren(parent_pid, child_exe, started_after, started_before, |
| 141 timeout=_DEFAULT_TIMEOUT, force=False): |
| 142 """Shuts down any lingering child processes of a given parent. |
| 143 |
| 144 This is an inherently racy thing to do as process IDs are aggressively reused |
| 145 on Windows. Filtering by a valid known |started_after| and |started_before| |
| 146 timestamp, as well as by the executable of the child process resolves this |
| 147 issue. Ugh. |
| 148 """ |
| 149 started = time.time() |
| 150 wmi = win32com.client.GetObject('winmgmts:') |
| 151 _LOGGER.debug('Shutting down lingering children processes.') |
| 152 for proc in wmi.InstancesOf('Win32_Process'): |
| 153 if proc.Properties_('ParentProcessId').Value != parent_pid: |
| 154 continue |
| 155 if proc.Properties_('ExecutablePath').Value != child_exe: |
| 156 continue |
| 157 t = _WmiTimeToLocalEpoch(proc.Properties_('CreationDate').Value) |
| 158 if t <= started_after or t >= started_before: |
| 159 continue |
| 160 pid = proc.Properties_('ProcessId').Value |
| 161 remaining = max(0, started + timeout - time.time()) |
| 162 ShutdownProcess(pid, remaining, force=force) |
| 163 |
| 164 |
| 165 class ChromeInstance(object): |
| 166 """A class encapsulating a running instance of Chrome for testing. |
| 167 |
| 168 The Chrome instance is controlled via chromedriver and Selenium.""" |
| 169 |
| 170 def __init__(self, chromedriver, chrome, user_data_dir): |
| 171 self.chromedriver_ = os.path.abspath(chromedriver) |
| 172 self.chrome_ = os.path.abspath(chrome) |
| 173 self.user_data_dir_ = user_data_dir |
| 174 |
| 175 def start(self, timeout=_DEFAULT_TIMEOUT): |
| 176 capabilities = { |
| 177 'chromeOptions': { |
| 178 'args': [ |
| 179 # This allows automated navigation to chrome:// URLs. |
| 180 '--enable-gpu-benchmarking', |
| 181 '--user-data-dir=%s' % self.user_data_dir_, |
| 182 ], |
| 183 'binary': self.chrome_, |
| 184 } |
| 185 } |
| 186 |
| 187 # Use a _ScopedStartStop helper so the service and driver clean themselves |
| 188 # up in case of any exceptions. |
| 189 _LOGGER.info('Starting chromedriver') |
| 190 with ScopedStartStop(service.Service(self.chromedriver_)) as \ |
| 191 scoped_service: |
| 192 _LOGGER.info('Starting chrome') |
| 193 with ScopedStartStop(webdriver.Remote(scoped_service.service.service_url, |
| 194 capabilities), |
| 195 start=lambda x: None, stop=lambda x: x.quit()) as \ |
| 196 scoped_driver: |
| 197 self.pid_ = FindChromeProcessId(self.user_data_dir_, timeout) |
| 198 self.started_at_ = GetProcessCreationDate(self.pid_) |
| 199 _LOGGER.debug('Chrome launched.') |
| 200 self.driver_ = scoped_driver.release() |
| 201 self.service_ = scoped_service.release() |
| 202 |
| 203 |
| 204 def stop(self, timeout=_DEFAULT_TIMEOUT): |
| 205 started = time.time() |
| 206 self.driver_.quit() |
| 207 self.stopped_at_ = time.time() |
| 208 self.service_.stop() |
| 209 self.driver_ = None |
| 210 self.service = None |
| 211 |
| 212 # Ensure that any lingering children processes are torn down as well. This |
| 213 # is generally racy on Windows, but is gated based on parent process ID, |
| 214 # child executable, and start time of the child process. These criteria |
| 215 # ensure we don't go indiscriminately killing processes. |
| 216 remaining = max(0, started + timeout - time.time()) |
| 217 ShutdownChildren(self.pid_, self.chrome_, self.started_at_, |
| 218 self.stopped_at_, remaining, force=True) |
| 219 |
| 220 def navigate_to(self, url): |
| 221 """Navigates the running Chrome instance to the provided URL.""" |
| 222 self.driver_.get(url) |
OLD | NEW |