class Plotter(gtk.DrawingArea): """A widget that contains multiple color-coded plots.""" config = [# label, regexp, color ('in', ' <= ', 'yellow'), ('out', ' => ', 'red'), ('local', ' => .+ R=local', 'blue'), ('smtp', ' => .+ T=[^ ]*smtp', 'green'), ('queue', None, 'white'), ] def __init__(self, logwatcher, queue_mgr, prefs): gtk.DrawingArea.__init__(self) self._logwatcher = logwatcher self._queue_mgr = queue_mgr self.set_size_request(-1, prefs.plot_area_height) self.visible = True self.interval = prefs.plotting_interval prefs.subscribe(self.apply_prefs) self.pangolayout = self.create_pango_layout("") self.gc = None # can't create as the window isn't realized yet colormap = self.get_colormap() self.background_color = colormap.alloc_color('black') self.guide_colors = [colormap.alloc_color('#AAAAAA'), colormap.alloc_color('#555555'), colormap.alloc_color('#222222')] self.plots = [] for label, regex, color in self.config: compiled_regex = regex and re.compile(regex) or None mapped_color = colormap.alloc_color(color) history = [] self.plots.append((label, compiled_regex, mapped_color, history)) self.connect('expose-event', self._redraw) self.timer = Timer(self.interval, self.update) def _redraw(self, area, event): """Redraw the plots. Must not be invoked directly, because double-buffering won't work; queue_redraw() should be used instead. """ geometry = self.window.get_geometry() width, height = geometry[2], geometry[3] if not self.gc: self.gc = self.window.new_gc() # clear drawing area self.gc.set_foreground(self.background_color) self.window.draw_rectangle(self.gc, True, 0, 0, width, height) start_x = 18 point_count = width - start_x plot_height = height - 20 label_offset = start_x + 10 self._draw_guides(width, plot_height, start_x) for label, compiled_regex, mapped_color, history in self.plots: scale = self.get_scale(history) self.gc.set_foreground(mapped_color) if len(history) > 1: # transform data points to screen coordinates points = enumerate(history[-point_count:]) coord_list = [(start_x + index, int(plot_height * (1 - value*scale))) for index, value in points] self.window.draw_lines(self.gc, coord_list) # draw label if label_offset < width: suffix = ": %.1f" % (history and history[-1] or 0) if abs(scale - 0.1) > DELTA: suffix = (" (%dx)" % (0.1/scale)) + suffix self.pangolayout.set_text(label + suffix) self.window.draw_layout(self.gc, label_offset, plot_height, self.pangolayout) label_offset += len(label + suffix) * 9 # XXX text width return True def _draw_guides(self, width, plot_height, start_x): """Draw guide lines.""" # draw main boundaries self.gc.set_foreground(self.guide_colors[0]) self.window.draw_line(self.gc, start_x, 0, width, 0) self.window.draw_line(self.gc, start_x, plot_height, width, plot_height) # draw numbers # XXX hardcoding font sizes number_offset = 0 self.pangolayout.set_text("10") self.window.draw_layout(self.gc, number_offset, -2, self.pangolayout) self.pangolayout.set_text(" 5") self.window.draw_layout(self.gc, number_offset, plot_height / 2 - 8, self.pangolayout) self.pangolayout.set_text(" 0") self.window.draw_layout(self.gc, number_offset, plot_height - 16, self.pangolayout) # draw a nice line indicating 5 self.gc.set_foreground(self.guide_colors[1]) self.window.draw_line(self.gc, start_x, plot_height / 2, width, plot_height / 2) # draw minor lines self.gc.set_foreground(self.guide_colors[2]) for v in [1, 2, 3, 4, 6, 7, 8, 9]: y = int(plot_height * (v / 10.0)) self.window.draw_line(self.gc, start_x, y, width, y) def get_scale(self, list): """Calculate a scaling value for a list of floats. Returns a floating point value - a multiplier to normalize the data. """ if len(list) < 2 or max(list) < 10: return 0.1 largest = max(list) scale = 1.0 while largest*scale > 1: scale /= 10 return scale def update(self, *ignored_arguments): """Update plot data.""" new_loglines = self._logwatcher.get_for_processing() for label, compiled_regex, mapped_color, history in self.plots: if label == 'queue': norm_count = float(self._queue_mgr.queue_length) else: count = self.count_matches(compiled_regex, new_loglines) norm_count = count * (1000.0 / self.interval) # normalize history.append(norm_count) if len(history) > KEEP_HISTORY: history.pop(0) self.queue_draw() def count_matches(self, regex, lines): count = 0 for line in lines: if regex.search(line): count += 1 return count def apply_prefs(self, prefs): self.set_size_request(-1, prefs.plot_area_height) self.interval = prefs.plotting_interval self.timer.update_interval(self.interval) if prefs.show_plotter != self.visible: self.set_visible(prefs.show_plotter) self.queue_draw() def set_visible(self, visible): self.visible = visible parent = self.get_parent() # hide the frame as well if visible: parent.show_all() else: parent.hide()
class LogWidget(WrappedTextView): """A widget that displays the tail of the exim main log.""" MAX_LOG_LINES = 10000 # maximum lines to have in the buffer at a time def __init__(self, logwatcher, prefs): WrappedTextView.__init__(self) self._track_log = prefs.track_log prefs.subscribe(self.apply_prefs) self._logwatcher = logwatcher self.buffer = self.get_buffer() self.buffer.create_tag('monospace', family='Monospace') self.buffer.create_tag('time', foreground='purple') self.buffer.create_tag('message_id', foreground='blue') self.buffer.create_tag('info', foreground='black') self.buffer.create_mark('end', self.buffer.get_end_iter(), False) self.buffer.insert_with_tags_by_name(self.buffer.get_start_iter(), _("geximon started at %s") % datetime.datetime.now(), 'monospace', 'info') self.timer = Timer(prefs.log_interval, self.update) def update(self): self._logwatcher.update() unseen = self._logwatcher.get_unseen() # remove the date (like eximon) unseen = \ map(lambda s: s[s.find(' ')+1:], unseen) # show the new data for line in unseen: # check for time signature if len(line) > 9 and line[2] == ':' and line[5] == ':': self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), "\n" + line[:9], 'monospace', 'time') else: # no time signature, print everything and continue self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), "\n" + line, 'monospace') continue # check for message id if len(line) > 25 and line[15] == '-' and line[22] == '-': self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), line[9:25], 'monospace', 'message_id') self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), line[25:], 'monospace', 'info') else: # no message id, print everything and continue self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), line[9:], 'monospace', 'info') if unseen: # if there was new data # discard old data if there's too much of it line_count = self.buffer.get_line_count() if line_count > self.MAX_LOG_LINES: start = self.buffer.get_start_iter() middle = self.buffer.get_iter_at_line_index( int(line_count - self.MAX_LOG_LINES*0.8), 0) self.buffer.delete(start, middle) if self._track_log: self.scroll_to_mark(self.buffer.get_mark('end'), 0.0) def apply_prefs(self, prefs): self._track_log = prefs.track_log self.set_wrap_mode(prefs.wrap_log) self.timer.update_interval(prefs.log_interval) self._logwatcher.use_sudo = prefs.use_sudo self._logwatcher.use_ssh = prefs.use_ssh self._logwatcher.hostname = prefs.hostname if (self._logwatcher.log_dir != prefs.log_dir or self._logwatcher.mainlog_name != prefs.mainlog_name): self._logwatcher._valid = True self._logwatcher.open(prefs.log_dir, prefs.mainlog_name) self.update()
class QueueWidget(gtk.TreeView): """A widget that displays the exim message queue.""" def __init__(self, main_win, logwatcher, queue_mgr, prefs): self._main_win = main_win # needed for popups self._statusbar = main_win.statusbar self._old_queue = {} self.queue_mgr = queue_mgr self.queue_mgr.callback = self.do_update self.logwatcher = logwatcher self.model = gtk.ListStore(gobject.TYPE_STRING, # 0 color gobject.TYPE_STRING, # 1 message id gobject.TYPE_STRING, # 2 sender gobject.TYPE_STRING, # 3 size gobject.TYPE_STRING, # 4 time in queue gobject.TYPE_STRING) # 5 recipients # GTK's recent addition, 'fixed_height_mode' would be really useful # to speed things up, however, it is not yet supported by pyGTK renderer = gtk.CellRendererText() renderer.set_property('family', 'Monospace') id_column = gtk.TreeViewColumn(_("Message ID"), renderer, text=1) id_column.add_attribute(renderer, 'foreground', 0) gtk.TreeView.__init__(self, self.model) self.connect('button-press-event', self.click) self.connect('popup-menu', self.popupMenu) self.total_str = "" self.selected_str = "" self.get_selection().connect('changed', self.selectionChanged) renderer = gtk.CellRendererText() columns = ([id_column] + [gtk.TreeViewColumn(title, renderer, text=source) for source, title in [(2, _("Sender")), (3, _("Size")), (4, _("Time")), (5, _("Recipients"))]]) for index, column in enumerate(columns): column.set_reorderable(True) column.set_resizable(True) column.set_sort_column_id(index + 1) # not very neat, but it works self.append_column(column) self._setUpSorting() self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.set_headers_clickable(True) self.set_rules_hint(True) self._initializing = True self.confirm_actions = prefs.confirm_actions self.report_success = prefs.report_success prefs.subscribe(self.apply_prefs) self.timer = Timer(prefs.queue_interval, self.update) def _setUpSorting(self): """Set up correct sorting by size and time.""" def sort_func(treemodel, iter1, iter2, data): col_number, coef = data val1 = treemodel.get_value(iter1, col_number) val2 = treemodel.get_value(iter2, col_number) def eval_suffix(s, coef): if s is None or len(s) == 0: return 0 suffix = s[-1] if suffix in coef: return float(s[:-1]) * coef[suffix] else: return float(s) result = cmp(eval_suffix(val1, coef), eval_suffix(val2, coef)) return result self.model.set_sort_func(3, sort_func, (3, {'K': 1024, 'M': 1048576})) self.model.set_sort_func(4, sort_func, (4, {'m': 1, 'h': 60, 'd': 24*60})) def update(self): """Schedule an immediate update of the queue list.""" self.queue_mgr.schedule_update() def do_update(self, queue): """Update the process list. Called from a background thread. """ # XXX this method is too long and needs to be split up gtk.gdk.threads_enter() if self._initializing: # the first update tends to be massive, so it is worth unbinding # the model from the view temporarily for performance reasons self.set_model(None) self.model.set_sort_column_id(0, gtk.SORT_ASCENDING) old_queue = self._old_queue # remove messages no longer in the queue from display iter = self.model.get_iter_first() while iter is not None: next = self.model.iter_next(iter) id = self.model.get_value(iter, 1) if id not in queue: self.model.remove(iter) elif queue[id] != old_queue[id]: msg = queue[id] self.model.set(iter, 0, msg.frozen and "#FF0000" or "#000000", 1, msg.id, 2, msg.sender, 3, msg.size, 4, msg.time, 5, " ".join(msg.recipients)) iter = next gtk.gdk.threads_leave() # it is safe to do this now because the obsolete messages have been # removed and only new ones will be added self._old_queue = queue # find all messages which should be added to the model new_rows = [] for id in queue: if id not in old_queue: msg = queue[id] row = (msg.frozen and "#FF0000" or "#000000", msg.id, msg.sender, msg.size, msg.time, " ".join(msg.recipients)) new_rows.append(row) # reflect that the list is being updated in the statusbar; # only bother if there are many new messages worth_bothering = len(queue) > 100 and len(new_rows) > 10 if self._initializing or worth_bothering: self.total_str = (_("Updating message list...")) gtk.gdk.threads_enter() self.updateStatusbar() gtk.gdk.threads_leave() if self._initializing: # the model is unbound so there is no need to call threads_enter() # and threads_leave() in every iteration; once is enough gtk.gdk.threads_enter() for row in new_rows: self.model.append(row) gtk.gdk.threads_leave() else: # the model is bound, so we need to do things the slow way # temporarily disabling sorting helps quite a bit if worth_bothering: gtk.gdk.threads_enter() # I HATE THESE! sort_mode = self.model.get_sort_column_id() self.model.set_sort_column_id(0, gtk.SORT_ASCENDING) gtk.gdk.threads_leave() for row in new_rows: gtk.gdk.threads_enter() self.model.append(row) gtk.gdk.threads_leave() if worth_bothering: gtk.gdk.threads_enter() self.model.set_sort_column_id(*sort_mode) gtk.gdk.threads_leave() # update the statusbar data msg_word = len(queue) > 1 and _("messages") or _("message") frozen = len(filter(lambda id: queue[id].frozen, queue)) self.total_str = (_("%d %s in queue (%d frozen).") % (len(queue), msg_word, frozen)) gtk.gdk.threads_enter() self.updateStatusbar() if self._initializing: self.set_model(self.model) # rebind the model self.model.set_sort_column_id(1, gtk.SORT_ASCENDING) self._initializing = False gtk.gdk.threads_leave() def click(self, widget, event): """Handle a click in the queue widget.""" if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: self.popupMenu(widget, event.button) return True # handled else: return False # not handled def popupMenu(self, widget, button=0): """Pop up the context menu.""" menu = QueueContextMenu(self.get_selection(), self._main_win, self) menu.show_all() menu.popup(None, None, None, button, gtk.get_current_event_time()) def selectionChanged(self, selection): """Update statusbar on selection change.""" messages = [0] def callback(model, path, iter): messages[0] += 1 selection.selected_foreach(callback) messages = messages[0] # this could be done with a single line: # messages = selection.count_selected_rows() # but I think the current way is more compatible # with older versions of pygtk if messages: msg_word = messages > 1 and _("messages") or _("message") self.selected_str = _("%d %s selected.") % (messages, msg_word) else: self.selected_str = "" self.updateStatusbar() def updateStatusbar(self): """Update the statusbar.""" # XXX this gets called from multiple threads, locking would be nice self._statusbar.pop(0) self._statusbar.push(0, self.total_str + ' ' + self.selected_str) def cleanup(self): """Clean up when the widget is destroyed.""" self.queue_mgr.stop() def apply_prefs(self, prefs): self.queue_mgr.bin_dir = prefs.bin_dir self.queue_mgr.use_sudo = prefs.use_sudo self.queue_mgr.use_ssh = prefs.use_ssh self.queue_mgr.hostname = prefs.hostname self.confirm_actions = prefs.confirm_actions self.report_success = prefs.report_success self.timer.update_interval(prefs.queue_interval) self.update()
class ProcessWidget(gtk.TreeView): """A widget that displays a list of processes.""" def __init__(self, statusbar, prefs): self._statusbar = statusbar self._old_processes = {} self.process_mgr = ProcessManager(self.do_update, prefs.bin_dir, prefs.use_sudo, prefs.use_ssh, prefs.hostname) self.model = gtk.ListStore(gobject.TYPE_INT, # 0 pid gobject.TYPE_STRING) # 1 status gtk.TreeView.__init__(self, self.model) self.connect('button-press-event', self.click) self.connect('popup-menu', self.popupMenu) renderer = gtk.CellRendererText() for index, title in enumerate(["PID", "Status"]): column = gtk.TreeViewColumn(title, renderer, text=index) column.set_reorderable(True) column.set_resizable(True) column.set_sort_column_id(index) self.append_column(column) self.get_column(0).clicked() # sort by pid self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.set_headers_clickable(True) prefs.subscribe(self.apply_prefs) self.timer = Timer(prefs.process_interval, self.update) def update(self, new_status="Running exiwhat..."): """Schedule an immediate update of process status.""" self._statusbar.pop(0) self._statusbar.push(0, new_status) self.process_mgr.schedule_update() def do_update(self, processes, info): """Update the process list. Called from a background thread. """ gtk.gdk.threads_enter() try: old_processes = self._old_processes # remove outdated entries iter = self.model.get_iter_first() while iter is not None: next = self.model.iter_next(iter) pid = self.model.get_value(iter, 0) if (pid not in processes or old_processes[pid] != processes[pid]): self.model.remove(iter) iter = next # add new and changed entries to list for pid, status in processes.iteritems(): if pid not in old_processes or old_processes[pid] != status: self.model.append((pid, status)) self._old_processes = processes self._statusbar.pop(0) self._statusbar.push(0, info) finally: gtk.gdk.threads_leave() def click(self, widget, event): """Handle a click in the process widget.""" if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: self.popupMenu(widget, event.button) return True # handled else: return False # not handled def popupMenu(self, widget, button=0): """Pop up the context menu.""" menu = ProcessContextMenu(self.get_selection(), self) menu.show_all() menu.popup(None, None, None, button, gtk.get_current_event_time()) def cleanup(self): """Clean up when the widget is destroyed.""" self.process_mgr.stop() def apply_prefs(self, prefs): self.process_mgr.bin_dir = prefs.bin_dir self.process_mgr.use_sudo = prefs.use_sudo self.process_mgr.use_ssh = prefs.use_ssh self.process_mgr.hostname = prefs.hostname self.timer.set_paused(not prefs.show_process_list) self.timer.update_interval(prefs.process_interval)