1#!/usr/bin/env python 2# Copyright (c) 2011 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"""SiteCompare module for invoking, locating, and manipulating windows. 7 8This module is a catch-all wrapper for operating system UI functionality 9that doesn't belong in other modules. It contains functions for finding 10particular windows, scraping their contents, and invoking processes to 11create them. 12""" 13 14import os 15import string 16import time 17 18import PIL.ImageGrab 19import pywintypes 20import win32event 21import win32gui 22import win32process 23 24 25def FindChildWindows(hwnd, path): 26 """Find a set of windows through a path specification. 27 28 Args: 29 hwnd: Handle of the parent window 30 path: Path to the window to find. Has the following form: 31 "foo/bar/baz|foobar/|foobarbaz" 32 The slashes specify the "path" to the child window. 33 The text is the window class, a pipe (if present) is a title. 34 * is a wildcard and will find all child windows at that level 35 36 Returns: 37 A list of the windows that were found 38 """ 39 windows_to_check = [hwnd] 40 41 # The strategy will be to take windows_to_check and use it 42 # to find a list of windows that match the next specification 43 # in the path, then repeat with the list of found windows as the 44 # new list of windows to check 45 for segment in path.split("/"): 46 windows_found = [] 47 check_values = segment.split("|") 48 49 # check_values is now a list with the first element being 50 # the window class, the second being the window caption. 51 # If the class is absent (or wildcarded) set it to None 52 if check_values[0] == "*" or not check_values[0]: check_values[0] = None 53 54 # If the window caption is also absent, force it to None as well 55 if len(check_values) == 1: check_values.append(None) 56 57 # Loop through the list of windows to check 58 for window_check in windows_to_check: 59 window_found = None 60 while window_found != 0: # lint complains, but 0 != None 61 if window_found is None: window_found = 0 62 try: 63 # Look for the next sibling (or first sibling if window_found is 0) 64 # of window_check with the specified caption and/or class 65 window_found = win32gui.FindWindowEx( 66 window_check, window_found, check_values[0], check_values[1]) 67 except pywintypes.error, e: 68 # FindWindowEx() raises error 2 if not found 69 if e[0] == 2: 70 window_found = 0 71 else: 72 raise e 73 74 # If FindWindowEx struck gold, add to our list of windows found 75 if window_found: windows_found.append(window_found) 76 77 # The windows we found become the windows to check for the next segment 78 windows_to_check = windows_found 79 80 return windows_found 81 82 83def FindChildWindow(hwnd, path): 84 """Find a window through a path specification. 85 86 This method is a simple wrapper for FindChildWindows() for the 87 case (the majority case) where you expect to find a single window 88 89 Args: 90 hwnd: Handle of the parent window 91 path: Path to the window to find. See FindChildWindows() 92 93 Returns: 94 The window that was found 95 """ 96 return FindChildWindows(hwnd, path)[0] 97 98 99def ScrapeWindow(hwnd, rect=None): 100 """Scrape a visible window and return its contents as a bitmap. 101 102 Args: 103 hwnd: handle of the window to scrape 104 rect: rectangle to scrape in client coords, defaults to the whole thing 105 If specified, it's a 4-tuple of (left, top, right, bottom) 106 107 Returns: 108 An Image containing the scraped data 109 """ 110 # Activate the window 111 SetForegroundWindow(hwnd) 112 113 # If no rectangle was specified, use the fill client rectangle 114 if not rect: rect = win32gui.GetClientRect(hwnd) 115 116 upper_left = win32gui.ClientToScreen(hwnd, (rect[0], rect[1])) 117 lower_right = win32gui.ClientToScreen(hwnd, (rect[2], rect[3])) 118 rect = upper_left+lower_right 119 120 return PIL.ImageGrab.grab(rect) 121 122 123def SetForegroundWindow(hwnd): 124 """Bring a window to the foreground.""" 125 win32gui.SetForegroundWindow(hwnd) 126 127 128def InvokeAndWait(path, cmdline="", timeout=10, tick=1.): 129 """Invoke an application and wait for it to bring up a window. 130 131 Args: 132 path: full path to the executable to invoke 133 cmdline: command line to pass to executable 134 timeout: how long (in seconds) to wait before giving up 135 tick: length of time to wait between checks 136 137 Returns: 138 A tuple of handles to the process and the application's window, 139 or (None, None) if it timed out waiting for the process 140 """ 141 142 def EnumWindowProc(hwnd, ret): 143 """Internal enumeration func, checks for visibility and proper PID.""" 144 if win32gui.IsWindowVisible(hwnd): # don't bother even checking hidden wnds 145 pid = win32process.GetWindowThreadProcessId(hwnd)[1] 146 if pid == ret[0]: 147 ret[1] = hwnd 148 return 0 # 0 means stop enumeration 149 return 1 # 1 means continue enumeration 150 151 # We don't need to change anything about the startupinfo structure 152 # (the default is quite sufficient) but we need to create it just the 153 # same. 154 sinfo = win32process.STARTUPINFO() 155 156 proc = win32process.CreateProcess( 157 path, # path to new process's executable 158 cmdline, # application's command line 159 None, # process security attributes (default) 160 None, # thread security attributes (default) 161 False, # inherit parent's handles 162 0, # creation flags 163 None, # environment variables 164 None, # directory 165 sinfo) # default startup info 166 167 # Create process returns (prochandle, pid, threadhandle, tid). At 168 # some point we may care about the other members, but for now, all 169 # we're after is the pid 170 pid = proc[2] 171 172 # Enumeration APIs can take an arbitrary integer, usually a pointer, 173 # to be passed to the enumeration function. We'll pass a pointer to 174 # a structure containing the PID we're looking for, and an empty out 175 # parameter to hold the found window ID 176 ret = [pid, None] 177 178 tries_until_timeout = timeout/tick 179 num_tries = 0 180 181 # Enumerate top-level windows, look for one with our PID 182 while num_tries < tries_until_timeout and ret[1] is None: 183 try: 184 win32gui.EnumWindows(EnumWindowProc, ret) 185 except pywintypes.error, e: 186 # error 0 isn't an error, it just meant the enumeration was 187 # terminated early 188 if e[0]: raise e 189 190 time.sleep(tick) 191 num_tries += 1 192 193 # TODO(jhaas): Should we throw an exception if we timeout? Or is returning 194 # a window ID of None sufficient? 195 return (proc[0], ret[1]) 196 197 198def WaitForProcessExit(proc, timeout=None): 199 """Waits for a given process to terminate. 200 201 Args: 202 proc: handle to process 203 timeout: timeout (in seconds). None = wait indefinitely 204 205 Returns: 206 True if process ended, False if timed out 207 """ 208 if timeout is None: 209 timeout = win32event.INFINITE 210 else: 211 # convert sec to msec 212 timeout *= 1000 213 214 return (win32event.WaitForSingleObject(proc, timeout) == 215 win32event.WAIT_OBJECT_0) 216 217 218def WaitForThrobber(hwnd, rect=None, timeout=20, tick=0.1, done=10): 219 """Wait for a browser's "throbber" (loading animation) to complete. 220 221 Args: 222 hwnd: window containing the throbber 223 rect: rectangle of the throbber, in client coords. If None, whole window 224 timeout: if the throbber is still throbbing after this long, give up 225 tick: how often to check the throbber 226 done: how long the throbber must be unmoving to be considered done 227 228 Returns: 229 Number of seconds waited, -1 if timed out 230 """ 231 if not rect: rect = win32gui.GetClientRect(hwnd) 232 233 # last_throbber will hold the results of the preceding scrape; 234 # we'll compare it against the current scrape to see if we're throbbing 235 last_throbber = ScrapeWindow(hwnd, rect) 236 start_clock = time.clock() 237 timeout_clock = start_clock + timeout 238 last_changed_clock = start_clock; 239 240 while time.clock() < timeout_clock: 241 time.sleep(tick) 242 243 current_throbber = ScrapeWindow(hwnd, rect) 244 if current_throbber.tostring() != last_throbber.tostring(): 245 last_throbber = current_throbber 246 last_changed_clock = time.clock() 247 else: 248 if time.clock() - last_changed_clock > done: 249 return last_changed_clock - start_clock 250 251 return -1 252 253 254def MoveAndSizeWindow(wnd, position=None, size=None, child=None): 255 """Moves and/or resizes a window. 256 257 Repositions and resizes a window. If a child window is provided, 258 the parent window is resized so the child window has the given size 259 260 Args: 261 wnd: handle of the frame window 262 position: new location for the frame window 263 size: new size for the frame window (or the child window) 264 child: handle of the child window 265 266 Returns: 267 None 268 """ 269 rect = win32gui.GetWindowRect(wnd) 270 271 if position is None: position = (rect[0], rect[1]) 272 if size is None: 273 size = (rect[2]-rect[0], rect[3]-rect[1]) 274 elif child is not None: 275 child_rect = win32gui.GetWindowRect(child) 276 slop = (rect[2]-rect[0]-child_rect[2]+child_rect[0], 277 rect[3]-rect[1]-child_rect[3]+child_rect[1]) 278 size = (size[0]+slop[0], size[1]+slop[1]) 279 280 win32gui.MoveWindow(wnd, # window to move 281 position[0], # new x coord 282 position[1], # new y coord 283 size[0], # new width 284 size[1], # new height 285 True) # repaint? 286 287 288def EndProcess(proc, code=0): 289 """Ends a process. 290 291 Wraps the OS TerminateProcess call for platform-independence 292 293 Args: 294 proc: process ID 295 code: process exit code 296 297 Returns: 298 None 299 """ 300 win32process.TerminateProcess(proc, code) 301 302 303def URLtoFilename(url, path=None, extension=None): 304 """Converts a URL to a filename, given a path. 305 306 This in theory could cause collisions if two URLs differ only 307 in unprintable characters (eg. http://www.foo.com/?bar and 308 http://www.foo.com/:bar. In practice this shouldn't be a problem. 309 310 Args: 311 url: The URL to convert 312 path: path to the directory to store the file 313 extension: string to append to filename 314 315 Returns: 316 filename 317 """ 318 trans = string.maketrans(r'\/:*?"<>|', '_________') 319 320 if path is None: path = "" 321 if extension is None: extension = "" 322 if len(path) > 0 and path[-1] != '\\': path += '\\' 323 url = url.translate(trans) 324 return "%s%s%s" % (path, url, extension) 325 326 327def PreparePath(path): 328 """Ensures that a given path exists, making subdirectories if necessary. 329 330 Args: 331 path: fully-qualified path of directory to ensure exists 332 333 Returns: 334 None 335 """ 336 try: 337 os.makedirs(path) 338 except OSError, e: 339 if e[0] != 17: raise e # error 17: path already exists 340 341 342def main(): 343 PreparePath(r"c:\sitecompare\scrapes\ie7") 344 # We're being invoked rather than imported. Let's do some tests 345 346 # Hardcode IE's location for the purpose of this test 347 (proc, wnd) = InvokeAndWait( 348 r"c:\program files\internet explorer\iexplore.exe") 349 350 # Find the browser pane in the IE window 351 browser = FindChildWindow( 352 wnd, "TabWindowClass/Shell DocObject View/Internet Explorer_Server") 353 354 # Move and size the window 355 MoveAndSizeWindow(wnd, (0, 0), (1024, 768), browser) 356 357 # Take a screenshot 358 i = ScrapeWindow(browser) 359 360 i.show() 361 362 EndProcess(proc, 0) 363 364 365if __name__ == "__main__": 366 sys.exit(main()) 367