sugar-logviewer-activity-6.orig/0000755000175000017500000000000010724107313015556 5ustar janijanisugar-logviewer-activity-6.orig/Log.activity/0000755000175000017500000000000010724107313020132 5ustar janijanisugar-logviewer-activity-6.orig/Log.activity/NEWS0000644000175000017500000000056510724055262020644 0ustar janijani5 * Harden logcollect.py with a whole bunch of try...except to always collect as much as possible. (pascal) 4 * logcollect.py now defaults to http://olpc.scheffers.net/olpc/submit.tcl * Fix command-line mode and cleanup 'usage' after rename from log-collect.py * Default text font size 3 * Read check permission 2 * Drop presence and network 1 * Initial Version sugar-logviewer-activity-6.orig/Log.activity/README0000644000175000017500000000000010715326074021007 0ustar janijanisugar-logviewer-activity-6.orig/Log.activity/logviewer.py0000644000175000017500000002352010723523264022517 0ustar janijani#!/usr/bin/env python # Copyright (C) 2006-2007, Eduardo Silva # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os import logging from gettext import gettext as _ import gtk import dbus import pygtk import gobject import pango import gnomevfs from sugar.activity import activity from sugar import env from sugar.graphics.toolbutton import ToolButton from sugar.graphics.palette import Palette 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 # 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) # 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._add_column(self._tv_menu, 'Sugar logs', 0) self._logs = {} # Activities menu self.hbox = gtk.HBox(False, 3) self.hbox.pack_start(self._tv_menu, True, True, 0) # Activity log, set width self._view = LogView() self._view.set_size_request(box_width*75/100, 0) self.hbox.pack_start(self._view, True, True, 0) self.hbox.show_all() self._configure_watcher() self._create_log_view() 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 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) if event == gnomevfs.MONITOR_EVENT_CHANGED: self._logs[filename].update() elif event == gnomevfs.MONITOR_EVENT_DELETED: self._delete_log_file_view(filename) 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) for ext in self._extra_files: self._add_log_file(ext) return True def _delete_log_file_view(self, logkey): self._store_menu.remove(self._logs[logkey].iter) del self._logs[logkey] def _get_filename_from_path(self, path): return path.split('/')[-1] 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 return False if not os.access(path, os.R_OK): print "ERROR: I can't read '%s' file" % path return False logfile = self._get_filename_from_path(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) # 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) treeview.append_column(col_tv) # 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) return iter class LogBuffer(gtk.TextBuffer): def __init__(self, logfile, iter=None): gtk.TextBuffer.__init__(self) self._logfile = logfile self._pos = 0 self.iter = iter self.update() def update(self): try: f = open(self._logfile, 'r') init_pos = self._pos f.seek(self._pos) self.insert(self.get_end_iter(), 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._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): def __init__(self, handle): activity.Activity.__init__(self, handle) logging.debug('Starting the Log Viewer activity') self.set_title(_('Log Viewer Activity')) # Main path to watch: ~/.sugar/someuser/logs... main_path = os.path.join(env.get_profile_path(), 'logs') # extra files to watch in logviewer ext_files = [] ext_files.append("/var/log/Xorg.0.log") ext_files.append("/var/log/syslog") ext_files.append("/var/log/messages") self._viewer = MultiLogView(main_path, ext_files).hbox self._box = gtk.HBox() self._box.pack_start(self._viewer) self._box.show() self.set_canvas(self._box) # TOOLBAR toolbox = activity.ActivityToolbox(self) toolbox.show() toolbar = LogToolbar(self) toolbox.add_toolbar(_('Tools'), toolbar) toolbar.show() self.set_toolbox(toolbox) self.show() # Dirty hide() 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 _on_logviewer_clicked_cb(self, widget): self._handler.switch_to_logviewer() class CollectorMenu(Palette): _DEFAULT_SERVER = 'http://olpc.scheffers.net/olpc/submit.tcl' def __init__(self): Palette.__init__(self, 'Log Collector: send XO information') 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')) send_button = gtk.Button(_('Send information')) send_button.connect('clicked', self._on_send_button_clicked_cb) vbox = gtk.VBox(False, 5) vbox.pack_start(label) vbox.pack_start(send_button) vbox.show_all() 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" os.remove(data) self.popdown() sugar-logviewer-activity-6.orig/Log.activity/activity/0000755000175000017500000000000010724107313021766 5ustar janijanisugar-logviewer-activity-6.orig/Log.activity/activity/activity.info0000644000175000017500000000023110724055262024500 0ustar janijani[Activity] name = Log Viewer activity_version = 6 service_name = org.laptop.LogViewer exec = sugar-activity logviewer.LogHandler -s icon = activity-log sugar-logviewer-activity-6.orig/Log.activity/activity/activity-log.svg0000644000175000017500000000234110715326074025131 0ustar janijani ]> sugar-logviewer-activity-6.orig/Log.activity/logcollect.py0000644000175000017500000004420410715326074022646 0ustar janijani#!/usr/bin/env python # # Copyright (C) 2007, Pascal Scheffers # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # log-collect for OLPC # # Compile a report containing: # * Basic system information: # ** Serial number # ** Battery type # ** Build number # ** Uptime # ** disk free space # ** ... # * Installed packages list # * All relevant log files (all of them, at first) # # The report is output as a tarfile # # This file has two modes: # 1. It is a stand-alone python script, when invoked as 'log-collect' # 2. It is a python module. import os import zipfile import glob import sys import time # The next couple are used by LogSend import httplib import mimetypes import urlparse class MachineProperties: """Various machine properties in easy to access chunks. """ def __read_file(self, filename): """Read the entire contents of a file and return it as a string""" data = '' f = open(filename) try: data = f.read() finally: f.close() return data def olpc_build(self): """Buildnumber, from /etc/issue""" # Is there a better place to get the build number? if not os.path.exists('/etc/issue'): return '#/etc/issue not found' # Needed, because we want to default to the first non blank line: first_line = '' for line in self.__read_file('/etc/issue').splitlines(): if line.lower().find('olpc build') > -1: return line if first_line == '': first_line=line return first_line def uptime(self): for line in self.__read_file('/proc/uptime').splitlines(): if line != '': return line return '' def loadavg(self): for line in self.__read_file('/proc/loadavg').splitlines(): if line != '': return line return '' def kernel_version(self): for line in self.__read_file('/proc/version').splitlines(): if line != '': return line return '' def memfree(self): line = '' for line in self.__read_file('/proc/meminfo').splitlines(): if line.find('MemFree:') > -1: return line[8:].strip() def _mfg_data(self, item): """Return mfg data item from /ofw/mfg-data/""" if not os.path.exists('/ofw/mfg-data/'+item): return '' v = self.__read_file('/ofw/mfg-data/'+item) # Remove trailing 0 character, if any: if v != '' and ord(v[len(v)-1]) == 0: v = v[:len(v)-1] return v def laptop_serial_number(self): return self._mfg_data('SN') def laptop_motherboard_number(self): return self._mfg_data('B#') def laptop_board_revision(self): s = self._mfg_data('SG')[0:1] if s == '': return '' return '%02X' % ord(self._mfg_data('SG')[0:1]) def laptop_uuid(self): return self._mfg_data('U#') def laptop_keyboard(self): kb = self._mfg_data('KM') + '-' kb += self._mfg_data('KL') + '-' kb += self._mfg_data('KV') return kb def laptop_wireless_mac(self): return self._mfg_data('WM') def laptop_bios_version(self): return self._mfg_data('BV') def laptop_country(self): return self._mfg_data('LA') def laptop_localization(self): return self._mfg_data('LO') def _battery_info(self, item): """ from /sys/class/power-supply/olpc-battery/ """ root = '/sys/class/power_supply/olpc-battery/' if not os.path.exists(root+item): return '' return self.__read_file(root+item).strip() def battery_serial_number(self): return self._battery_info('serial_number') def battery_capacity(self): return self._battery_info('capacity') + ' ' + \ self._battery_info('capacity_level') def battery_info(self): #Should be just: #return self._battery_info('uevent') #But because of a bug in the kernel, that has trash, lets filter: bi = '' for line in self._battery_info('uevent').splitlines(): if line.startswith('POWER_'): bi += line + '\n' return bi def disksize(self, path): return os.statvfs(path).f_bsize * os.statvfs(path).f_blocks def diskfree(self, path): return os.statvfs(path).f_bsize * os.statvfs(path).f_bavail def _read_popen(self, cmd): p = os.popen(cmd) s = '' try: for line in p: s += line finally: p.close() return s def ifconfig(self): return self._read_popen('/sbin/ifconfig') def route_n(self): return self._read_popen('/sbin/route -n') def df_a(self): return self._read_popen('/bin/df -a') def ps_auxfwww(self): return self._read_popen('/bin/ps auxfwww') def usr_bin_free(self): return self._read_popen('/usr/bin/free') def top(self): return self._read_popen('/usr/bin/top -bn2') def installed_activities(self): s = '' for path in glob.glob('/usr/share/activities/*.activity'): s += os.path.basename(path) + '\n' for path in glob.glob('/home/olpc/Activities/*'): s += '~' + os.path.basename(path) + '\n' return s class LogCollect: """Collect XO logfiles and machine metadata for reporting to OLPC """ def __init__(self): self._mp = MachineProperties() def write_logs(self, archive='', logbytes=15360): """Write a zipfile containing the tails of the logfiles and machine info of the XO Arguments: archive - Specifies the location where to store the data defaults to /dev/shm/logs-.zip logbytes - Maximum number of bytes to read from each log file. 0 means complete logfiles, not just the tail -1 means only save machine info, no logs """ #This function is crammed with try...except to make sure we get as much #data as possible, if anything fails. if archive=='': archive = '/dev/shm/logs.zip' try: #With serial number is more convenient, but might fail for some #Unknown reason... archive = '/dev/shm/logs-%s.zip' % self._mp.laptop_serial_number() except Exception: pass z = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) try: try: z.writestr('info.txt', self.laptop_info()) except Exception, e: z.writestr('info.txt', "logcollect: could not add info.txt: %s" % e) if logbytes > -1: # Include some log files from /var/log. for fn in ['dmesg', 'messages', 'cron', 'maillog','rpmpkgs', 'Xorg.0.log', 'spooler']: try: if os.access('/var/log/'+fn, os.F_OK): if logbytes == 0: z.write('/var/log/'+fn, 'var-log/'+fn) else: z.writestr('var-log/'+fn, self.file_tail('/var/log/'+fn, logbytes)) except Exception, e: z.writestr('var-log/'+fn, "logcollect: could not add %s: %s" % (fn, e)) # Include all current ones from sugar/logs for path in glob.glob('/home/olpc/.sugar/default/logs/*.log'): try: if os.access(path, os.F_OK): if logbytes == 0: z.write(path, 'sugar-logs/'+os.path.basename(path)) else: z.writestr('sugar-logs/'+os.path.basename(path), self.file_tail(path, logbytes)) except Exception, e: z.writestr('sugar-logs/'+fn, "logcollect: could not add %s: %s" % (fn, e)) try: z.write('/etc/resolv.conf') except Exception, e: z.writestr('/etc/resolv.conf', "logcollect: could not add resolv.conf: %s" % e) except Exception, e: print 'While creating zip archive: %s' % e z.close() return archive def file_tail(self, filename, tailbytes): """Read the tail (end) of the file Arguments: filename The name of the file to read tailbytes Number of bytes to include or 0 for entire file """ data = '' f = open(filename) try: fsize = os.stat(filename).st_size if tailbytes > 0 and fsize > tailbytes: f.seek(-tailbytes, 2) data = f.read() finally: f.close() return data def make_report(self, target='stdout'): """Create the report Arguments: target - where to save the logs, a path or stdout """ li = self.laptop_info() for k, v in li.iteritems(): print k + ': ' +v print self._mp.battery_info() def laptop_info(self): """Return a string with laptop serial, battery type, build, memory info, etc.""" s = '' try: # Do not include UUID! s += 'laptop-info-version: 1.0\n' s += 'clock: %f\n' % time.clock() s += 'date: %s' % time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) s += 'memfree: %s\n' % self._mp.memfree() s += 'disksize: %s MB\n' % ( self._mp.disksize('/') / (1024*1024) ) s += 'diskfree: %s MB\n' % ( self._mp.diskfree('/') / (1024*1024) ) s += 'olpc_build: %s\n' % self._mp.olpc_build() s += 'kernel_version: %s\n' % self._mp.kernel_version() s += 'uptime: %s\n' % self._mp.uptime() s += 'loadavg: %s\n' % self._mp.loadavg() s += 'serial-number: %s\n' % self._mp.laptop_serial_number() s += 'motherboard-number: %s\n' % self._mp.laptop_motherboard_number() s += 'board-revision: %s\n' % self._mp.laptop_board_revision() s += 'keyboard: %s\n' % self._mp.laptop_keyboard() s += 'wireless_mac: %s\n' % self._mp.laptop_wireless_mac() s += 'firmware: %s\n' % self._mp.laptop_bios_version() s += 'country: %s\n' % self._mp.laptop_country() s += 'localization: %s\n' % self._mp.laptop_localization() s += self._mp.battery_info() s += "\n[/sbin/ifconfig]\n%s\n" % self._mp.ifconfig() s += "\n[/sbin/route -n]\n%s\n" % self._mp.route_n() s += '\n[Installed Activities]\n%s\n' % self._mp.installed_activities() s += '\n[df -a]\n%s\n' % self._mp.df_a() s += '\n[ps auxwww]\n%s\n' % self._mp.ps_auxfwww() s += '\n[free]\n%s\n' % self._mp.usr_bin_free() s += '\n[top -bn2]\n%s\n' % self._mp.top() except Exception, e: s += '\nException while building info:\n%s\n' % e return s class LogSend: # post_multipart and encode_multipart_formdata have been taken from # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 def post_multipart(self, host, selector, fields, files): """ Post fields and files to an http host as multipart/form-data. fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files Return the server's response page. """ content_type, body = self.encode_multipart_formdata(fields, files) h = httplib.HTTP(host) h.putrequest('POST', selector) h.putheader('content-type', content_type) h.putheader('content-length', str(len(body))) h.putheader('Host', host) h.endheaders() h.send(body) errcode, errmsg, headers = h.getreply() return h.file.read() def encode_multipart_formdata(self, fields, files): """ fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files Return (content_type, body) ready for httplib.HTTP instance """ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' L = [] for (key, value) in fields: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('') L.append(value) for (key, filename, value) in files: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) L.append('Content-Type: %s' % self.get_content_type(filename)) L.append('') L.append(value) L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body def read_file(self, filename): """Read the entire contents of a file and return it as a string""" data = '' f = open(filename) try: data = f.read() finally: f.close() return data def get_content_type(self, filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' def http_post_logs(self, url, archive): #host, selector, fields, files files = ('logs', os.path.basename(archive), self.read_file(archive)), # Client= olpc will make the server return just "OK" or "FAIL" fields = ('client', 'xo'), urlparts = urlparse.urlsplit(url) print "Sending logs to %s" % url r = self.post_multipart(urlparts[1], urlparts[2], fields, files) print r return (r == 'OK') # This script is dual-mode, it can be used as a command line tool and as # a library. if sys.argv[0].endswith('logcollect.py') or \ sys.argv[0].endswith('logcollect'): print 'log-collect utility 1.0' lc = LogCollect() ls = LogSend() logs = '' mode = 'http' if len(sys.argv)==1: print """logcollect.py - send your XO logs to OLPC Usage: logcollect.py http - send logs to default server logcollect.py http://server.name/submit.php - submit logs to alternative server logcollect.py file:/media/xxxx-yyyy/mylog.zip - save the zip file on a USB device or SD card logcollect.py all file:/media/xxxx-yyyy/mylog.zip - Save to zip file and include ALL logs logcollect.py none http - Just send info.txt, but no logs via http. logcollect.py none file - Just save info.txt in /dev/shm/logs-SN123.zip If you specify 'all' or 'none' you must specify http or file as well. """ sys.exit() logbytes = 15360 if len(sys.argv)>1: mode = sys.argv[len(sys.argv)-1] if sys.argv[1] == 'all': logbytes = 0 if sys.argv[1] == 'none': logbytes = -1 if mode.startswith('file'): # file:// logs = mode[5:] #if mode.lower().startswith('http'): # pass #else if mode.lower().startswith('usb'): # pass #else if mode.lower().startswith('sd'): # pass logs = lc.write_logs(logs, logbytes) print 'Logs saved in %s' % logs sent_ok = False if len(sys.argv)>1: mode = sys.argv[len(sys.argv)-1] if mode.startswith('http'): print "Trying to send the logs using HTTP (web)" if len(mode) == 4: url = 'http://olpc.scheffers.net/olpc/submit.tcl' else: url = mode if ls.http_post_logs(url, logs): print "Logs were sent." sent_ok = True else: print "FAILED to send logs." if sent_ok: os.remove(logs) print "Logs were sent, tempfile deleted." sugar-logviewer-activity-6.orig/Log.activity/setup.py0000644000175000017500000000147510715326074021662 0ustar janijani#!/usr/bin/python # Copyright (C) 2006, Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from sugar.activity import bundlebuilder bundlebuilder.start('Log')