Skip to content

Commit 2c0488a

Browse files
committed
Make X11Window class handle xdotool/xprop errors gracefully
X11Window no longer raises exceptions if xdotool/xprop fail with non-zero return codes. Placeholder values are now returned where applicable and the subprocess's stderr text is printed. Window control methods return True if successful and False otherwise.
1 parent ff03f05 commit 2c0488a

File tree

1 file changed

+98
-44
lines changed

1 file changed

+98
-44
lines changed

dragonfly/windows/x11_window.py

+98-44
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
2424
"""
2525

26+
from __future__ import print_function
27+
2628
import locale
2729
import logging
2830
from subprocess import Popen, PIPE
31+
import sys
2932

3033
import psutil
3134
from six import binary_type
@@ -39,6 +42,9 @@ class X11Window(BaseWindow):
3942
The Window class is an interface to the window control and
4043
placement APIs for X11.
4144
45+
Window control methods such as :meth:`close` will return ``True``
46+
if successful.
47+
4248
"""
4349

4450
_log = logging.getLogger("window")
@@ -51,23 +57,31 @@ class X11Window(BaseWindow):
5157
xprop = "xprop"
5258

5359
@classmethod
54-
def _run_xdotool_command(cls, arguments, error_on_failure=True):
55-
return cls._run_command(cls.xdotool, arguments, error_on_failure)
60+
def _run_xdotool_command(cls, arguments):
61+
return cls._run_command(cls.xdotool, arguments)
62+
63+
@classmethod
64+
def _run_xdotool_command_simple(cls, arguments):
65+
# Run the command and return whether or not it succeeded based on
66+
# the return code.
67+
stdout, return_code = cls._run_command(cls.xdotool, arguments)
68+
if stdout: print(stdout)
69+
return return_code == 0
5670

5771
@classmethod
58-
def _run_xprop_command(cls, arguments, error_on_failure=True):
59-
return cls._run_command(cls.xprop, arguments, error_on_failure)
72+
def _run_xprop_command(cls, arguments):
73+
return cls._run_command(cls.xprop, arguments)
6074

6175
@classmethod
62-
def _run_command(cls, command, arguments, error_on_failure=True):
76+
def _run_command(cls, command, arguments):
6377
"""
6478
Run a command with arguments and return the result.
6579
6680
:param command: command to run
6781
:type command: str
6882
:param arguments: arguments to append
6983
:type arguments: list
70-
:returns: stdout, stderr, return_code
84+
:returns: stdout, return_code
7185
:rtype: tuple
7286
"""
7387
arguments = [str(arg) for arg in arguments]
@@ -85,16 +99,13 @@ def _run_command(cls, command, arguments, error_on_failure=True):
8599
if isinstance(stderr, binary_type):
86100
stderr = stderr.decode(encoding)
87101

88-
# Handle non-zero return codes.
89-
if p.wait() > 0 and error_on_failure:
90-
print(stdout)
91-
print(stderr)
92-
raise RuntimeError("%s command exited with non-zero return"
93-
" code %d" % (command, p.returncode))
102+
stderr = stderr.rstrip()
103+
if stderr:
104+
print(stderr, file=sys.stderr)
94105

95106
# Return the process output and return code.
96-
return stdout.rstrip(), stderr.rstrip(), p.returncode
97-
except Exception as e:
107+
return stdout.rstrip(), p.returncode
108+
except OSError as e:
98109
cls._log.error("Failed to execute command '%s': %s. Is "
99110
"%s installed?",
100111
full_readable_command, e, command)
@@ -105,16 +116,27 @@ def _run_command(cls, command, arguments, error_on_failure=True):
105116

106117
@classmethod
107118
def get_foreground(cls):
108-
window_id, _, _ = cls._run_xdotool_command(["getactivewindow"])
109-
return cls.get_window(int(window_id))
119+
window_id, return_code = cls._run_xdotool_command([
120+
"getactivewindow"
121+
])
122+
if return_code == 0:
123+
return cls.get_window(int(window_id))
124+
else:
125+
return cls.get_window(0) # return an invalid window
110126

111127
@classmethod
112128
def get_all_windows(cls):
113129
# Get all window IDs using 'xdotool search'.
114-
output, _, _ = cls._run_xdotool_command(['search', '--onlyvisible',
115-
'--name', ''])
116-
lines = [line for line in output.split('\n') if line]
117-
windows = [cls.get_window(int(line)) for line in lines]
130+
stdout, return_code = cls._run_xdotool_command([
131+
'search', '--onlyvisible', '--name', ''
132+
])
133+
if return_code == 0:
134+
lines = [line for line in stdout.split('\n') if line]
135+
windows = [cls.get_window(int(line)) for line in lines]
136+
else:
137+
# Return any windows found previously.
138+
if stdout: print(stdout)
139+
return list(cls._windows_by_id.values())
118140

