1
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
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
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
105
106 doDelay(timeout)
107 else:
108
109
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:
123 pass
124 doDelay(interval)
125 return pid
126
127
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
141
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
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
168 INTERVAL_MS = 1000
169 main_loop = GLib.MainLoop()
170
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
181 self.highlight_window.destroy()
182 self.main_loop.quit()
183 return False
184
185
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
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
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
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
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
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
263 return (dconfEnabled or envEnabled)
264
265
267 if sys.argv[0].endswith("pydoc"):
268 return
269 try:
270 if file("/proc/%s/cmdline" % os.getpid()).read().find('epydoc') != -1:
271 return
272 except:
273 pass
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
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
295
296
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
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):
344
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
371
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
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