Exemple #1
0
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()
Exemple #2
0
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()
Exemple #3
0
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()
Exemple #4
0
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)