From 3673659a2ef602a507619f1448226aa546512d27 Mon Sep 17 00:00:00 2001 From: Wade Brainerd Date: Mon, 12 May 2008 01:35:28 +0000 Subject: Log Viewer overhaul. - Strips ANSI escape codes from logs. - New Edit toolbar with Copy, Word wrap toggle & Search. - Delete Log added to Tools menu. - Notification added after log upload. - Watches multiple directories, plus extra files. - Organizes logs in tree view by directory. - Tree view sorted alphabetically, with special sorting for xxxx-NNN.log files. - Activity version 7 --- (limited to 'logviewer.py') diff --git a/logviewer.py b/logviewer.py index ed5eb1d..8551b19 100644 --- a/logviewer.py +++ b/logviewer.py @@ -20,275 +20,487 @@ import os import logging from gettext import gettext as _ +import re + import gtk import dbus +import pango import pygtk import gobject -import pango import gnomevfs from sugar.activity import activity from sugar import env +from sugar.graphics import iconentry from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toggletoolbutton import ToggleToolButton from sugar.graphics.palette import Palette +from sugar.graphics.alert import NotifyAlert from logcollect import LogCollect, LogSend -class MultiLogView(gtk.VBox): - def __init__(self, path, extra_files): - self._logs_path = path - self._active_log = None - self._extra_files = extra_files +# Should be builtin to sugar.graphics.alert.NotifyAlert... +def _notify_response_cb(notify, response, activity): + activity.remove_alert(notify) - # Creating Main treeview with Actitivities list - self._tv_menu = gtk.TreeView() - self._tv_menu.connect('cursor-changed', self._load_log) - self._tv_menu.set_rules_hint(True) +class MultiLogView(gtk.HBox): + def __init__(self, paths, extra_files): + gtk.HBox.__init__(self, False, 3) + + self.paths = paths + self.extra_files = extra_files - # Set width - box_width = gtk.gdk.screen_width() * 80 / 100 - self._tv_menu.set_size_request(box_width*25/100, 0) - - self._store_menu = gtk.TreeStore(str) - self._tv_menu.set_model(self._store_menu) + self.active_log = None + self.logs = {} + + self._search_text = '' + + self._build_treeview() + self._build_textview() + + self.show_all() + + self._configure_watcher() + self._find_logs() - self._add_column(self._tv_menu, 'Sugar logs', 0) - self._logs = {} + def _build_treeview(self): + self._treeview = gtk.TreeView() - # Activities menu - self.hbox = gtk.HBox(False, 3) - self.hbox.pack_start(self._tv_menu, True, True, 0) + self._treeview.set_rules_hint(True) + self._treeview.connect('cursor-changed', self._cursor_changed_cb) - # Activity log, set width - self._view = LogView() - self._view.set_size_request(box_width*75/100, 0) + self._treemodel = gtk.TreeStore(gobject.TYPE_STRING) - self.hbox.pack_start(self._view, True, True, 0) - self.hbox.show_all() - self._configure_watcher() - self._create_log_view() + sorted = gtk.TreeModelSort(self._treemodel) + sorted.set_sort_column_id(0, gtk.SORT_ASCENDING) + sorted.set_sort_func(0, self._sort_logfile) + self._treeview.set_model(sorted) + + renderer = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Log Files'), renderer, text=0) + self._treeview.append_column(col) + self.path_iter = {} + for p in self.paths: + self.path_iter[p] = self._treemodel.append(None, [p]) + + if len(self.extra_files): + self.extra_iter = self._treemodel.append(None, ['Other']) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(self._treeview) + + scroll.set_size_request(gtk.gdk.screen_width()*30/100, 0) + self.pack_start(scroll, True, True, 0) + + def _build_textview(self): + self._textview = gtk.TextView() + self._textview.set_wrap_mode(gtk.WRAP_NONE) + + pangoFont = pango.FontDescription('Courier 8') + self._textview.modify_font(pangoFont) + + bgcolor = gtk.gdk.color_parse("#FFFFFF") + self._textview.modify_base(gtk.STATE_NORMAL, bgcolor) + + self._textview.set_editable(False) + + self._tagtable = gtk.TextTagTable() + hilite_tag = gtk.TextTag('search-hilite') + hilite_tag.props.background = '#FFFFB0' + self._tagtable.add(hilite_tag) + select_tag = gtk.TextTag('search-select') + select_tag.props.background = '#B0B0FF' + self._tagtable.add(select_tag) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(self._textview) + + scroll.set_size_request(gtk.gdk.screen_width()*70/100, 0) + self.pack_start(scroll, True, True, 0) + + def _sort_logfile(self, treemodel, itera, iterb): + a = treemodel.get_value(itera, 0) + b = treemodel.get_value(iterb, 0) + if a == None or b == None: + return 0 + a = a.lower() + b = b.lower() + + # Filenames are parased as xxxx-YYY.log + # Sort first by xxxx, then numerically by YYY. + logre = re.compile(r'(.*)-(\d+)\.log', re.IGNORECASE) + ma = logre.match(a) + mb = logre.match(b) + if ma and mb: + if ma.group(1) > mb.group(1): return 1 + if ma.group(1) < mb.group(1): return -1 + if int(ma.group(2)) > int(mb.group(2)): return 1 + if int(ma.group(2)) < int(mb.group(2)): return -1 + return 0 + else: + if a > b: return 1 + if a < b: return -1 + return 0 def _configure_watcher(self): # Setting where gnomeVFS will be watching - gnomevfs.monitor_add('file://' + self._logs_path, - gnomevfs.MONITOR_DIRECTORY, - self._log_file_changed_cb) + for p in self.paths: + gnomevfs.monitor_add('file://' + p, + gnomevfs.MONITOR_DIRECTORY, + self._log_file_changed_cb) - for f in self._extra_files: + for f in self.extra_files: gnomevfs.monitor_add('file://' + f, gnomevfs.MONITOR_FILE, self._log_file_changed_cb) def _log_file_changed_cb(self, monitor_uri, info_uri, event): path = info_uri.split('file://')[-1] - filename = self._get_filename_from_path(path) + dir, logfile = os.path.split(path) if event == gnomevfs.MONITOR_EVENT_CHANGED: - self._logs[filename].update() + if self.logs.has_key(logfile): + self.logs[logfile].update() elif event == gnomevfs.MONITOR_EVENT_DELETED: - self._delete_log_file_view(filename) + if self.logs.has_key(logfile): + self._remove_log_file(logfile) elif event == gnomevfs.MONITOR_EVENT_CREATED: self._add_log_file(path) - # Load the log information in View (textview) - def _load_log(self, treeview): - treeselection = treeview.get_selection() - treestore, iter = treeselection.get_selected() - - # Get current selection - act_log = self._store_menu.get_value(iter, 0) - - # Set buffer and scroll down - self._view.textview.set_buffer(self._logs[act_log]) - self._view.textview.scroll_to_mark(self._logs[act_log].get_insert(), 0) - self._active_log = act_log - - def _create_log_view(self): - # Searching log files - for logfile in os.listdir(self._logs_path): - full_log_path = os.path.join(self._logs_path, logfile) - self._add_log_file(full_log_path) + def _cursor_changed_cb(self, treeview): + treestore, iter = self._treeview.get_selection().get_selected() + self._show_log(treestore.get_value(iter, 0)) - for ext in self._extra_files: - self._add_log_file(ext) + def _show_log(self, logfile): + if self.logs.has_key(logfile): + log = self.logs[logfile] + self._textview.set_buffer(log) + self._textview.scroll_to_mark(log.get_insert(), 0) + self.active_log = log - return True + def _find_logs(self): + for path in self.paths: + for logfile in os.listdir(path): + self._add_log_file(os.path.join(path, logfile)) - def _delete_log_file_view(self, logkey): - self._store_menu.remove(self._logs[logkey].iter) - del self._logs[logkey] + for logfile in self.extra_files: + self._add_log_file(logfile) - def _get_filename_from_path(self, path): - return path.split('/')[-1] + self._treeview.expand_all() def _add_log_file(self, path): if os.path.isdir(path): return False if not os.path.exists(path): - print "ERROR: %s don't exists" % path + logging.debug(_("ERROR: File '%s' does not exist.") % path) return False if not os.access(path, os.R_OK): - print "ERROR: I can't read '%s' file" % path + logging.debug(_("ERROR: Unable to read file '%s'.") % path) return False - logfile = self._get_filename_from_path(path) + dir, logfile = os.path.split(path) - if not self._logs.has_key(logfile): - iter = self._add_log_row(logfile) - model = LogBuffer(path, iter) - self._logs[logfile] = model - - self._logs[logfile].update() - written = self._logs[logfile]._written - - # Load the first iter - if self._active_log == None: - self._active_log = logfile - iter = self._tv_menu.get_model().get_iter_root() - self._tv_menu.get_selection().select_iter(iter) - self._load_log(self._tv_menu) - - if written > 0 and self._active_log == logfile: - self._view.textview.scroll_to_mark(self._logs[logfile].get_insert(), 0) - - - def _add_log_row(self, name): - return self._insert_row(self._store_menu, None, name) + if not self.logs.has_key(logfile): + parent = self.extra_iter + if self.path_iter.has_key(dir): + parent = self.path_iter[dir] + iter = self._treemodel.append(parent, [logfile]) + + model = LogBuffer(self._tagtable, path, iter) + self.logs[logfile] = model + + log = self.logs[logfile] + log.update() + written = log._written + + if self.active_log == None: + self.active_log = log + self._treeview.get_selection().select_iter(log.iter) + self._show_log(logfile) + + if written > 0 and self.active_log == log: + self._textview.scroll_to_mark(log.get_insert(), 0) + + def _remove_log_file(self, logfile): + log = self.logs[logfile] + self._treemodel.remove(log.iter) + if self.active_log == log: + self.active_log = None + del self.logs[logfile] + + def set_search_text(self, text): + self._search_text = text - # Add a new column to the main treeview, (code from Memphis) - def _add_column(self, treeview, column_name, index): - cell = gtk.CellRendererText() - col_tv = gtk.TreeViewColumn(column_name, cell, text=index) - col_tv.set_resizable(True) - col_tv.set_property('clickable', True) + buffer = self._textview.get_buffer() - treeview.append_column(col_tv) + start, end = buffer.get_bounds() + buffer.remove_tag_by_name('search-hilite', start, end) + buffer.remove_tag_by_name('search-select', start, end) + buffer.search_sel = None - # Set the last column index added - self.last_col_index = index - - # Insert a Row in our TreeView - def _insert_row(self, store, parent, name): - iter = store.insert_before(parent, None) - index = 0 - store.set_value(iter, index , name) + iter = buffer.get_start_iter() + while True: + next = iter.forward_search(text, 0) + if not next: break + start, end = next + buffer.apply_tag_by_name('search-hilite', start, end) + iter = end + + if self.get_next_result(True): + self.search_next(True) + elif self.get_next_result(False): + self.search_next(False) + + def get_search_text(self): + return self._search_text + + def get_next_result(self, forward, iter=None): + buffer = self._textview.get_buffer() + if not iter: + iter = buffer.get_iter_at_mark(buffer.get_insert()) + if forward: + return iter.forward_search(self._search_text, 0) + else: + return iter.backward_search(self._search_text, 0) + + def search_next(self, forward): + buffer = self._textview.get_buffer() + + if buffer.search_sel is not None: + if forward: + iter = buffer.search_sel[1] + else: + iter = buffer.search_sel[0] + else: + iter = buffer.get_iter_at_mark(buffer.get_insert()) + + next = self.get_next_result(forward, iter) + if next: + if buffer.search_sel is not None: + buffer.remove_tag_by_name( + 'search-select', buffer.search_sel[0], buffer.search_sel[1]) + + start, end = next + buffer.apply_tag_by_name('search-select', start, end) + buffer.search_sel = (start, end) - return iter + buffer.place_cursor(start) + self._textview.scroll_mark_onscreen(buffer.get_insert()) class LogBuffer(gtk.TextBuffer): - def __init__(self, logfile, iter=None): - gtk.TextBuffer.__init__(self) + def __init__(self, tagtable, logfile, iter): + gtk.TextBuffer.__init__(self, tagtable) - self._logfile = logfile + self.logfile = logfile self._pos = 0 self.iter = iter + self.search_sel = None self.update() + def append_formatted_text(self, text): + # Remove ANSI escape codes. + # todo- Handle a subset of them. + strip_ansi = re.compile(r'\033\[[\d;]*m') + text = strip_ansi.sub('', text) + self.insert(self.get_end_iter(), text) + def update(self): try: - f = open(self._logfile, 'r') + f = open(self.logfile, 'r') init_pos = self._pos - + f.seek(self._pos) - self.insert(self.get_end_iter(), f.read()) + self.append_formatted_text(f.read()) self._pos = f.tell() f.close() - + self._written = (self._pos - init_pos) except: - self.insert(self.get_end_iter(), "Console error: can't open the file\n") + self.insert(self.get_end_iter(), _("Error: Can't open file '%s'\n") % self.logfile) self._written = 0 -class LogView(gtk.ScrolledWindow): - def __init__(self): - gtk.ScrolledWindow.__init__(self) - - self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - - self.textview = gtk.TextView() - self.textview.set_wrap_mode(gtk.WRAP_WORD) - - # Set background color - bgcolor = gtk.gdk.color_parse("#FFFFFF") - self.textview.modify_base(gtk.STATE_NORMAL, bgcolor) - - self.textview.set_editable(False) - - self.add(self.textview) - self.textview.show() - - -class LogHandler(activity.Activity): +class LogActivity(activity.Activity): def __init__(self, handle): activity.Activity.__init__(self, handle) - logging.debug('Starting the Log Viewer activity') - self.set_title(_('Log Viewer Activity')) + self.set_title(_('Log Activity')) - # Main path to watch: ~/.sugar/someuser/logs... - main_path = os.path.join(env.get_profile_path(), 'logs') + # Paths to watch: ~/.sugar/someuser/logs, /var/log + paths = [] + paths.append(os.path.join(env.get_profile_path(), 'logs')) + paths.append('/var/log') - # extra files to watch in logviewer + # Additional misc files. ext_files = [] - ext_files.append("/var/log/Xorg.0.log") - ext_files.append("/var/log/syslog") - ext_files.append("/var/log/messages") + ext_files.append("/home/olpc/.bash_history") - self._viewer = MultiLogView(main_path, ext_files).hbox + self.viewer = MultiLogView(paths, ext_files) + self.set_canvas(self.viewer) - self._box = gtk.HBox() - self._box.pack_start(self._viewer) - self._box.show() - - self.set_canvas(self._box) + self._build_toolbox() + + self.show() - # TOOLBAR + def _build_toolbox(self): toolbox = activity.ActivityToolbox(self) - toolbox.show() - - toolbar = LogToolbar(self) - toolbox.add_toolbar(_('Tools'), toolbar) - toolbar.show() + edit_toolbar = activity.EditToolbar() + + edit_toolbar.paste.props.visible = False + edit_toolbar.undo.props.visible = False + edit_toolbar.redo.props.visible = False + edit_toolbar.separator.props.visible = False + edit_toolbar.copy.connect('clicked', self._copy_cb) + + wrap_btn = ToggleToolButton('format-justify-left') + wrap_btn.set_tooltip(_("Word Wrap")) + wrap_btn.connect('clicked', self._wrap_cb) + wrap_btn.show() + edit_toolbar.insert(wrap_btn, -1) + + separator = gtk.SeparatorToolItem() + separator.set_draw(False) + separator.set_expand(True) + edit_toolbar.insert(separator, -1) + separator.show() + + search_item = gtk.ToolItem() + + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._search_entry.add_clear_button() + self._search_entry.connect('activate', self._search_entry_activate_cb) + self._search_entry.connect('changed', self._search_entry_changed_cb) + + width = int(gtk.gdk.screen_width() / 3) + self._search_entry.set_size_request(width, -1) + self._search_entry.show() + search_item.add(self._search_entry) + + search_item.show() + edit_toolbar.insert(search_item, -1) + + self._search_prev = ToolButton('go-previous-paired') + self._search_prev.set_tooltip(_('Previous')) + #self._search_prev.props.sensitive = False + self._search_prev.connect('clicked', self._search_prev_cb) + self._search_prev.show() + edit_toolbar.insert(self._search_prev, -1) + + self._search_next = ToolButton('go-next-paired') + self._search_next.set_tooltip(_('Next')) + #self._search_next.props.sensitive = False + self._search_next.connect('clicked', self._search_next_cb) + self._search_next.show() + edit_toolbar.insert(self._search_next, -1) + + self._update_search_buttons() + + edit_toolbar.show() + toolbox.add_toolbar(_('Edit'), edit_toolbar) + + tools_toolbar = gtk.Toolbar() + + delete_btn = ToolButton('list-remove') + delete_btn.set_tooltip(_("Delete Log File")) + delete_btn.connect('clicked', self._delete_log_cb) + delete_btn.show() + tools_toolbar.insert(delete_btn, -1) + + separator = gtk.SeparatorToolItem() + separator.set_expand(True) + separator.set_draw(False) + separator.show() + tools_toolbar.insert(separator, -1) + + self.collector_palette = CollectorPalette(self) + collector_btn = ToolButton('zoom-best-fit') + collector_btn.set_palette(self.collector_palette) + collector_btn.connect('clicked', self._logviewer_cb) + collector_btn.show() + tools_toolbar.insert(collector_btn, -1) + + tools_toolbar.show() + toolbox.add_toolbar(_('Tools'), tools_toolbar) + + toolbox.show() self.set_toolbox(toolbox) - self.show() - # Dirty hide() + # Hide unsupported Activity tools. toolbar = toolbox.get_activity_toolbar() toolbar.share.hide() toolbar.keep.hide() - - # Keeping this method to add new funcs later - def switch_to_logviewer(self): - self._clean_box() - self._box.pack_start(self._viewer) - -class LogToolbar(gtk.Toolbar): - def __init__(self, handler): - gtk.Toolbar.__init__(self) - self._handler = handler - collector_palette = CollectorMenu() - logviewer = ToolButton('zoom-best-fit') - logviewer.set_palette(collector_palette) - logviewer.connect('clicked', self._on_logviewer_clicked_cb) - self.insert(logviewer, -1) - logviewer.show() + def _copy_cb(self, button): + if self.viewer.active_log: + self.viewer.active_log.copy_clipboard(gtk.clipboard_get()) + + def _wrap_cb(self, button): + if button.get_active(): + self.viewer._textview.set_wrap_mode(gtk.WRAP_WORD_CHAR) + else: + self.viewer._textview.set_wrap_mode(gtk.WRAP_NONE) - def _on_logviewer_clicked_cb(self, widget): - self._handler.switch_to_logviewer() + def _search_entry_activate_cb(self, entry): + self.viewer.set_search_text(entry.props.text) + self._update_search_buttons() -class CollectorMenu(Palette): + def _search_entry_changed_cb(self, entry): + self.viewer.set_search_text(entry.props.text) + self._update_search_buttons() + + def _search_prev_cb(self, button): + self.viewer.search_next(False) + self._update_search_buttons() + + def _search_next_cb(self, button): + self.viewer.search_next(True) + self._update_search_buttons() + + def _update_search_buttons(self,): + text = self.viewer.get_search_text() + if len(text) == 0: + self._search_prev.props.sensitive = False + self._search_next.props.sensitive = False + else: + prev = self.viewer.get_next_result(False) + next = self.viewer.get_next_result(True) + self._search_prev.props.sensitive = len(text) > 0 and prev != None + self._search_next.props.sensitive = len(text) > 0 and next != None + + def _delete_log_cb(self, widget): + if self.viewer.active_log: + logfile = self.viewer.active_log.logfile + try: + os.remove(logfile) + except OSError, err: + notify = NotifyAlert() + notify.props.title = 'Error' + notify.props.msg = err.strerror + _(' when deleting ') + logfile + notify.connect('response', _notify_response_cb, self) + self.add_alert(notify) + + def _logviewer_cb(self, widget): + self.collector_palette.popup(True) + +class CollectorPalette(Palette): _DEFAULT_SERVER = 'http://olpc.scheffers.net/olpc/submit.tcl' - def __init__(self): - Palette.__init__(self, 'Log Collector: send XO information') + def __init__(self, handler): + Palette.__init__(self, 'Log Collector: Send XO information') + self._handler = handler + self._collector = LogCollect() - label = gtk.Label(_('Log collector allow to send information about\n\ -the system and running process to a central\nserver, use this option if you \ -want to report\nsome detected problem')) + + label = gtk.Label( + _('Log collector sends information about the system\n'\ + 'and running processes to a central server. Use\n'\ + 'this option if you want to report a problem.')) send_button = gtk.Button(_('Send information')) send_button.connect('clicked', self._on_send_button_clicked_cb) @@ -301,14 +513,30 @@ want to report\nsome detected problem')) self.set_content(vbox) def _on_send_button_clicked_cb(self, button): - # Using the default values, just for testing... - data = self._collector.write_logs() - sender = LogSend() - - if sender.http_post_logs(self._DEFAULT_SERVER, data): - print "Logs sent...OK" - else: - print "FAILED to send logs" + success = True + try: + data = self._collector.write_logs() + sender = LogSend() + success = sender.http_post_logs(self._DEFAULT_SERVER, data) + except: + success = False os.remove(data) - self.popdown() + self.popdown(True) + + title = '' + msg = '' + if success: + title = _('Logs sent') + msg = _('The logs were uploaded to the server.') + else: + title = _('Logs not sent') + msg = _('The logs could not be uploaded to the server. '\ + 'Please check your network connection.') + + notify = NotifyAlert() + notify.props.title = title + notify.props.msg = msg + notify.connect('response', _notify_response_cb, self._handler) + self._handler.add_alert(notify) + -- cgit v0.9.1