Merge lp:~jamesodhunt/upstart/upstart-monitor into lp:upstart
- upstart-monitor
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1457 |
Proposed branch: | lp:~jamesodhunt/upstart/upstart-monitor |
Merge into: | lp:upstart |
Diff against target: |
898 lines (+827/-4) 8 files modified
ChangeLog (+20/-2) configure.ac (+1/-1) po/POTFILES.in (+2/-0) scripts/Makefile.am (+4/-1) scripts/data/Makefile.am (+24/-0) scripts/data/upstart-monitor.desktop (+8/-0) scripts/man/upstart-monitor.8 (+81/-0) scripts/upstart-monitor.py (+687/-0) |
To merge this branch: | bzr merge lp:~jamesodhunt/upstart/upstart-monitor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Dimitri John Ledkov | Approve | ||
Review via email: mp+153839@code.launchpad.net |
Commit message
Description of the change
Initial implementation of upstart-monitor event gui+cli tool.
See: http://
Steve Langasek (vorlon) wrote : | # |
Dimitri John Ledkov (xnox) wrote : | # |
On 20 March 2013 21:52, Steve Langasek <email address hidden> wrote:
> I notice that the UI is hand-coded in python. Are you concerned about the maintainability of this, vs. using glade and gtkbuilder?
>
Well, If one uses glade, one should be prepared to keep local builds
and send patches to upstream when it crashes on loading ubiquity.ui
for example =)
I know Colin, mostly edits gtkbuilder xml with vim. Which is good a well.
I guess long term, it might be more appropriate to do it in Qt5, once
that's on the default desktop and the iso. At least Qt UI definitions
& GUI editor do not crash.
Anyway the UI code here is quite short and easy to understand /
extend. And mostly deal with list-store, which is a pain to code up
right in Glade. One nitpick would be to use GtkGrid instead of GtkBox.
As the Box is deprecated and you will notice weird sizing problems
(just look for the tonne of bugs mpt filed about dialogs/windows being
too tall for their width).
Regards,
Dmitrijs.
- 1452. By James Hunt
-
* po/POTFILES.in: Added upstart-monitor.py.
* scripts/upstart- monitor. py: UpstartEventsGui(): Removed class
attributes and added explicit instance ones in __init__().
James Hunt (jamesodhunt) wrote : | # |
@vorlon - I have been in two minds about even putting a gui tool into the upstream package; we already have a few scripts which set some sort of precedent, but they are all cli-based.
Adding gui tools is naturally going to increase the dependencies. To try and minimise these, I've coded the monitor to not need any GUI libraries to keep the spec for actually using this utility (in some fashion) to a minimum. However, I'd personally rather not also require additional tooling like Glade as it increases the requirements and might be an extra barrier to entry (well for some - for others not using Glade might be one I suppose).
Thanks - I've updated po/.
@xnox - I'm not sure about Qt: it might be appropriate for Ubuntu specifically. Since this monitor app is so simple, I do wonder if it could be implemented in a few lines of QML anyway? Maybe we should just opt for a good old curses interface ;-)
Dimitri John Ledkov (xnox) wrote : | # |
On 21 March 2013 12:21, James Hunt <email address hidden> wrote:
> @vorlon - I have been in two minds about even putting a gui tool into the upstream package; we already have a few scripts which set some sort of precedent, but they are all cli-based.
>
> Adding gui tools is naturally going to increase the dependencies. To try and minimise these, I've coded the monitor to not need any GUI libraries to keep the spec for actually using this utility (in some fashion) to a minimum. However, I'd personally rather not also require additional tooling like Glade as it increases the requirements and might be an extra barrier to entry (well for some - for others not using Glade might be one I suppose).
Glade is a point&click interface to load/edit/save GtkBuilder xml,
which is loaded directly by GtkBuilder. So there is no extra
dependencies. One can write GtkBuilder xml by hand, which you already
do for menus in your code.
Regards,
Dmitrijs.
Steve Langasek (vorlon) wrote : | # |
$ ./scripts/
Traceback (most recent call last):
File "./scripts/
main()
File "./scripts/
win = UpstartEventsGui()
File "./scripts/
self.
File "/usr/lib/
return info.invoke(*args, **kwargs)
gi._glib.GError: Icon 'upstart-monitor' not present in theme
Is there supposed to be an icon as part of this branch?
James Hunt (jamesodhunt) wrote : | # |
Hi Steve - you need to "make install" to get the icon in the correct location or you can do:
dir=$HOME/
mkdir -p $dir
cp upstart-
dir=$HOME/
mkdir -p $dir
cp upstart-monitor.svg $dir
update-icon-caches $dir
Then, ./scripts/
Steve Langasek (vorlon) wrote : | # |
My point is, this svg file doesn't appear to be committed to the branch.
James Hunt (jamesodhunt) wrote : | # |
It's in the doc/ directory as upstart-logo.svg and gets copied to upstart-monitor.svg as part of the build.
Steve Langasek (vorlon) wrote : | # |
On Thu, Mar 21, 2013 at 09:48:22PM -0000, James Hunt wrote:
> It's in the doc/ directory as upstart-logo.svg and gets copied to
> upstart-monitor.svg as part of the build.
Ahhh ok, I missed that it was using an existing file already in the tree.
Sorry. :)
Dimitri John Ledkov (xnox) wrote : | # |
mpt would cringe at seeing that UI =) we will tweak it later ;-)
- 1453. By James Hunt
-
* Sync with lp:upstart.
Preview Diff
1 | === modified file 'ChangeLog' |
2 | --- ChangeLog 2013-03-21 11:58:30 +0000 |
3 | +++ ChangeLog 2013-03-22 10:57:19 +0000 |
4 | @@ -1,7 +1,18 @@ |
5 | 2013-03-21 James Hunt <james.hunt@ubuntu.com> |
6 | |
7 | - * po/POTFILES.in: Added missing entries for init/quiesce.c |
8 | - and init/state.c. |
9 | + * po/POTFILES.in: |
10 | + - Added missing entries for init/quiesce.c and init/state.c. |
11 | + - Added upstart-monitor.py. |
12 | + * scripts/upstart-monitor.py: UpstartEventsGui(): Removed class |
13 | + attributes and added explicit instance ones in __init__(). |
14 | + |
15 | +2013-03-18 James Hunt <james.hunt@ubuntu.com> |
16 | + |
17 | + * configure.ac: Added scripts/data/Makefile. |
18 | + * scripts/Makefile.am: Added SUBDIRS=data. |
19 | + * scripts/data/Makefile.am: New Makefile. |
20 | + * scripts/data/upstart-monitor.desktop: Desktop file for |
21 | + upstart-monitor. |
22 | |
23 | 2013-03-15 James Hunt <james.hunt@ubuntu.com> |
24 | |
25 | @@ -28,6 +39,13 @@ |
26 | * extra/conf/upstart-file-bridge.conf: Change start on condition |
27 | to ensure all filesystems are mounted before it starts. |
28 | |
29 | +2013-03-13 James Hunt <james.hunt@ubuntu.com> |
30 | + |
31 | + * scripts/man/upstart-monitor.8: New manpage. |
32 | + * scripts/upstart-monitor.py: New cli+gui tool to monitor |
33 | + Upstart events. |
34 | + * scripts/Makefile.am: Updated for upstart-monitor. |
35 | + |
36 | 2013-03-11 James Hunt <james.hunt@ubuntu.com> |
37 | |
38 | * extra/Makefile.am: Add file bridge and conf file. |
39 | |
40 | === modified file 'configure.ac' |
41 | --- configure.ac 2013-03-04 11:57:18 +0000 |
42 | +++ configure.ac 2013-03-22 10:57:19 +0000 |
43 | @@ -101,6 +101,6 @@ |
44 | AC_CONFIG_FILES([ Makefile intl/Makefile |
45 | dbus/Makefile init/Makefile util/Makefile conf/Makefile |
46 | extra/Makefile doc/Makefile contrib/Makefile po/Makefile.in |
47 | - scripts/Makefile ]) |
48 | + scripts/Makefile scripts/data/Makefile ]) |
49 | AC_CONFIG_HEADERS([config.h]) |
50 | AC_OUTPUT |
51 | |
52 | === modified file 'po/POTFILES.in' |
53 | --- po/POTFILES.in 2013-03-21 11:58:30 +0000 |
54 | +++ po/POTFILES.in 2013-03-22 10:57:19 +0000 |
55 | @@ -27,3 +27,5 @@ |
56 | |
57 | extra/upstart-udev-bridge.c |
58 | extra/upstart-socket-bridge.c |
59 | + |
60 | +scripts/upstart-monitor.py |
61 | |
62 | === modified file 'scripts/Makefile.am' |
63 | --- scripts/Makefile.am 2011-06-01 14:57:41 +0000 |
64 | +++ scripts/Makefile.am 2013-03-22 10:57:19 +0000 |
65 | @@ -1,8 +1,11 @@ |
66 | ## Process this file with automake to produce Makefile.in |
67 | |
68 | +SUBDIRS = data |
69 | + |
70 | EXTRA_DIST = \ |
71 | initctl2dot.py \ |
72 | - init-checkconf.sh |
73 | + init-checkconf.sh \ |
74 | + upstart-monitor.py |
75 | |
76 | dist_man_MANS = \ |
77 | man/initctl2dot.8 \ |
78 | |
79 | === added directory 'scripts/data' |
80 | === added file 'scripts/data/Makefile.am' |
81 | --- scripts/data/Makefile.am 1970-01-01 00:00:00 +0000 |
82 | +++ scripts/data/Makefile.am 2013-03-22 10:57:19 +0000 |
83 | @@ -0,0 +1,24 @@ |
84 | +desktopdir = $(datadir)/applications |
85 | +desktop_DATA = upstart-monitor.desktop |
86 | + |
87 | +icondir = $(datadir)/icons/hicolor/scalable/apps |
88 | +icon = upstart-monitor.svg |
89 | +icon_DATA = $(icon) |
90 | + |
91 | +$(icon): $(top_srcdir)/doc/upstart-logo.svg |
92 | + cp $< $@ |
93 | + |
94 | +EXTRA_DIST = $(desktop_DATA) |
95 | + |
96 | +install-icon: |
97 | + mkdir -p $(DESTDIR)$(pkgdatadir)/icons/ |
98 | + $(INSTALL_DATA) $(icon) $(DESTDIR)$(pkgdatadir)/icons/ |
99 | + |
100 | +uninstall-icon: |
101 | + rm -f $(DESTDIR)$(pkgdatadir)/icons/$(icon) |
102 | + |
103 | +install-data-local: install-icon |
104 | +uninstall-local: uninstall-icon |
105 | + |
106 | +clean-local: |
107 | + rm -f $(icon) |
108 | |
109 | === added file 'scripts/data/upstart-monitor.desktop' |
110 | --- scripts/data/upstart-monitor.desktop 1970-01-01 00:00:00 +0000 |
111 | +++ scripts/data/upstart-monitor.desktop 2013-03-22 10:57:19 +0000 |
112 | @@ -0,0 +1,8 @@ |
113 | +[Desktop Entry] |
114 | +Encoding=UTF-8 |
115 | +Comment=Upstart Event Monitor |
116 | +Name=upstart-monitor |
117 | +Exec=upstart-monitor |
118 | +Icon=upstart-monitor |
119 | +Type=Application |
120 | +Categories=GNOME;GTK;Utility; |
121 | |
122 | === added file 'scripts/man/upstart-monitor.8' |
123 | --- scripts/man/upstart-monitor.8 1970-01-01 00:00:00 +0000 |
124 | +++ scripts/man/upstart-monitor.8 2013-03-22 10:57:19 +0000 |
125 | @@ -0,0 +1,81 @@ |
126 | +.TH upstart\-monitor 8 2013-03-13 upstart |
127 | +.\" |
128 | +.SH NAME |
129 | +upstart\-monitor \- Display system and session Upstart events |
130 | +.\" |
131 | +.SH SYNOPSIS |
132 | +.B upstart\-monitor |
133 | +.RI [ OPTIONS ]... |
134 | +.\" |
135 | +.SH DESCRIPTION |
136 | +.B upstart\-monitor |
137 | +displays Upstart system events or Upstart session events using either a |
138 | +command-line or graphical interface. |
139 | +.\" |
140 | +.SH OPTIONS |
141 | +.\" |
142 | +.TP |
143 | +.BR \-d " \fIdestination\fP" " , " \-\-destination=\fIdestination\fP |
144 | +Specify the endpoint to connect to. Valid destinations are |
145 | +.IR session\-socket ", " |
146 | +.IR session\-bus ", " |
147 | +.IR system\-socket " and " |
148 | +.IR system-bus "." |
149 | +.\" |
150 | +.TP |
151 | +.B \-\-help |
152 | +Show brief usage summary. |
153 | +.\" |
154 | +.TP |
155 | +.BR \-n " , " \-\-no-gui |
156 | +Run in command-line mode. |
157 | +.\" |
158 | +.TP |
159 | +.BR \-s " , " \-\-separator |
160 | +Specify alternate field separator to use for command-line output |
161 | +(default is a single tab character). |
162 | +.\" |
163 | +.SH NOTES |
164 | +.\" |
165 | +.IP \(bu 4 |
166 | +By default, |
167 | +.B upstart\-monitor |
168 | +will attempt to start in a graphical mode unless |
169 | +.B \-\-no\-gui |
170 | +is specified. However, if it is unable to display a graphical interface, |
171 | +it will display a warning message and automatically revert to the |
172 | +command-line interface. |
173 | +.\" |
174 | +.IP \(bu 4 |
175 | +When no command-line option (that affects the destination) is specified, |
176 | +.B upstart\-monitor |
177 | +will attempt to connect to the Session Init (destination |
178 | +\fIsession\-socket\fR) and display session events. In this mode, it |
179 | +will display both session events |
180 | +.I and |
181 | +system events, assuming the |
182 | +.BR upstart-event-bridge (8) |
183 | +is running. |
184 | + |
185 | +If invoked with no arguments from within a non-Upstart session |
186 | +environment it will attempt to connect to Upstart running as process ID |
187 | +1 (destination \fIsystem\-bus\fR) and will only display system events. |
188 | +.\" |
189 | +.SH AUTHOR |
190 | +Written by James Hunt |
191 | +.RB < james.hunt@ubuntu.com > |
192 | +.\" |
193 | +.SH BUGS |
194 | +Report bugs at |
195 | +.RB < https://launchpad.net/ubuntu/+source/upstart/+bugs > |
196 | +.\" |
197 | +.SH COPYRIGHT |
198 | +Copyright \(co 2013 Canonical Ltd. |
199 | +.PP |
200 | +This is free software; see the source for copying conditions. There is NO |
201 | +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
202 | +.SH SEE ALSO |
203 | +.BR init (5) |
204 | +.BR init (8) |
205 | +.BR upstart-events (7) |
206 | +.BR upstart-event-bridge (8) |
207 | |
208 | === added file 'scripts/upstart-monitor.py' |
209 | --- scripts/upstart-monitor.py 1970-01-01 00:00:00 +0000 |
210 | +++ scripts/upstart-monitor.py 2013-03-22 10:57:19 +0000 |
211 | @@ -0,0 +1,687 @@ |
212 | +#!/usr/bin/python3 |
213 | +# -*- coding: utf-8 -*- |
214 | +# vim: set fileencoding=utf-8 |
215 | +#--------------------------------------------------------------------- |
216 | +# Copyright © 2013 Canonical Ltd. |
217 | +# |
218 | +# Author: James Hunt <james.hunt@canonical.com> |
219 | +# |
220 | +# This program is free software; you can redistribute it and/or modify |
221 | +# it under the terms of the GNU General Public License version 2, as |
222 | +# published by the Free Software Foundation. |
223 | +# |
224 | +# This program is distributed in the hope that it will be useful, |
225 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
226 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
227 | +# GNU General Public License for more details. |
228 | +# |
229 | +# You should have received a copy of the GNU General Public License along |
230 | +# with this program; if not, write to the Free Software Foundation, Inc., |
231 | +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
232 | +#--------------------------------------------------------------------- |
233 | +# TODO: |
234 | +# |
235 | +# - misc |
236 | +# - handle_clear_data() default message should use gettext |
237 | +# - gui |
238 | +# - Gtk.STOCK_MEDIA_PLAY toggle |
239 | +# - menus |
240 | +# - Edit->Copy |
241 | +# - Edit->Select |
242 | +# - Edit->Select All |
243 | +# - Edit->Copy All |
244 | +# - Connection |
245 | +# - Choose from 4 connection types. |
246 | +# - View |
247 | +# - Show/Hide index+time columns |
248 | +# - Pause of auto-scroll |
249 | +#--------------------------------------------------------------------- |
250 | + |
251 | +import os |
252 | +import sys |
253 | +import gettext |
254 | +import argparse |
255 | +import signal |
256 | +import dbus |
257 | +import dbus.mainloop.glib |
258 | +from datetime import datetime |
259 | + |
260 | +VERSION = 0.1 |
261 | +COPYRIGHT = 'Copyright © 2013 Canonical Ltd.' |
262 | +DESCRIPTION = 'Simple Upstart Event Monitor' |
263 | +NAME = 'upstart-monitor' |
264 | +AUTHORS = [ |
265 | + 'James Hunt <james.hunt@ubuntu.com>' |
266 | +] |
267 | +WEBSITE = 'http://upstart.ubuntu.com' |
268 | + |
269 | +cli = False |
270 | + |
271 | +try: |
272 | + from gi.repository import Gtk, Gdk, GdkPixbuf, GLib |
273 | +except ImportError: |
274 | + gettext.install(NAME) |
275 | + print("%s: %s" % (_('WARNING'), _('GUI modules not available - falling back to CLI')), file=sys.stderr) |
276 | + cli = True |
277 | + |
278 | +""" |
279 | +Simple command-line and GUI event monitor for Upstart. |
280 | +""" |
281 | + |
282 | +# If this file does not exist or is not readable... |
283 | +LICENSE_FILE = 'aa/usr/share/common-licenses/GPL-2' |
284 | + |
285 | +# ... display this text instead. |
286 | +LICENSE = """ |
287 | +http://www.gnu.org/ |
288 | +This program is free software; you can redistribute it and/or modify |
289 | +it under the terms of the GNU General Public License version 2, as |
290 | +published by the Free Software Foundation. |
291 | + |
292 | +This program is distributed in the hope that it will be useful, |
293 | +but WITHOUT ANY WARRANTY; without even the implied warranty of |
294 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
295 | +GNU General Public License for more details. |
296 | + |
297 | +You should have received a copy of the GNU General Public License along |
298 | +with this program; if not, write to the Free Software Foundation, Inc., |
299 | +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
300 | +""" |
301 | + |
302 | +#------------------------------ |
303 | + |
304 | +DEFAULT_WIN_SIZE_WIDTH = 600 |
305 | +DEFAULT_WIN_SIZE_HEIGHT = 200 |
306 | + |
307 | +DEFAULT_LOGO_SIZE_WIDTH = 150 |
308 | +DEFAULT_LOGO_SIZE_HEIGHT = 150 |
309 | + |
310 | +DEFAULT_OUTPUT_FILE = 'upstart-events.txt' |
311 | + |
312 | +# key=type : value=description |
313 | +destinations = \ |
314 | +{ |
315 | + 'system-bus' : 'D-Bus system bus', |
316 | + 'session-bus' : 'D-Bus session bus', |
317 | + 'system-socket' : 'D-Bus system socket', |
318 | + 'session-socket' : 'D-Bus session socket' |
319 | +} |
320 | + |
321 | +#------------------------------ |
322 | + |
323 | +# well-known address for Upstart running as PID 1 |
324 | +INIT_SOCKET = 'unix:abstract=/com/ubuntu/upstart' |
325 | + |
326 | +# Address of Session Inits private socket |
327 | +SESSION_SOCKET = os.environ.get('UPSTART_SESSION') |
328 | + |
329 | +UI_INFO = """ |
330 | +<ui> |
331 | + <menubar name='MenuBar'> |
332 | + |
333 | + <menu action='FileMenu'> |
334 | + <menuitem action='FileNew'/> |
335 | + <menuitem action='FileSave'/> |
336 | + <menuitem action='FileSaveAs'/> |
337 | + <separator/> |
338 | + <menuitem action='FileQuit'/> |
339 | + </menu> |
340 | + |
341 | +<!-- |
342 | + <menu action='EditMenu'> |
343 | + <menuitem action='EditCopy'/> |
344 | + </menu> |
345 | + |
346 | + <menu action='OptionsMenu'> |
347 | + <menuitem action='OptionsAutoScroll'/> |
348 | + <menu action='ConnectionMenu'> |
349 | + <menuitem action='System Bus'/> |
350 | + <menuitem action='System Socket'/> |
351 | + <menuitem action='Session Bus'/> |
352 | + <menuitem action='Session Socket'/> |
353 | + </menu> |
354 | + </menu> |
355 | + |
356 | +--> |
357 | + <menu action='HelpMenu'> |
358 | + <menuitem action='HelpAbout'/> |
359 | + </menu> |
360 | + |
361 | + </menubar> |
362 | + |
363 | + <popup name='ContextMenu'> |
364 | + <menuitem action='ContextCopy'/> |
365 | + </popup> |
366 | +</ui> |
367 | +""" |
368 | + |
369 | + |
370 | +def format_event(*args): |
371 | + """ |
372 | + Format raw D-Bus event details. |
373 | + |
374 | + Returns: event and quoted event environment tuple. |
375 | + """ |
376 | + quoted_env = [] |
377 | + |
378 | + event = args[0] |
379 | + |
380 | + # unfortunately, quotes are stripped so they need to be re-added |
381 | + # to preserve whitespace in the environment variable values. |
382 | + raw_env = args[1] |
383 | + |
384 | + for e in raw_env: |
385 | + f = e.split('=') |
386 | + quoted_env.append("%s='%s'" % (f[0], ''.join(f[1:]))) |
387 | + |
388 | + env = ' '.join(str(e) for e in quoted_env) |
389 | + |
390 | + return event, env |
391 | + |
392 | + |
393 | +def cmdline_event_handler(*args): |
394 | + """ |
395 | + format event data for command-line display |
396 | + """ |
397 | + global cmdline_args |
398 | + |
399 | + event, env = format_event(*args) |
400 | + now = datetime.now().strftime("%F %T.%f") |
401 | + event_str = "%s %s" % (event, env) if env else event |
402 | + sep = cmdline_args.separator if cmdline_args.separator else "\t" |
403 | + print("%s%s%s" % (now, sep, event_str)) |
404 | + |
405 | + |
406 | +class UpstartEventsGui(Gtk.Window): |
407 | + """ |
408 | + GUI Upstart Event monitor. |
409 | + """ |
410 | + global bus |
411 | + global cmdline_args |
412 | + |
413 | + def add_row(self, event, event_env): |
414 | + """ |
415 | + Add new row to view. |
416 | + """ |
417 | + self.data_index += 1 |
418 | + now = datetime.now().strftime("%F %T.%f") |
419 | + event_str = "%s %s" % (event, event_env) if event_env else event |
420 | + |
421 | + row = [self.data_index, now, event_str] |
422 | + self.liststore.append(row) |
423 | + |
424 | + |
425 | + def event_handler(self, *args): |
426 | + """ |
427 | + Upstart has emitted an event so create a new row in the view. |
428 | + """ |
429 | + event, env = format_event(*args) |
430 | + self.add_row(event, env) |
431 | + |
432 | + # New data arrived since last save (if any). |
433 | + self.need_save = True |
434 | + |
435 | + |
436 | + def register_cb(self): |
437 | + """ |
438 | + Callback to register a D-Bus callback whenever Upstart emits an |
439 | + event. |
440 | + """ |
441 | + # register a D-Bus handler |
442 | + bus.add_signal_receiver(self.event_handler, |
443 | + dbus_interface='com.ubuntu.Upstart0_6', |
444 | + signal_name="EventEmitted") |
445 | + |
446 | + # deregister this callback |
447 | + return False |
448 | + |
449 | + |
450 | + def treeview_changed(self, widget, event, data=None): |
451 | + """ |
452 | + Allow GUI to auto-scroll. |
453 | + """ |
454 | + adj = self.scrolled_window.get_vadjustment() |
455 | + adj.set_value(adj.get_upper() - adj.get_page_size()) |
456 | + |
457 | + |
458 | + def ask_question(self, msg): |
459 | + """ |
460 | + Ask a question. |
461 | + |
462 | + @msg: message to display. |
463 | + |
464 | + Returns: True if user answered positively, else False. |
465 | + """ |
466 | + dialog = Gtk.MessageDialog(self, |
467 | + Gtk.DialogFlags.DESTROY_WITH_PARENT, |
468 | + Gtk.MessageType.QUESTION, |
469 | + Gtk.ButtonsType.YES_NO, |
470 | + msg) |
471 | + response = dialog.run() |
472 | + dialog.destroy() |
473 | + return True if response == Gtk.ResponseType.YES else False |
474 | + |
475 | + |
476 | + def show_dialog(self, msg): |
477 | + """ |
478 | + Display an "ok" dialog which the user must click. |
479 | + |
480 | + @msg: message to display. |
481 | + """ |
482 | + dialog = Gtk.MessageDialog(self, |
483 | + Gtk.DialogFlags.DESTROY_WITH_PARENT, |
484 | + Gtk.MessageType.ERROR, |
485 | + Gtk.ButtonsType.OK, |
486 | + msg) |
487 | + dialog.run() |
488 | + dialog.destroy() |
489 | + |
490 | + |
491 | + def show_about(self): |
492 | + """ |
493 | + Show an About dialog. |
494 | + """ |
495 | + about = Gtk.AboutDialog() |
496 | + about.set_program_name(NAME) |
497 | + about.set_version("%s %s" % (_('Version'), VERSION)) |
498 | + about.set_copyright(COPYRIGHT) |
499 | + about.set_comments(DESCRIPTION) |
500 | + about.set_authors(AUTHORS) |
501 | + about.set_website(WEBSITE) |
502 | + |
503 | + try: |
504 | + with open(LICENSE_FILE, 'r') as f: |
505 | + license = f.read() |
506 | + except IOError: |
507 | + license = LICENSE |
508 | + |
509 | + about.set_license(license) |
510 | + about.set_logo(self.icon_pixbuf) |
511 | + about.run() |
512 | + about.destroy() |
513 | + |
514 | + |
515 | + def on_button_clear_clicked(self, widget): |
516 | + """ |
517 | + Clear cached event data (also clears GUI window). |
518 | + """ |
519 | + if self.handle_clear_data(): |
520 | + self.liststore.clear() |
521 | + self.data_index = 1 |
522 | + self.need_save = False |
523 | + |
524 | + def on_button_pause_clicked(self, widget): |
525 | + """ |
526 | + Toggle auto-scroll. |
527 | + """ |
528 | + if self.auto_scroll: |
529 | + self.treeview.disconnect(self.auto_scroll_handler) |
530 | + self.auto_scroll = False |
531 | + else: |
532 | + self.auto_scroll_handler = \ |
533 | + self.treeview.connect('size-allocate', self.treeview_changed) |
534 | + self.auto_scroll = True |
535 | + |
536 | + |
537 | + def save_data(self, path): |
538 | + """ |
539 | + Write received event data to specified path. |
540 | + """ |
541 | + try: |
542 | + fh = open(path, 'w') |
543 | + except IOError: |
544 | + self.show_dialog('%s: %s' % (_('Error saving file'), path)) |
545 | + |
546 | + for row in self.liststore: |
547 | + fh.write("%d\t%s\t%s\n" % (row[0], row[1], row[2])) |
548 | + fh.close() |
549 | + |
550 | + # All data was written |
551 | + self.need_save = False |
552 | + |
553 | + |
554 | + def handle_save(self, filename=None): |
555 | + """ |
556 | + Display a file chooser dialog to allow user to select a filename to |
557 | + hold the cached event data. |
558 | + """ |
559 | + # no data yet |
560 | + if not len(self.liststore): |
561 | + self.show_dialog(_('No events to save')) |
562 | + return |
563 | + |
564 | + if filename != None: |
565 | + path = filename |
566 | + self.save_data(path) |
567 | + return |
568 | + |
569 | + file_chooser = Gtk.FileChooserDialog(_('Save File'), self.get_toplevel(), |
570 | + Gtk.FileChooserAction.SAVE, |
571 | + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, |
572 | + Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)) |
573 | + |
574 | + file_chooser.set_current_name(DEFAULT_OUTPUT_FILE) |
575 | + |
576 | + response = file_chooser.run() |
577 | + |
578 | + if response != Gtk.ResponseType.ACCEPT: |
579 | + file_chooser.destroy() |
580 | + return |
581 | + |
582 | + path = file_chooser.get_filename() |
583 | + |
584 | + self.save_data(path) |
585 | + file_chooser.destroy() |
586 | + |
587 | + def handle_quit(self): |
588 | + """ |
589 | + Quit application; if unsaved data exists, prompt user. |
590 | + """ |
591 | + |
592 | + if self.handle_clear_data('Quit without saving?'): |
593 | + Gtk.main_quit() |
594 | + |
595 | + def on_button_quit_clicked(self, widget): |
596 | + """ |
597 | + Handle quit request. |
598 | + """ |
599 | + self.handle_quit() |
600 | + |
601 | + |
602 | + def on_button_save_clicked(self, widget): |
603 | + """ |
604 | + Handle save request. |
605 | + """ |
606 | + self.handle_save() |
607 | + |
608 | + # FIXME: default message should use gettext |
609 | + def handle_clear_data(self, message='Clear data without saving?'): |
610 | + """ |
611 | + Handle potentially clearing of cached events. |
612 | + |
613 | + Returns: True if user is happy to clear the data, else False. |
614 | + """ |
615 | + if len(self.liststore) and self.need_save: |
616 | + answer = self.ask_question(message) |
617 | + return answer |
618 | + else: |
619 | + # no data would be lost so allow action |
620 | + return True |
621 | + |
622 | + def activate_action(self, action, user_data=None): |
623 | + """ |
624 | + Main menu-callback-handling method. |
625 | + """ |
626 | + name = action.get_name() |
627 | + |
628 | + if name == 'FileNew': |
629 | + if self.handle_clear_data(): |
630 | + self.liststore.clear() |
631 | + self.data_index = 1 |
632 | + self.need_save = False |
633 | + elif name == 'FileSave': |
634 | + self.handle_save(DEFAULT_OUTPUT_FILE) |
635 | + elif name == 'FileSaveAs': |
636 | + self.handle_save() |
637 | + elif name == 'FileQuit': |
638 | + self.handle_quit() |
639 | + elif name == 'HelpAbout': |
640 | + self.show_about() |
641 | + elif name == 'ContextCopy': |
642 | + # Save event details from cell text to clipboard |
643 | + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) |
644 | + clipboard.set_text(self.context_value, -1) |
645 | + |
646 | + def create_ui_manager(self): |
647 | + """ |
648 | + Errrm... create the UI Manager. |
649 | + """ |
650 | + ui_manager = Gtk.UIManager() |
651 | + |
652 | + # Throws exception if something went wrong |
653 | + ui_manager.add_ui_from_string(UI_INFO) |
654 | + |
655 | + # Add the accelerator group to the toplevel window |
656 | + accelgroup = ui_manager.get_accel_group() |
657 | + self.add_accel_group(accelgroup) |
658 | + return ui_manager |
659 | + |
660 | + def on_button_press_event(self, treeview, event): |
661 | + """ |
662 | + Handle mouse button press events. |
663 | + """ |
664 | + if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3: |
665 | + x = int(event.x) |
666 | + y = int(event.y) |
667 | + path_info = treeview.get_path_at_pos(x, y) |
668 | + if path_info: |
669 | + path, col, cell_x, cell_y = path_info |
670 | + |
671 | + model = treeview.get_model() |
672 | + model_iter = model.get_iter(path) |
673 | + |
674 | + # Save the event details from the selected cell |
675 | + # (event is 3rd column (zero-indexed)). |
676 | + self.context_value = model.get_value (model_iter, 2) |
677 | + |
678 | + treeview.grab_focus() |
679 | + treeview.set_cursor(path, col, 0) |
680 | + |
681 | + # Display context menu |
682 | + self.popup.popup(None, None, None, None, event.button, event.time) |
683 | + return True |
684 | + |
685 | + |
686 | + def __init__(self): |
687 | + """ |
688 | + Setup. |
689 | + """ |
690 | + Gtk.Window.__init__(self, title=DESCRIPTION) |
691 | + |
692 | + main_menu_action_entries = ( |
693 | + ('FileMenu', None, 'File'), |
694 | + ('FileNew', Gtk.STOCK_NEW, 'New', '<control>N', |
695 | + _('Clear events'), self.activate_action), |
696 | + ('FileSave', Gtk.STOCK_SAVE, 'Save', '<control>S', |
697 | + _('Save events to default file'), self.activate_action), |
698 | + ('FileSaveAs', Gtk.STOCK_SAVE_AS, 'Save As', None, |
699 | + _('Save events to specified file'), self.activate_action), |
700 | + ('FileQuit', Gtk.STOCK_QUIT, 'Quit', '<control>Q', |
701 | + _('Exit application'), self.activate_action), |
702 | + |
703 | + ('HelpMenu', None, 'Help'), |
704 | + ('HelpAbout', Gtk.STOCK_HELP, 'About', '<control>A', |
705 | + _('Application details'), self.activate_action), |
706 | + ) |
707 | + |
708 | + context_menu_action_entries = ( |
709 | + ('ContextCopy', Gtk.STOCK_COPY, 'Copy', '<control>C', |
710 | + _('Copy'), self.activate_action), |
711 | + ) |
712 | + |
713 | + self.auto_scroll = True |
714 | + self.need_save = False |
715 | + |
716 | + # value of cell containing event details that user has |
717 | + # right-clicked over |
718 | + self.context_value = None |
719 | + |
720 | + self.data_index = 0 |
721 | + |
722 | + self.set_default_size(DEFAULT_WIN_SIZE_WIDTH, DEFAULT_WIN_SIZE_HEIGHT) |
723 | + self.set_resizable(True) |
724 | + |
725 | + theme = Gtk.IconTheme.get_default() |
726 | + self.icon_pixbuf = theme.load_icon(NAME, 128, Gtk.IconLookupFlags.GENERIC_FALLBACK) |
727 | + self.set_icon(self.icon_pixbuf) |
728 | + |
729 | + action_group = Gtk.ActionGroup('UpstartMonitorActions') |
730 | + action_group.add_actions(main_menu_action_entries) |
731 | + action_group.add_actions(context_menu_action_entries) |
732 | + |
733 | + ui_manager = self.create_ui_manager() |
734 | + ui_manager.insert_action_group(action_group) |
735 | + |
736 | + menubar = ui_manager.get_widget("/MenuBar") |
737 | + |
738 | + # data types we'll be using in each column |
739 | + self.liststore = Gtk.ListStore(int, str, str) |
740 | + |
741 | + GLib.idle_add(self.register_cb) |
742 | + |
743 | + self.box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) |
744 | + |
745 | + self.scrolled_window = Gtk.ScrolledWindow() |
746 | + self.scrolled_window.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS) |
747 | + |
748 | + self.treeview = Gtk.TreeView(model=self.liststore) |
749 | + |
750 | + # columns are zero-based |
751 | + self.treeview.set_search_column(2) |
752 | + |
753 | + self.treeview.set_headers_clickable(True) |
754 | + |
755 | + if self.auto_scroll: |
756 | + self.auto_scroll_handler = \ |
757 | + self.treeview.connect('size-allocate', self.treeview_changed) |
758 | + |
759 | + renderer_text = Gtk.CellRendererText() |
760 | + |
761 | + # XXX: mark column as editable, but do NOT connect the 'edited' |
762 | + # signal. This allows the user to select the text and copy it, |
763 | + # but they cannot modify it. |
764 | + renderer_editabletext = Gtk.CellRendererText() |
765 | + renderer_editabletext.set_property('editable', True) |
766 | + |
767 | + column_index = Gtk.TreeViewColumn(_('Index'), renderer_text, text=0) |
768 | + column_index.set_resizable(True) |
769 | + self.treeview.append_column(column_index) |
770 | + |
771 | + column_time = Gtk.TreeViewColumn(_('Time'), renderer_text, text=1) |
772 | + column_time.set_resizable(True) |
773 | + self.treeview.append_column(column_time) |
774 | + |
775 | + column_event = Gtk.TreeViewColumn(_('Event and environment'), |
776 | + renderer_editabletext, text=2) |
777 | + column_event.set_resizable(True) |
778 | + |
779 | + self.popup = ui_manager.get_widget("/ContextMenu") |
780 | + self.treeview.connect('button-press-event', self.on_button_press_event) |
781 | + |
782 | + self.treeview.append_column(column_event) |
783 | + |
784 | + # stops column titles from scrolling along with the data |
785 | + self.scrolled_window.add(self.treeview) |
786 | + |
787 | + self.button_clear = Gtk.Button(stock=Gtk.STOCK_CLEAR) |
788 | + self.button_clear.connect('clicked', self.on_button_clear_clicked) |
789 | + |
790 | + # FIXME: toggle to Gtk.STOCK_MEDIA_PLAY |
791 | + self.button_pause = Gtk.Button(stock=Gtk.STOCK_MEDIA_PAUSE) |
792 | + self.button_pause.connect('clicked', self.on_button_pause_clicked) |
793 | + |
794 | + self.button_save = Gtk.Button(stock=Gtk.STOCK_SAVE) |
795 | + self.button_save.connect('clicked', self.on_button_save_clicked) |
796 | + |
797 | + self.button_quit = Gtk.Button(stock=Gtk.STOCK_QUIT) |
798 | + self.button_quit.connect('clicked', self.on_button_quit_clicked) |
799 | + |
800 | + self.label_connected = Gtk.Label('%s %s' % (_('Connected to'), |
801 | + destinations[cmdline_args.destination])) |
802 | + self.label_connected.set_justify(Gtk.Justification.RIGHT) |
803 | + |
804 | + self.buttons_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL) |
805 | + self.buttons_box.pack_start(self.button_clear, False, True, 0) |
806 | + self.buttons_box.pack_start(self.button_pause, False, True, 0) |
807 | + self.buttons_box.pack_start(self.button_save, False, True, 0) |
808 | + self.buttons_box.pack_start(self.button_quit, False, True, 0) |
809 | + self.buttons_box.pack_start(self.label_connected, True, True, 0) |
810 | + |
811 | + self.box.pack_start(menubar, False, False, 0) |
812 | + self.box.pack_start(self.scrolled_window, True, True, 0) |
813 | + self.box.pack_end(self.buttons_box, False, False, 0) |
814 | + self.add(self.box) |
815 | + |
816 | + |
817 | + def text_edited(self, widget, path, text): |
818 | + self.liststore[path][1] = text |
819 | + |
820 | + |
821 | +def main(): |
822 | + """ |
823 | + Parse arguments and run either the command-line or the GUI monitor. |
824 | + """ |
825 | + global bus |
826 | + global cmdline_args |
827 | + global cli |
828 | + |
829 | + gettext.install(NAME) |
830 | + |
831 | + parser = argparse.ArgumentParser(description=_('Upstart Event Monitor')) |
832 | + |
833 | + parser.add_argument('-n', '--no-gui', |
834 | + action='store_true', |
835 | + help=_('run in command-line mode')) |
836 | + |
837 | + parser.add_argument('-s', '--separator', |
838 | + help=_('field separator to use for command-line output')) |
839 | + |
840 | + parser.add_argument('-d', '--destination', |
841 | + choices=destinations.keys(), |
842 | + help=_('connect to Upstart via specified D-Bus route')) |
843 | + |
844 | + cmdline_args = parser.parse_args() |
845 | + |
846 | + # allow interrupt |
847 | + signal.signal(signal.SIGINT, signal.SIG_DFL) |
848 | + |
849 | + dbus.set_default_main_loop(dbus.mainloop.glib.DBusGMainLoop()) |
850 | + |
851 | + if not cmdline_args.destination: |
852 | + # If no destination specified, attempt to connect to session |
853 | + # if running under a Session Init, else connect to the system |
854 | + # bus (since this allows even non-priv users to see events). |
855 | + if SESSION_SOCKET: |
856 | + cmdline_args.destination = 'session-socket' |
857 | + else: |
858 | + cmdline_args.destination = 'system-bus' |
859 | + |
860 | + if cmdline_args.destination == 'system-bus': |
861 | + bus = dbus.SystemBus() |
862 | + elif cmdline_args.destination == 'session-bus': |
863 | + bus = dbus.SessionBus() |
864 | + elif cmdline_args.destination == 'system-socket': |
865 | + socket = INIT_SOCKET |
866 | + bus = dbus.connection.Connection(socket) |
867 | + elif cmdline_args.destination == 'session-socket': |
868 | + socket = SESSION_SOCKET |
869 | + bus = dbus.connection.Connection(socket) |
870 | + if cmdline_args.no_gui or not os.environ.get('DISPLAY'): |
871 | + cli = True |
872 | + |
873 | + # dynamically load GUI elements so we can fall back to the |
874 | + # command-line version if we cannot display a GUI |
875 | + if cli == True: |
876 | + # register a D-Bus handler |
877 | + bus.add_signal_receiver(cmdline_event_handler, dbus_interface='com.ubuntu.Upstart0_6', |
878 | + signal_name='EventEmitted') |
879 | + loop = GLib.MainLoop() |
880 | + print('# Upstart Event Monitor (%s)' % _('console mode')) |
881 | + print('#') |
882 | + print('# %s %s' % (_('Connected to'), destinations[cmdline_args.destination])) |
883 | + print('#') |
884 | + print('# %s' % _('Columns: time, event and environment')) |
885 | + print('') |
886 | + loop.run() |
887 | + else: |
888 | + win = UpstartEventsGui() |
889 | + win.connect('delete-event', Gtk.main_quit) |
890 | + win.show_all() |
891 | + Gtk.main() |
892 | + |
893 | + |
894 | +if __name__ == '__main__': |
895 | + # Allow _() to be called from this point onwards |
896 | + gettext.install(NAME) |
897 | + |
898 | + main() |
I notice that the UI is hand-coded in python. Are you concerned about the maintainability of this, vs. using glade and gtkbuilder?
I see that the script is gettext-enabled, but I don't see any updates to po/ that will cause the script to be included in the translations.