119141
# Exclude window IDs that have no associated process ID.
120142
result = []
@@ -140,9 +162,15 @@ def get_matching_windows(cls, executable=None, title=None):
140162
args.append(title)
141163
else:
142164
args.append('')
143-
output, _, _ = cls._run_xdotool_command(args, False)
144-
lines = [line for line in output.split('\n') if line]
145-
windows = [cls.get_window(int(line)) for line in lines]
165+
stdout, return_code = cls._run_xdotool_command(args)
166+
if return_code == 0:
167+
lines = [line for line in stdout.split('\n') if line]
168+
windows = [cls.get_window(int(line)) for line in lines]
169+
else:
170+
# Use windows found previously.
171+
if stdout: print(stdout)
172+
windows = list(cls._windows_by_id.values())
173+
146174
matching = []
147175
for window in windows:
148176
if executable:
@@ -174,11 +202,12 @@ def _get_properties_from_xprop(self, *properties):
174202
# This method retrieves windows properties by shelling out to xprop.
175203
result = {}
176204
args = ['-id', self.id] + list(properties)
177-
output, _, return_code = self._run_xprop_command(args, False)
205+
stdout, return_code = self._run_xprop_command(args)
178206
if return_code > 0:
207+
if stdout: print(stdout)
179208
return {}
180209

181-
for line in output.split('\n'):
210+
for line in stdout.split('\n'):
182211
line = line.split(' =', 1)
183212
if len(line) != 2:
184213
continue
@@ -208,7 +237,12 @@ def _get_properties_from_xprop(self, *properties):
208237
def _get_window_text(self):
209238
# Get the title text.
210239
args = ['getwindowname', self.id]
211-
return self._run_xdotool_command(args)[0]
240+
stdout, return_code = self._run_xdotool_command(args)
241+
if return_code == 0:
242+
return stdout
243+
else:
244+
if stdout: print(stdout)
245+
return ""
212246

213247
def _get_class_name(self):
214248
return (self._get_properties_from_xprop("WM_CLASS")
@@ -364,9 +398,13 @@ def is_focused(self):
364398
# Methods related to window geometry.
365399

366400
def get_position(self):
367-
output, _, _ = self._run_xdotool_command(
401+
stdout, return_code = self._run_xdotool_command(
368402
['getwindowgeometry', '--shell', self.id])
369-
geometry = output.strip().split('\n')
403+
if return_code > 0:
404+
if stdout: print(stdout)
405+
return Rectangle(0, 0, 0, 0)
406+
407+
geometry = stdout.strip().split('\n')
370408
geo = dict([val.lower()
371409
for val in line.split('=')]
372410
for line in geometry)
@@ -376,44 +414,58 @@ def get_position(self):
376414
def set_position(self, rectangle):
377415
l, t, w, h = rectangle.ltwh
378416
id = self.id
379-
self._run_xdotool_command(
417+
return self._run_xdotool_command_simple(
380418
['windowmove', id, l, t,
381-
'windowsize', id, w, h])
419+
'windowsize', id, w, h]
420+
)
382421

383422
#-----------------------------------------------------------------------
384423
# Methods for miscellaneous window control.
385424

386425
def minimize(self):
387-
self._run_xdotool_command(['windowminimize', self.id])
426+
# Attempt to minimize the window. Return the command's success.
427+
return self._run_xdotool_command_simple(['windowminimize',
428+
self.id])
388429

389430
def _toggle_maximize(self):
390431
# Doesn't seem possible with xdotool. We'll try pressing a-f10
391-
# with the window focused.
392-
self.set_foreground()
393-
self._run_xdotool_command(['keydown', 'Alt_L', 'key', 'F10',
394-
'keyup', 'Alt_L'])
432+
# with the window focused. Only maximize if set_foreground()
433+
# succeeded.
434+
return self.set_foreground() and self._run_xdotool_command_simple([
435+
'keydown', 'Alt_L', 'key', 'F10', 'keyup', 'Alt_L'
436+
])
395437

396438
def maximize(self):
397439
if not self.is_maximized:
398-
self._toggle_maximize()
440+
return self._toggle_maximize()
441+
return True # already maximized.
399442

400443
def restore(self):
401444
state = self.state
402445
if self._is_minimized(state):
403-
self._run_xdotool_command(['windowactivate', self.id])
446+
return self._run_xdotool_command_simple([
447+
'windowactivate', self.id
448+
])
404449
elif self._is_maximized(state):
405-
self._toggle_maximize()
450+
return self._toggle_maximize()
451+
else:
452+
# True if already restored or False if no _NET_WM_STATE.
453+
return state is not None
406454

407455
def close(self):
408-
self._run_xdotool_command(['windowclose', self.id])
456+
return self._run_xdotool_command_simple(['windowclose', self.id])
409457

410458
def set_foreground(self):
411-
if self.is_minimized:
412-
self.restore()
459+
# Restore if minimized.
460+
if self.is_minimized and not self.restore():
461+
return False # restore() failed
413462
if not self.is_focused:
414463
id = '%i' % self.id
415-
self._run_xdotool_command(['windowactivate', id,
416-
'windowfocus', id])
464+
return self._run_xdotool_command_simple([
465+
'windowactivate', id, 'windowfocus', id
466+
])
467+
468+
return True
417469

418470
def set_focus(self):
419471
"""
@@ -422,4 +474,6 @@ def set_focus(self):
422474
This method will set the input focus, but will not necessarily bring
423475
the window to the front.
424476
"""
425-
self._run_xdotool_command(['windowfocus', self.id])
477+
return self._run_xdotool_command_simple([
478+
'windowfocus', self.id
479+
])

0 commit comments

Comments
 (0)