#! @PYTHON@ ### -*-python-*- ### ### Catch input and trap it in an X window ### ### (c) 2008 Straylight/Edgeware ### ###----- Licensing notice --------------------------------------------------- ### ### This file is part of the Edgeware X tools collection. ### ### X tools is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by ### the Free Software Foundation; either version 2 of the License, or ### (at your option) any later version. ### ### X tools is distributed in the hope that it will be useful, ### but WITHOUT ANY WARRANTY; without even the implied warranty of ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### GNU General Public License for more details. ### ### You should have received a copy of the GNU General Public License ### along with X tools; if not, write to the Free Software Foundation, ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. VERSION = '@VERSION@' ###-------------------------------------------------------------------------- ### External dependencies. import optparse as O from sys import stdin, stdout, stderr, exit import sys as SYS import os as OS import fcntl as FC import errno as E import subprocess as S import pango as P import signal as SIG import traceback as TB import xtoys as XT GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO ###-------------------------------------------------------------------------- ### Utilities. def nonblocking(file): """ Make the FILE be nonblocking. FILE may be either an integer file descriptor, or something whose fileno method yields up a file descriptor. """ if isinstance(file, int): fd = file else: fd = file.fileno() flags = FC.fcntl(fd, FC.F_GETFL) FC.fcntl(fd, FC.F_SETFL, flags | OS.O_NONBLOCK) class complain (object): """ Function decorator: catch exceptions and report them in an error box. Example: @complain(DEFAULT) def foo(...): ... The decorated function is called normally. If no exception occurs (or at least none propagates out of the function) then it returns normally too. Otherwise, the exception is trapped and displayed in a Message window, and the function returns DEFAULT, which defaults to None. """ def __init__(me, default = None): """Initializer: store the DEFAULT value for later.""" me._default = default def __call__(me, func): """Decorate the function.""" def _(*args, **kw): try: return func(*args, **kw) except: type, info, _ = SYS.exc_info() if isinstance(type, str): head = 'Unexpected exception' msg = type else: head = type.__name__ msg = ', '.join(info.args) XT.Message(title = 'Error!', type = 'error', headline = head, buttons = ['gtk-ok'], message = msg).ask() return me._default return _ ###-------------------------------------------------------------------------- ### Watching processes. class Reaper (object): """ The Reaper catches SIGCHLD and collects exit statuses. There should ideally be only one instance of the class; the reaper method returns the instance, creating it if necessary. (The reaper uses up resources which we can avoid wasting under some circumstances.) Call add(KID, FUNC) to watch process-id KID; when it exits, the reaper calls FUNC(KID, STATUS), where STATUS is the exit status directly from wait. Even though SIGCHLD occurs at unpredictable times, processes are not reaped until we return to the GTK event loop. This means that you can safely create processes and add them without needing to interlock with the reaper in complicated ways, and it also means that handler functions are not called at unpredictable times. """ def __init__(me): """ Initialize the reaper. We create a pipe and register the read end of it with the GTK event system. The SIGCHLD handler writes a byte to the pipe. """ me._kidmap = {} me._prd, pwr = OS.pipe() nonblocking(me._prd) SIG.signal(SIG.SIGCHLD, lambda sig, tb: OS.write(pwr, '?')) GO.io_add_watch(me._prd, GO.IO_IN | GO.IO_HUP, me._wake) _reaper = None @classmethod def reaper(cls): """Return the instance of the Reaper, creating it if necessary.""" if cls._reaper is None: cls._reaper = cls() return cls._reaper def add(me, kid, func): """ Register the process-id KID with the reaper, calling FUNC when it exits. As described, FUNC is called with two arguments, the KID and its exit status. """ me._kidmap[kid] = func def _wake(me, file, reason): """ Called when the event loop notices something in the signal pipe. We empty the pipe and then reap any processes which need it. """ ## Empty the pipe. It doesn't matter how many bytes are stored in the ## pipe, or what their contents are. try: while True: OS.read(me._prd, 16384) except OSError, err: if err.errno != E.EAGAIN: raise ## Reap processes and pass their exit statuses on. while True: try: kid, st = OS.waitpid(-1, OS.WNOHANG) except OSError, err: if err.errno == E.ECHILD: break else: raise if kid == 0: break try: func = me._kidmap[kid] del me._kidmap[kid] except KeyError: continue func(kid, st) ## Done: call me again. return True ###-------------------------------------------------------------------------- ### Catching and displaying output. class Catcher (object): """ Catcher objects watch an input file and display the results in a window. Initialization is a little cumbersome. You make an object, and then add a file and maybe a process-id to watch. The catcher will not create a window until it actually needs to display something. The object can be configured by setting attributes before it first opens its window. * title: is the title for the window * font: is the font to display text in, as a string The rc attribute is set to a suitable exit status. """ def __init__(me): """Initialize the catcher.""" me.title = 'xcatch' me._file = None me._window = None me._buf = None me.font = None me._openp = False me.rc = 0 def watch_file(me, file): """ Watch the FILE for input. Any data arriving for the FILE is recoded into UTF-8 and displayed in the window. The file not reaching EOF is considered a reason not to end the program. """ XT.addreason() nonblocking(file) me._src = GO.io_add_watch(file, GO.IO_IN | GO.IO_HUP, me._ready) me._file = file def watch_kid(me, kid): """ Watch the process-id KID for exit. If the child dies abnormally then a message is written to the window. """ XT.addreason() Reaper.reaper().add(kid, me._exit) def make_window(me): """ Construct the output window if necessary. Having the window open is a reason to continue. """ ## If the window exists, just make sure it's visible. if me._window is not None: if me._openp == False: me._window.present() XT.addreason() me._openp = True return ## Make the buffer. buf = GTK.TextBuffer() me._deftag = buf.create_tag('default') if me.font is not None: me._deftag.set_properties(font = me.font) me._exittag = \ buf.create_tag('exit', style_set = True, style = P.STYLE_ITALIC) me._buf = buf ## Make the window. win = GTK.Window(GTK.WINDOW_TOPLEVEL) win.set_title(me.title) win.connect('delete-event', me._delete) win.connect('key-press-event', me._keypress) view = GTK.TextView(buf) view.set_editable(False) scr = GTK.ScrolledWindow() scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC) scr.set_shadow_type(GTK.SHADOW_IN) scr.add(view) win.set_default_size(480, 200) win.add(scr) ## All done. win.show_all() XT.addreason() me._openp = True me._window = win def _keypress(me, win, event): """ Handle a keypress on the window. Escape or Q will close the window. """ key = GDK.keyval_name(event.keyval) if key in ['Escape', 'q', 'Q']: me._delete() return True return False def _delete(me, *_): """ Handle a close request on the window. Closing the window removes a reason to continue. """ me._window.hide() XT.delreason() me._openp = False return True @complain(True) def _ready(me, file, *_): """ Process input arriving on the FILE. """ try: buf = file.read(16384) except IOError, err: if err.errno == E.EAGAIN: return True me._close() me.rc = 127 raise if buf == '': me._close() return True me.make_window() uni = buf.decode(SYS.getdefaultencoding(), 'replace') utf8 = uni.encode('utf-8') end = me._buf.get_end_iter() me._buf.insert_with_tags(end, utf8, me._deftag) return True def _close(me): """ Close the input file. """ XT.delreason() GO.source_remove(me._src) me._file = None def _exit(me, kid, st): """ Handle the child process exiting. """ if st == 0: XT.delreason() return me.make_window() XT.delreason() end = me._buf.get_end_iter() if not end.starts_line(): me._buf.insert(end, '\n') if OS.WIFEXITED(st): msg = 'exited with status %d' % OS.WEXITSTATUS(st) me.rc = OS.WEXITSTATUS(st) elif OS.WIFSIGNALED(st): msg = 'killed by signal %d' % OS.WTERMSIG(st) me.rc = OS.WTERMSIG(st) | 128 if OS.WCOREDUMP(st): msg += ' (core dumped)' else: msg = 'exited with unknown code 0x%x' % st me.rc = 255 me._buf.insert_with_tags(end, '\n[%s]\n' % msg, me._deftag, me._exittag) ###-------------------------------------------------------------------------- ### Option parsing. def parse_args(): """ Parse the command line, returning a triple (PARSER, OPTS, ARGS). """ op = XT.make_optparse \ ([('f', 'file', {'dest': 'file', 'help': "Read input from FILE."}), ('F', 'font', {'dest': 'font', 'help': "Display output using FONT."})], version = VERSION, usage = '%prog [-f FILE] [-F FONT] [COMMAND [ARGS...]]') op.set_defaults(file = None, font = 'monospace') opts, args = op.parse_args() if len(args) > 0 and opts.file is not None: op.error("Can't read from a file and a command simultaneously.") return op, opts, args ###-------------------------------------------------------------------------- ### Main program. def main(): ## Check options. op, opts, args = parse_args() ## Set up the file to read from. catcher = Catcher() if opts.file is not None: if opts.file == '-': name = '' catcher.watch_file(stdin) else: name = opts.file catcher.watch_file(open(opts.file, 'r')) elif len(args) == 0: name = '' catcher.watch_file(stdin) else: name = ' '.join(args) Reaper.reaper() proc = S.Popen(args, stdout = S.PIPE, stderr = S.STDOUT) catcher.watch_file(proc.stdout) catcher.watch_kid(proc.pid) catcher.title = 'xcatch: ' + name catcher.font = opts.font ## Let things run their course. GTK.main() exit(catcher.rc) if __name__ == '__main__': main() ###----- That's all, folks --------------------------------------------------