Package dogtail :: Module utils
[hide private]
[frames] | no frames]

Source Code for Module dogtail.utils

  1  # -*- coding: utf-8 -*- 
  2  """ 
  3  Various utilities 
  4   
  5  Authors: Ed Rousseau <rousseau@redhat.com>, Zack Cerza <zcerza@redhat.com, David Malcolm <dmalcolm@redhat.com> 
  6  """ 
  7   
  8  __author__ = """Ed Rousseau <rousseau@redhat.com>, 
  9  Zack Cerza <zcerza@redhat.com, 
 10  David Malcolm <dmalcolm@redhat.com> 
 11  """ 
 12   
 13  import os 
 14  import sys 
 15  import subprocess 
 16  import cairo 
 17  import predicate 
 18  import errno 
 19  import shlex 
 20   
 21  import gi 
 22  gi.require_version('Gtk', '3.0') 
 23  gi.require_version('Gdk', '3.0') 
 24   
 25  from gi.repository import Gtk 
 26  from gi.repository import GLib 
 27  from config import config 
 28  from time import sleep 
 29  from logging import debugLogger as logger 
 30  from logging import TimeStamp 
 31  from __builtin__ import file 
 32   
 33   
34 -def screenshot(file='screenshot.png', timeStamp=True):
35 """ 36 This function wraps the ImageMagick import command to take a screenshot. 37 38 The file argument may be specified as 'foo', 'foo.png', or using any other 39 extension that ImageMagick supports. PNG is the default. 40 41 By default, screenshot filenames are in the format of foo_YYYYMMDD-hhmmss.png . 42 The timeStamp argument may be set to False to name the file foo.png. 43 """ 44 if not isinstance(timeStamp, bool): 45 raise TypeError("timeStampt must be True or False") 46 # config is supposed to create this for us. If it's not there, bail. 47 assert os.path.isdir(config.scratchDir) 48 49 baseName = ''.join(file.split('.')[0:-1]) 50 fileExt = file.split('.')[-1].lower() 51 if not baseName: 52 baseName = file 53 fileExt = 'png' 54 55 if timeStamp: 56 ts = TimeStamp() 57 newFile = ts.fileStamp(baseName) + '.' + fileExt 58 path = config.scratchDir + newFile 59 else: 60 newFile = baseName + '.' + fileExt 61 path = config.scratchDir + newFile 62 63 from gi.repository import Gdk 64 from gi.repository import GLib 65 from gi.repository import GdkPixbuf 66 rootWindow = Gdk.get_default_root_window() 67 geometry = rootWindow.get_geometry() 68 pixbuf = GdkPixbuf.Pixbuf(colorspace=GdkPixbuf.Colorspace.RGB, 69 has_alpha=False, 70 bits_per_sample=8, 71 width=geometry[2], 72 height=geometry[3]) 73 74 pixbuf = Gdk.pixbuf_get_from_window(rootWindow, 0, 0, 75 geometry[2], geometry[3]) 76 # GdkPixbuf.Pixbuf.save() needs 'jpeg' and not 'jpg' 77 if fileExt == 'jpg': 78 fileExt = 'jpeg' 79 try: 80 pixbuf.savev(path, fileExt, [], []) 81 except GLib.GError: 82 raise ValueError("Failed to save screenshot in %s format" % fileExt) 83 assert os.path.exists(path) 84 logger.log("Screenshot taken: " + path) 85 return path
86 87
88 -def run(string, timeout=config.runTimeout, interval=config.runInterval, desktop=None, dumb=False, appName=''):
89 """ 90 Runs an application. [For simple command execution such as 'rm *', use os.popen() or os.system()] 91 If dumb is omitted or is False, polls at interval seconds until the application is finished starting, or until timeout is reached. 92 If dumb is True, returns when timeout is reached. 93 """ 94 if not desktop: 95 from tree import root as desktop 96 args = shlex.split(string) 97 os.environ['GTK_MODULES'] = 'gail:atk-bridge' 98 pid = subprocess.Popen(args, env=os.environ).pid 99 100 if not appName: 101 appName = args[0] 102 103 if dumb: 104 # We're starting a non-AT-SPI-aware application. Disable startup 105 # detection. 106 doDelay(timeout) 107 else: 108 # Startup detection code 109 # The timing here is not totally precise, but it's good enough for now. 110 time = 0 111 while time < timeout: 112 time = time + interval 113 try: 114 for child in desktop.children[::-1]: 115 if child.name == appName: 116 for grandchild in child.children: 117 if grandchild.roleName == 'frame': 118 from procedural import focus 119 focus.application.node = child 120 doDelay(interval) 121 return pid 122 except AttributeError: # pragma: no cover 123 pass 124 doDelay(interval) 125 return pid
126 127
128 -def doDelay(delay=None):
129 """ 130 Utility function to insert a delay (with logging and a configurable 131 default delay) 132 """ 133 if delay is None: 134 delay = config.defaultDelay 135 if config.debugSleep: 136 logger.log("sleeping for %f" % delay) 137 sleep(delay)
138 139
140 -class Highlight (Gtk.Window): # pragma: no cover
141
142 - def __init__(self, x, y, w, h): # pragma: no cover
143 super(Highlight, self).__init__() 144 self.set_decorated(False) 145 self.set_has_resize_grip(False) 146 self.set_default_size(w, h) 147 self.screen = self.get_screen() 148 self.visual = self.screen.get_rgba_visual() 149 if self.visual is not None and self.screen.is_composited(): 150 self.set_visual(self.visual) 151 self.set_app_paintable(True) 152 self.connect("draw", self.area_draw) 153 self.show_all() 154 self.move(x, y) 155
156 - def area_draw(self, widget, cr): # pragma: no cover
157 cr.set_source_rgba(.0, .0, .0, 0.0) 158 cr.set_operator(cairo.OPERATOR_SOURCE) 159 cr.paint() 160 cr.set_operator(cairo.OPERATOR_OVER) 161 cr.set_source_rgb(0.9, 0.1, 0.1) 162 cr.set_line_width(6) 163 cr.rectangle(0, 0, self.get_size()[0], self.get_size()[1]) 164 cr.stroke() 165 166
167 -class Blinker(object): # pragma: no cover
168 INTERVAL_MS = 1000 169 main_loop = GLib.MainLoop() 170
171 - def __init__(self, x, y, w, h): # pragma: no cover
172 self.highlight_window = Highlight(x, y, w, h) 173 if self.highlight_window.screen.is_composited() is not False: 174 self.timeout_handler_id = GLib.timeout_add( 175 Blinker.INTERVAL_MS, self.destroyHighlight) 176 self.main_loop.run() 177 else: 178 self.highlight_window.destroy() 179
180 - def destroyHighlight(self): # pragma: no cover
181 self.highlight_window.destroy() 182 self.main_loop.quit() 183 return False 184 185
186 -class Lock(object):
187 188 """ 189 A mutex implementation that uses atomicity of the mkdir operation in UNIX-like 190 systems. This can be used by scripts to provide for mutual exlusion, either in single 191 scripts using threads etc. or i.e. to handle sitations of possible collisions among 192 multiple running scripts. You can choose to make randomized single-script wise locks 193 or a more general locks if you do not choose to randomize the lockdir name 194 """ 195
196 - def __init__(self, location='/tmp', lockname='dogtail_lockdir_', randomize=True):
197 """ 198 You can change the default lockdir location or name. Setting randomize to 199 False will result in no random string being appened to the lockdir name. 200 """ 201 self.lockdir = os.path.join(os.path.normpath(location), lockname) 202 if randomize: 203 self.lockdir = "%s%s" % (self.lockdir, self.__getPostfix())
204
205 - def lock(self):
206 """ 207 Creates a lockdir based on the settings on Lock() instance creation. 208 Raises OSError exception of the lock is already present. Should be 209 atomic on POSIX compliant systems. 210 """ 211 locked_msg = 'Dogtail lock: Already locked with the same lock' 212 if not os.path.exists(self.lockdir): 213 try: 214 os.mkdir(self.lockdir) 215 return self.lockdir 216 except OSError as e: 217 if e.errno == errno.EEXIST and os.path.isdir(self.lockdir): 218 raise OSError(locked_msg) 219 else: 220 raise OSError(locked_msg)
221
222 - def unlock(self):
223 """ 224 Removes a lock. Will raise OSError exception if the lock was not present. 225 Should be atomic on POSIX compliant systems. 226 """ 227 import os # have to import here for situations when executed from __del__ 228 if os.path.exists(self.lockdir): 229 try: 230 os.rmdir(self.lockdir) 231 except OSError as e: 232 if e.erron == errno.EEXIST: 233 raise OSError('Dogtail unlock: lockdir removed elsewhere!') 234 else: 235 raise OSError('Dogtail unlock: not locked')
236
237 - def __del__(self):
238 """ 239 Makes sure lock is removed when the process ends. Although not when killed indeed. 240 """ 241 self.unlock()
242
243 - def __getPostfix(self):
244 import random 245 import string 246 return ''.join(random.choice(string.letters + string.digits) for x in range(5))
247 248 249 a11yDConfKey = 'org.gnome.desktop.interface' 250 251
252 -def isA11yEnabled():
253 """ 254 Checks if accessibility is enabled via DConf. 255 """ 256 from gi.repository.Gio import Settings 257 InterfaceSettings = Settings(a11yDConfKey) 258 dconfEnabled = InterfaceSettings.get_boolean('toolkit-accessibility') 259 if os.environ.get('GTK_MODULES', '').find('gail:atk-bridge') == -1: 260 envEnabled = False 261 else: 262 envEnabled = True # pragma: no cover 263 return (dconfEnabled or envEnabled)
264 265
266 -def bailBecauseA11yIsDisabled():
267 if sys.argv[0].endswith("pydoc"): 268 return # pragma: no cover 269 try: 270 if file("/proc/%s/cmdline" % os.getpid()).read().find('epydoc') != -1: 271 return # pragma: no cover 272 except: # pragma: no cover 273 pass # pragma: no cover 274 logger.log("Dogtail requires that Assistive Technology support be enabled." 275 "\nYou can enable accessibility with sniff or by running:\n" 276 "'gsettings set org.gnome.desktop.interface toolkit-accessibility true'\nAborting...") 277 sys.exit(1)
278 279
280 -def enableA11y(enable=True):
281 """ 282 Enables accessibility via DConf. 283 """ 284 from gi.repository.Gio import Settings 285 InterfaceSettings = Settings(schema=a11yDConfKey) 286 InterfaceSettings.set_boolean('toolkit-accessibility', enable)
287 288
289 -def checkForA11y():
290 """ 291 Checks if accessibility is enabled, and halts execution if it is not. 292 """ 293 if not isA11yEnabled(): # pragma: no cover 294 bailBecauseA11yIsDisabled()
295 296
297 -def checkForA11yInteractively(): # pragma: no cover
298 """ 299 Checks if accessibility is enabled, and presents a dialog prompting the 300 user if it should be enabled if it is not already, then halts execution. 301 """ 302 if isA11yEnabled(): 303 return 304 from gi.repository import Gtk 305 dialog = Gtk.Dialog('Enable Assistive Technology Support?', 306 None, 307 Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 308 (Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE, 309 "_Enable", Gtk.ResponseType.ACCEPT)) 310 question = """Dogtail requires that Assistive Technology Support be enabled for it to function. Would you like to enable Assistive Technology support now? 311 312 Note that you will have to log out for the change to fully take effect. 313 """.strip() 314 dialog.set_default_response(Gtk.ResponseType.ACCEPT) 315 questionLabel = Gtk.Label(label=question) 316 questionLabel.set_line_wrap(True) 317 dialog.vbox.pack_start(questionLabel, True, True, 0) 318 dialog.show_all() 319 result = dialog.run() 320 if result == Gtk.ResponseType.ACCEPT: 321 logger.log("Enabling accessibility...") 322 enableA11y() 323 elif result == Gtk.ResponseType.CLOSE: 324 bailBecauseA11yIsDisabled() 325 dialog.destroy() 326 327
328 -class GnomeShell(object): # pragma: no cover
329 330 """ 331 Utility class to help working with certain atributes of gnome-shell. 332 Currently that means handling the Application menu available for apps 333 on the top gnome-shell panel. Searching for the menu and its items is 334 somewhat tricky due to fuzzy a11y tree of gnome-shell, mainly since the 335 actual menu is not present as child to the menu-spawning button. Also, 336 the menus get constructed/destroyed on the fly with application focus 337 changes. Thus current application name as displayed plus a reference 338 known menu item (with 'Quit' as default) are required by these methods. 339 """ 340
341 - def __init__(self, classic_mode=False):
342 from tree import root 343 self.shell = root.application('gnome-shell')
344
345 - def getApplicationMenuList(self, search_by_item='Quit'):
346 """ 347 Returns list of all menu item nodes. Searches for the menu by a reference item. 348 Provide a different item name, if the 'Quit' is not present - but beware picking one 349 present elsewhere, like 'Lock' or 'Power Off' present under the user menu. 350 """ 351 matches = self.shell.findChildren( 352 predicate.GenericPredicate(name=search_by_item, roleName='label')) 353 for match in matches: 354 ancestor = match.parent.parent.parent 355 if ancestor.roleName == 'panel': 356 return ancestor.findChildren(predicate.GenericPredicate(roleName='label')) 357 from tree import SearchError 358 raise SearchError("Could not find the Application menu based on '%s' item. Please provide an existing reference item" 359 % search_by_item)
360
361 - def getApplicationMenuButton(self, app_name):
362 """ 363 Returns the application menu 'button' node as present on the gnome-shell top panel. 364 """ 365 try: 366 return self.shell[0][0][3].child(app_name, roleName='label') 367 except: 368 from tree import SearchError 369 raise SearchError( 370 "Application menu button of %s could not be found within gnome-shell!" % app_name)
371
372 - def getApplicationMenuItem(self, item, search_by_item='Quit'):
373 """ 374 Returns a particilar menu item node. Uses a different 'Quit' or custom item name for reference, but also 375 attempts to use the given item if the general reference fails. 376 """ 377 try: 378 menu_items = self.getApplicationMenuList(search_by_item) 379 except: 380 menu_items = self.getApplicationMenuList(item) 381 for node in menu_items: 382 if node.name == item: 383 return node 384 raise Exception( 385 'Could not find the item, did application focus change?')
386
387 - def clickApplicationMenuItem(self, app_name, item, search_by_item='Quit'):
388 """ 389 Executes the given menu item through opening the menu first followed 390 by a click at the particular item. The menu search reference 'Quit' 391 may be customized. Also attempts to use the given item for reference 392 if search fails with the default/custom one. 393 """ 394 self.getApplicationMenuButton(app_name).click() 395 self.getApplicationMenuItem(item, search_by_item).click()
396