4 ### Catch input and trap it in an X window
6 ### (c) 2008 Straylight/Edgeware
9 ###----- Licensing notice ---------------------------------------------------
11 ### This file is part of the Edgeware X tools collection.
13 ### X tools is free software; you can redistribute it and/or modify
14 ### it under the terms of the GNU General Public License as published by
15 ### the Free Software Foundation; either version 2 of the License, or
16 ### (at your option) any later version.
18 ### X tools is distributed in the hope that it will be useful,
19 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ### GNU General Public License for more details.
23 ### You should have received a copy of the GNU General Public License
24 ### along with X tools; if not, write to the Free Software Foundation,
25 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ###--------------------------------------------------------------------------
30 ### External dependencies.
33 from sys import stdin, stdout, stderr, exit
38 import subprocess as S
41 import traceback as TB
44 GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO
46 ###--------------------------------------------------------------------------
49 def nonblocking(file):
51 Make the FILE be nonblocking.
53 FILE may be either an integer file descriptor, or something whose fileno
54 method yields up a file descriptor.
56 if isinstance(file, int):
60 flags = FC.fcntl(fd, FC.F_GETFL)
61 FC.fcntl(fd, FC.F_SETFL, flags | OS.O_NONBLOCK)
63 class complain (object):
65 Function decorator: catch exceptions and report them in an error box.
73 The decorated function is called normally. If no exception occurs (or at
74 least none propagates out of the function) then it returns normally too.
75 Otherwise, the exception is trapped and displayed in a Message window, and
76 the function returns DEFAULT, which defaults to None.
79 def __init__(me, default = None):
80 """Initializer: store the DEFAULT value for later."""
83 def __call__(me, func):
84 """Decorate the function."""
87 return func(*args, **kw)
89 type, info, _ = SYS.exc_info()
90 if isinstance(type, str):
91 head = 'Unexpected exception'
95 msg = ', '.join(info.args)
96 XT.Message(title = 'Error!', type = 'error', headline = head,
97 buttons = ['gtk-ok'], message = msg).ask()
101 ###--------------------------------------------------------------------------
102 ### Watching processes.
104 class Reaper (object):
106 The Reaper catches SIGCHLD and collects exit statuses.
108 There should ideally be only one instance of the class; the reaper method
109 returns the instance, creating it if necessary. (The reaper uses up
110 resources which we can avoid wasting under some circumstances.)
112 Call add(KID, FUNC) to watch process-id KID; when it exits, the reaper
113 calls FUNC(KID, STATUS), where STATUS is the exit status directly from
116 Even though SIGCHLD occurs at unpredictable times, processes are not reaped
117 until we return to the GTK event loop. This means that you can safely
118 create processes and add them without needing to interlock with the reaper
119 in complicated ways, and it also means that handler functions are not
120 called at unpredictable times.
125 Initialize the reaper.
127 We create a pipe and register the read end of it with the GTK event
128 system. The SIGCHLD handler writes a byte to the pipe.
131 me._prd, pwr = OS.pipe()
133 SIG.signal(SIG.SIGCHLD, lambda sig, tb: OS.write(pwr, '?'))
134 GO.io_add_watch(me._prd, GO.IO_IN | GO.IO_HUP, me._wake)
139 """Return the instance of the Reaper, creating it if necessary."""
140 if cls._reaper is None:
144 def add(me, kid, func):
146 Register the process-id KID with the reaper, calling FUNC when it exits.
148 As described, FUNC is called with two arguments, the KID and its exit
151 me._kidmap[kid] = func
153 def _wake(me, file, reason):
155 Called when the event loop notices something in the signal pipe.
157 We empty the pipe and then reap any processes which need it.
160 ## Empty the pipe. It doesn't matter how many bytes are stored in the
161 ## pipe, or what their contents are.
164 OS.read(me._prd, 16384)
166 if err.errno != E.EAGAIN:
169 ## Reap processes and pass their exit statuses on.
172 kid, st = OS.waitpid(-1, OS.WNOHANG)
174 if err.errno == E.ECHILD:
181 func = me._kidmap[kid]
187 ## Done: call me again.
190 ###--------------------------------------------------------------------------
191 ### Catching and displaying output.
193 class Catcher (object):
195 Catcher objects watch an input file and display the results in a window.
197 Initialization is a little cumbersome. You make an object, and then add a
198 file and maybe a process-id to watch. The catcher will not create a window
199 until it actually needs to display something.
201 The object can be configured by setting attributes before it first opens
204 * title: is the title for the window
205 * font: is the font to display text in, as a string
207 The rc attribute is set to a suitable exit status.
211 """Initialize the catcher."""
220 def watch_file(me, file):
222 Watch the FILE for input.
224 Any data arriving for the FILE is recoded into UTF-8 and displayed in the
225 window. The file not reaching EOF is considered a reason not to end the
230 me._src = GO.io_add_watch(file, GO.IO_IN | GO.IO_HUP, me._ready)
233 def watch_kid(me, kid):
235 Watch the process-id KID for exit.
237 If the child dies abnormally then a message is written to the window.
240 Reaper.reaper().add(kid, me._exit)
244 Construct the output window if necessary.
246 Having the window open is a reason to continue.
249 ## If the window exists, just make sure it's visible.
250 if me._window is not None:
251 if me._openp == False:
258 buf = GTK.TextBuffer()
259 me._deftag = buf.create_tag('default')
260 if me.font is not None:
261 me._deftag.set_properties(font = me.font)
263 buf.create_tag('exit', style_set = True, style = P.STYLE_ITALIC)
267 win = GTK.Window(GTK.WINDOW_TOPLEVEL)
268 win.set_title(me.title)
269 win.connect('delete-event', me._delete)
270 win.connect('key-press-event', me._keypress)
271 view = GTK.TextView(buf)
272 view.set_editable(False)
273 scr = GTK.ScrolledWindow()
274 scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC)
275 scr.set_shadow_type(GTK.SHADOW_IN)
277 win.set_default_size(480, 200)
286 def _keypress(me, win, event):
288 Handle a keypress on the window.
290 Escape or Q will close the window.
292 key = GDK.keyval_name(event.keyval)
293 if key in ['Escape', 'q', 'Q']:
300 Handle a close request on the window.
302 Closing the window removes a reason to continue.
309 def _ready(me, file, *_):
311 Process input arriving on the FILE.
314 buf = file.read(16384)
316 if err.errno == E.EAGAIN:
325 uni = buf.decode(SYS.getdefaultencoding(), 'replace')
326 utf8 = uni.encode('utf-8')
327 end = me._buf.get_end_iter()
328 me._buf.insert_with_tags(end, utf8, me._deftag)
333 Close the input file.
336 GO.source_remove(me._src)
339 def _exit(me, kid, st):
341 Handle the child process exiting.
348 end = me._buf.get_end_iter()
349 if not end.starts_line():
350 me._buf.insert(end, '\n')
352 msg = 'exited with status %d' % OS.WEXITSTATUS(st)
353 me.rc = OS.WEXITSTATUS(st)
354 elif OS.WIFSIGNALED(st):
355 msg = 'killed by signal %d' % OS.WTERMSIG(st)
356 me.rc = OS.WTERMSIG(st) | 128
358 msg += ' (core dumped)'
360 msg = 'exited with unknown code 0x%x' % st
362 me._buf.insert_with_tags(end, '\n[%s]\n' % msg,
363 me._deftag, me._exittag)
365 ###--------------------------------------------------------------------------
370 Parse the command line, returning a triple (PARSER, OPTS, ARGS).
373 op = XT.make_optparse \
376 'help': "Read input from FILE."}),
379 'help': "Display output using FONT."})],
381 usage = '%prog [-f FILE] [-F FONT] [COMMAND [ARGS...]]')
383 op.set_defaults(file = None,
386 opts, args = op.parse_args()
387 if len(args) > 0 and opts.file is not None:
388 op.error("Can't read from a file and a command simultaneously.")
389 return op, opts, args
391 ###--------------------------------------------------------------------------
397 op, opts, args = parse_args()
399 ## Set up the file to read from.
401 if opts.file is not None:
404 catcher.watch_file(stdin)
407 catcher.watch(open(opts.file, 'r'))
410 catcher.watch_file(stdin)
412 name = ' '.join(args)
414 proc = S.Popen(args, stdout = S.PIPE, stderr = S.STDOUT)
415 catcher.watch_file(proc.stdout)
416 catcher.watch_kid(proc.pid)
418 catcher.title = 'xcatch: ' + name
419 catcher.font = opts.font
421 ## Let things run their course.
425 if __name__ == '__main__':
428 ###----- That's all, folks --------------------------------------------------