Commit | Line | Data |
---|---|---|
bce8c6ee MW |
1 | #! @PYTHON@ |
2 | ### -*-python-*- | |
3 | ### | |
4 | ### Catch input and trap it in an X window | |
5 | ### | |
6 | ### (c) 2008 Straylight/Edgeware | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This file is part of the Edgeware X tools collection. | |
12 | ### | |
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. | |
17 | ### | |
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. | |
22 | ### | |
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. | |
26 | ||
27 | VERSION = '@VERSION@' | |
28 | ||
29 | ###-------------------------------------------------------------------------- | |
30 | ### External dependencies. | |
31 | ||
32 | import optparse as O | |
33 | from sys import stdin, stdout, stderr, exit | |
34 | import sys as SYS | |
35 | import os as OS | |
36 | import fcntl as FC | |
37 | import errno as E | |
38 | import subprocess as S | |
39 | import pango as P | |
40 | import signal as SIG | |
41 | import traceback as TB | |
42 | ||
43 | import xtoys as XT | |
44 | GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO | |
45 | ||
46 | ###-------------------------------------------------------------------------- | |
47 | ### Utilities. | |
48 | ||
49 | def nonblocking(file): | |
50 | """ | |
51 | Make the FILE be nonblocking. | |
52 | ||
53 | FILE may be either an integer file descriptor, or something whose fileno | |
54 | method yields up a file descriptor. | |
55 | """ | |
56 | if isinstance(file, int): | |
57 | fd = file | |
58 | else: | |
59 | fd = file.fileno() | |
60 | flags = FC.fcntl(fd, FC.F_GETFL) | |
61 | FC.fcntl(fd, FC.F_SETFL, flags | OS.O_NONBLOCK) | |
62 | ||
63 | class complain (object): | |
64 | """ | |
65 | Function decorator: catch exceptions and report them in an error box. | |
66 | ||
67 | Example: | |
68 | ||
69 | @complain(DEFAULT) | |
70 | def foo(...): | |
71 | ... | |
72 | ||
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. | |
77 | """ | |
78 | ||
79 | def __init__(me, default = None): | |
80 | """Initializer: store the DEFAULT value for later.""" | |
81 | me._default = default | |
82 | ||
83 | def __call__(me, func): | |
84 | """Decorate the function.""" | |
85 | def _(*args, **kw): | |
86 | try: | |
87 | return func(*args, **kw) | |
88 | except: | |
89 | type, info, _ = SYS.exc_info() | |
90 | if isinstance(type, str): | |
91 | head = 'Unexpected exception' | |
92 | msg = type | |
93 | else: | |
94 | head = type.__name__ | |
95 | msg = ', '.join(info.args) | |
96 | XT.Message(title = 'Error!', type = 'error', headline = head, | |
97 | buttons = ['gtk-ok'], message = msg).ask() | |
98 | return me._default | |
99 | return _ | |
100 | ||
101 | ###-------------------------------------------------------------------------- | |
102 | ### Watching processes. | |
103 | ||
104 | class Reaper (object): | |
105 | """ | |
106 | The Reaper catches SIGCHLD and collects exit statuses. | |
107 | ||
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.) | |
111 | ||
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 | |
114 | wait. | |
115 | ||
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. | |
121 | """ | |
122 | ||
123 | def __init__(me): | |
124 | """ | |
125 | Initialize the reaper. | |
126 | ||
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. | |
129 | """ | |
130 | me._kidmap = {} | |
131 | me._prd, pwr = OS.pipe() | |
132 | nonblocking(me._prd) | |
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) | |
135 | ||
136 | _reaper = None | |
137 | @classmethod | |
138 | def reaper(cls): | |
139 | """Return the instance of the Reaper, creating it if necessary.""" | |
140 | if cls._reaper is None: | |
141 | cls._reaper = cls() | |
142 | return cls._reaper | |
143 | ||
144 | def add(me, kid, func): | |
145 | """ | |
146 | Register the process-id KID with the reaper, calling FUNC when it exits. | |
147 | ||
148 | As described, FUNC is called with two arguments, the KID and its exit | |
149 | status. | |
150 | """ | |
151 | me._kidmap[kid] = func | |
152 | ||
153 | def _wake(me, file, reason): | |
154 | """ | |
155 | Called when the event loop notices something in the signal pipe. | |
156 | ||
157 | We empty the pipe and then reap any processes which need it. | |
158 | """ | |
159 | ||
160 | ## Empty the pipe. It doesn't matter how many bytes are stored in the | |
161 | ## pipe, or what their contents are. | |
162 | try: | |
163 | while True: | |
164 | OS.read(me._prd, 16384) | |
165 | except OSError, err: | |
166 | if err.errno != E.EAGAIN: | |
167 | raise | |
168 | ||
169 | ## Reap processes and pass their exit statuses on. | |
170 | while True: | |
171 | try: | |
172 | kid, st = OS.waitpid(-1, OS.WNOHANG) | |
173 | except OSError, err: | |
174 | if err.errno == E.ECHILD: | |
175 | break | |
176 | else: | |
177 | raise | |
178 | if kid == 0: | |
179 | break | |
180 | try: | |
181 | func = me._kidmap[kid] | |
182 | del me._kidmap[kid] | |
183 | except KeyError: | |
184 | continue | |
185 | func(kid, st) | |
186 | ||
187 | ## Done: call me again. | |
188 | return True | |
189 | ||
190 | ###-------------------------------------------------------------------------- | |
191 | ### Catching and displaying output. | |
192 | ||
193 | class Catcher (object): | |
194 | """ | |
195 | Catcher objects watch an input file and display the results in a window. | |
196 | ||
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. | |
200 | ||
201 | The object can be configured by setting attributes before it first opens | |
202 | its window. | |
203 | ||
204 | * title: is the title for the window | |
205 | * font: is the font to display text in, as a string | |
206 | ||
207 | The rc attribute is set to a suitable exit status. | |
208 | """ | |
209 | ||
210 | def __init__(me): | |
211 | """Initialize the catcher.""" | |
212 | me.title = 'xcatch' | |
213 | me._file = None | |
214 | me._window = None | |
215 | me._buf = None | |
216 | me.font = None | |
217 | me._openp = False | |
218 | me.rc = 0 | |
219 | ||
220 | def watch_file(me, file): | |
221 | """ | |
222 | Watch the FILE for input. | |
223 | ||
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 | |
226 | program. | |
227 | """ | |
228 | XT.addreason() | |
229 | nonblocking(file) | |
230 | me._src = GO.io_add_watch(file, GO.IO_IN | GO.IO_HUP, me._ready) | |
231 | me._file = file | |
232 | ||
233 | def watch_kid(me, kid): | |
234 | """ | |
235 | Watch the process-id KID for exit. | |
236 | ||
237 | If the child dies abnormally then a message is written to the window. | |
238 | """ | |
239 | XT.addreason() | |
240 | Reaper.reaper().add(kid, me._exit) | |
241 | ||
242 | def make_window(me): | |
243 | """ | |
244 | Construct the output window if necessary. | |
245 | ||
246 | Having the window open is a reason to continue. | |
247 | """ | |
248 | ||
249 | ## If the window exists, just make sure it's visible. | |
250 | if me._window is not None: | |
251 | if me._openp == False: | |
252 | me._window.present() | |
253 | XT.addreason() | |
254 | me._openp = True | |
255 | return | |
256 | ||
257 | ## Make the buffer. | |
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) | |
262 | me._exittag = \ | |
263 | buf.create_tag('exit', style_set = True, style = P.STYLE_ITALIC) | |
264 | me._buf = buf | |
265 | ||
266 | ## Make the window. | |
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) | |
276 | scr.add(view) | |
277 | win.set_default_size(480, 200) | |
278 | win.add(scr) | |
279 | ||
280 | ## All done. | |
281 | win.show_all() | |
282 | XT.addreason() | |
283 | me._openp = True | |
284 | me._window = win | |
285 | ||
286 | def _keypress(me, win, event): | |
287 | """ | |
288 | Handle a keypress on the window. | |
289 | ||
290 | Escape or Q will close the window. | |
291 | """ | |
292 | key = GDK.keyval_name(event.keyval) | |
293 | if key in ['Escape', 'q', 'Q']: | |
294 | me._delete() | |
295 | return True | |
296 | return False | |
297 | ||
298 | def _delete(me, *_): | |
299 | """ | |
300 | Handle a close request on the window. | |
301 | ||
302 | Closing the window removes a reason to continue. | |
303 | """ | |
304 | me._window.hide() | |
305 | XT.delreason() | |
306 | me._openp = False | |
58b5e33d | 307 | return True |
bce8c6ee MW |
308 | |
309 | @complain(True) | |
310 | def _ready(me, file, *_): | |
311 | """ | |
312 | Process input arriving on the FILE. | |
313 | """ | |
314 | try: | |
315 | buf = file.read(16384) | |
316 | except IOError, err: | |
317 | if err.errno == E.EAGAIN: | |
318 | return True | |
319 | me._close() | |
320 | me.rc = 127 | |
321 | raise | |
322 | if buf == '': | |
323 | me._close() | |
324 | return True | |
325 | me.make_window() | |
326 | uni = buf.decode(SYS.getdefaultencoding(), 'replace') | |
327 | utf8 = uni.encode('utf-8') | |
328 | end = me._buf.get_end_iter() | |
329 | me._buf.insert_with_tags(end, utf8, me._deftag) | |
330 | return True | |
331 | ||
332 | def _close(me): | |
333 | """ | |
334 | Close the input file. | |
335 | """ | |
336 | XT.delreason() | |
337 | GO.source_remove(me._src) | |
338 | me._file = None | |
339 | ||
340 | def _exit(me, kid, st): | |
341 | """ | |
342 | Handle the child process exiting. | |
343 | """ | |
344 | if st == 0: | |
345 | XT.delreason() | |
346 | return | |
347 | me.make_window() | |
348 | XT.delreason() | |
349 | end = me._buf.get_end_iter() | |
350 | if not end.starts_line(): | |
351 | me._buf.insert(end, '\n') | |
352 | if OS.WIFEXITED(st): | |
353 | msg = 'exited with status %d' % OS.WEXITSTATUS(st) | |
354 | me.rc = OS.WEXITSTATUS(st) | |
355 | elif OS.WIFSIGNALED(st): | |
356 | msg = 'killed by signal %d' % OS.WTERMSIG(st) | |
357 | me.rc = OS.WTERMSIG(st) | 128 | |
358 | if OS.WCOREDUMP(st): | |
359 | msg += ' (core dumped)' | |
360 | else: | |
361 | msg = 'exited with unknown code 0x%x' % st | |
362 | me.rc = 255 | |
363 | me._buf.insert_with_tags(end, '\n[%s]\n' % msg, | |
364 | me._deftag, me._exittag) | |
365 | ||
366 | ###-------------------------------------------------------------------------- | |
367 | ### Option parsing. | |
368 | ||
369 | def parse_args(): | |
370 | """ | |
371 | Parse the command line, returning a triple (PARSER, OPTS, ARGS). | |
372 | """ | |
373 | ||
374 | op = XT.make_optparse \ | |
375 | ([('f', 'file', | |
376 | {'dest': 'file', | |
377 | 'help': "Read input from FILE."}), | |
378 | ('F', 'font', | |
379 | {'dest': 'font', | |
380 | 'help': "Display output using FONT."})], | |
381 | version = VERSION, | |
382 | usage = '%prog [-f FILE] [-F FONT] [COMMAND [ARGS...]]') | |
383 | ||
384 | op.set_defaults(file = None, | |
385 | font = 'monospace') | |
386 | ||
387 | opts, args = op.parse_args() | |
388 | if len(args) > 0 and opts.file is not None: | |
389 | op.error("Can't read from a file and a command simultaneously.") | |
390 | return op, opts, args | |
391 | ||
392 | ###-------------------------------------------------------------------------- | |
393 | ### Main program. | |
394 | ||
395 | def main(): | |
396 | ||
397 | ## Check options. | |
398 | op, opts, args = parse_args() | |
399 | ||
400 | ## Set up the file to read from. | |
401 | catcher = Catcher() | |
402 | if opts.file is not None: | |
403 | if opts.file == '-': | |
404 | name = '<stdin>' | |
405 | catcher.watch_file(stdin) | |
406 | else: | |
d174a250 MW |
407 | name = opts.file |
408 | catcher.watch_file(open(opts.file, 'r')) | |
bce8c6ee MW |
409 | elif len(args) == 0: |
410 | name = '<stdin>' | |
411 | catcher.watch_file(stdin) | |
412 | else: | |
413 | name = ' '.join(args) | |
414 | Reaper.reaper() | |
415 | proc = S.Popen(args, stdout = S.PIPE, stderr = S.STDOUT) | |
416 | catcher.watch_file(proc.stdout) | |
417 | catcher.watch_kid(proc.pid) | |
418 | ||
419 | catcher.title = 'xcatch: ' + name | |
420 | catcher.font = opts.font | |
421 | ||
422 | ## Let things run their course. | |
423 | GTK.main() | |
424 | exit(catcher.rc) | |
425 | ||
426 | if __name__ == '__main__': | |
427 | main() | |
428 | ||
429 | ###----- That's all, folks -------------------------------------------------- |