• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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