[Dextrose] [PATCH sugar 2/2] Initial client implementation for feedback feature

Aleksey Lim alsroot at member.fsf.org
Mon Jan 17 21:53:45 EST 2011


---
 bin/sugar-session                      |    8 +-
 data/sugar.schemas.in                  |   44 +++++++
 src/jarabe/model/Makefile.am           |    1 +
 src/jarabe/model/feedback_collector.py |  201 ++++++++++++++++++++++++++++++++
 src/jarabe/model/shell.py              |   16 +++
 src/jarabe/view/service.py             |    5 +
 6 files changed, 274 insertions(+), 1 deletions(-)
 create mode 100644 src/jarabe/model/feedback_collector.py

diff --git a/bin/sugar-session b/bin/sugar-session
index 0501311..d4362ef 100755
--- a/bin/sugar-session
+++ b/bin/sugar-session
@@ -229,7 +229,7 @@ def main():
     gettext.textdomain('sugar')
 
     from jarabe.desktop import homewindow
-    from jarabe.model import sound
+    from jarabe.model import sound, feedback_collector
     from jarabe import intro
 
     logger.start('shell')
@@ -238,6 +238,12 @@ def main():
     client.set_string('/apps/metacity/general/mouse_button_modifier',
                       '<Super>')
 
+    if client.get_bool('/desktop/sugar/feedback/enabled'):
+        feedback_collector.start(
+                client.get_string('/desktop/sugar/feedback/server_host'),
+                client.get_int('/desktop/sugar/feedback/server_port'),
+                client.get_int('/desktop/sugar/feedback/auto_submit_delay'))
+
     timezone = client.get_string('/desktop/sugar/date/timezone')
     if timezone is not None and timezone:
         os.environ['TZ'] = timezone
diff --git a/data/sugar.schemas.in b/data/sugar.schemas.in
index a3817e9..880c9d1 100644
--- a/data/sugar.schemas.in
+++ b/data/sugar.schemas.in
@@ -2,6 +2,50 @@
 <gconfschemafile>
   <schemalist>
     <schema>
+      <key>/schemas/desktop/sugar/feedback/enabled</key>
+      <applyto>/desktop/sugar/feedback/enabled</applyto>
+      <owner>sugar</owner>
+      <type>bool</type>
+      <default>false</default>
+      <locale name="C">
+        <short>Enable of disable feedback feature</short>
+        <long>Setting value to false will disable any feedback reports.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/server_host</key>
+      <applyto>/desktop/sugar/feedback/server_host</applyto>
+      <owner>sugar</owner>
+      <type>string</type>
+      <default>feedback.sugarlabs.org</default>
+      <locale name="C">
+        <short>Server host to send reports to</short>
+        <long>Server that will handle reports sent via HTTPS POST requests.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/server_port</key>
+      <applyto>/desktop/sugar/feedback/server_port</applyto>
+      <owner>sugar</owner>
+      <type>int</type>
+      <default>80</default>
+      <locale name="C">
+        <short>Server port to send reports to</short>
+        <long>TCP port that will used to send HTTPS POST requests.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/auto_submit_delay</key>
+      <applyto>/desktop/sugar/feedback/auto_submit_delay</applyto>
+      <owner>sugar</owner>
+      <type>int</type>
+      <default>0</default>
+      <locale name="C">
+        <short>Delay in seconds to send anonymous reports automatically</short>
+        <long>Setting value to 0 will disable automatic submiting.</long>
+      </locale>
+    </schema>
+    <schema>
       <key>/schemas/desktop/sugar/user/nick</key>
       <applyto>/desktop/sugar/user/nick</applyto>
       <owner>sugar</owner>
diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am
index 374d6fc..c221e86 100644
--- a/src/jarabe/model/Makefile.am
+++ b/src/jarabe/model/Makefile.am
@@ -19,4 +19,5 @@ sugar_PYTHON =			\
         session.py		\
 	sound.py		\
 	processmanagement.py	\
+	feedback_collector.py	\
 	virtualkeyboard.py
diff --git a/src/jarabe/model/feedback_collector.py b/src/jarabe/model/feedback_collector.py
new file mode 100644
index 0000000..c0deae2
--- /dev/null
+++ b/src/jarabe/model/feedback_collector.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2011, Aleksey Lim
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+import os
+import time
+import httplib
+import logging
+import tarfile
+import threading
+from cStringIO import StringIO
+from os.path import join, exists, basename
+from email.mime.multipart import MIMEMultipart
+from email.mime.application import MIMEApplication
+from email.generator import Generator
+from email.encoders import encode_noop
+
+import gconf
+import gobject
+import simplejson
+
+from sugar import logger, feedback, util
+
+
+_reports = {}
+_logs = set()
+_host = None
+_port = None
+
+
+def start(host, port, auto_submit_delay):
+    global _host
+    global _port
+
+    _host = host
+    _port = port
+
+    if auto_submit_delay > 0:
+        gobject.timeout_add_seconds(auto_submit_delay, _submit)
+
+
+def update(bundle_id, report, log_file):
+    if bundle_id not in _reports:
+        _reports[bundle_id] = {}
+    stat = _reports[bundle_id]
+
+    for key, count in report.items():
+        if key not in stat:
+            stat[key] = 0
+        stat[key] += count
+
+    if log_file:
+        _logs.add(log_file)
+
+
+def is_empty():
+    report, shell_log = feedback.flush()
+    if report:
+        if shell_log:
+            shell_log = join(logger.get_logs_dir(), 'shell.log')
+        update('shell', report, shell_log)
+
+    return not _reports
+
+
+def submit(message):
+    from jarabe.journal import misc
+
+    client = gconf.client_get_default()
+    jabber = client.get_string('/desktop/sugar/collaboration/jabber_server')
+    nick = client.get_string("/desktop/sugar/user/nick")
+
+    data = {'message': message,
+            'serial_number': misc.get_xo_serial(),
+            'nick': nick,
+            'jabber_server': jabber,
+            }
+    _submit(data)
+
+
+def anonymous_submit():
+    _submit()
+
+
+def _submit(data=None):
+    if data:
+        _reports.update(data)
+    if is_empty():
+        return True
+
+    logging.debug('Sending feedback report: %r', _reports)
+
+    report = simplejson.dumps(_reports)
+    _reports.clear()
+
+    tar_file = util.TempFilePath()
+    tar = tarfile.open(tar_file, 'w:gz')
+
+    while _logs:
+        log_file = _logs.pop()
+        if exists(log_file):
+            tar.add(log_file, arcname=basename(log_file))
+
+    report_file = tarfile.TarInfo('report')
+    report_file.mode = 0644
+    report_file.mtime = int(time.time())
+    report_file.size = len(report)
+    tar.addfile(report_file, StringIO(report))
+
+    tar.close()
+
+    _SubmitThread(tar_file).run()
+
+    return True
+
+
+class _SubmitThread(threading.Thread):
+
+    def __init__(self, tar_file):
+        threading.Thread.__init__(self)
+        self._tar_file = tar_file
+
+    def run(self):
+        try:
+            message = _FormData()
+            attachment = MIMEApplication(file(self._tar_file).read(),
+                    _encoder=encode_noop)
+            message.attach_file(attachment,
+                    name='report', filename='report.tar.gz')
+            body, headers = message.get_request_data()
+
+            conn = httplib.HTTPSConnection(_host, _port)
+            conn.request('POST', '/', body, headers)
+            response = conn.getresponse()
+
+            if response.status != 200:
+                logging.error('Incorrect feedback submit: %s, %s',
+                        response.status, response.read())
+
+        except Exception:
+            logging.exception('Cannot submit feedback')
+        finally:
+            os.unlink(self._tar_file)
+            self._tar_file = None
+
+
+class _FormData(MIMEMultipart):
+    '''A simple RFC2388 multipart/form-data implementation.
+
+    A snippet from http://bugs.python.org/issue3244
+
+    '''
+
+    def __init__(self, boundary=None, _subparts=None, **kwargs):
+        MIMEMultipart.__init__(self, _subtype='form-data',
+                boundary=boundary, _subparts=_subparts, **kwargs)
+
+    def attach(self, subpart):
+        if 'MIME-Version' in subpart:
+            if subpart['MIME-Version'] != self['MIME-Version']:
+                raise ValueError('subpart has incompatible MIME-Version')
+            # Note: This isn't strictly necessary, but there is no point in
+            # including a MIME-Version header in each subpart.
+            del subpart['MIME-Version']
+        MIMEMultipart.attach(self, subpart)
+
+    def attach_file(self, subpart, name, filename):
+        '''
+        Attach a subpart, setting it's Content-Disposition header to "file".
+        '''
+        name = name.replace('"', '\\"')
+        filename = filename.replace('"', '\\"')
+        subpart['Content-Disposition'] = \
+                'form-data; name="%s"; filename="%s"' % (name, filename)
+        self.attach(subpart)
+
+    def get_request_data(self, trailing_newline=True):
+        '''Return the encoded message body.'''
+        f = StringIO()
+        generator = Generator(f, mangle_from_=False)
+        # pylint: disable-msg=W0212
+        generator._dispatch(self)
+        # HTTP needs a trailing newline.  Since our return value is likely to
+        # be passed directly to an HTTP connection, we might as well add it
+        # here.
+        if trailing_newline:
+            f.write('\n')
+        body = f.getvalue()
+        headers = dict(self)
+        return body, headers
diff --git a/src/jarabe/model/shell.py b/src/jarabe/model/shell.py
index 69b45e2..b9cb0c2 100644
--- a/src/jarabe/model/shell.py
+++ b/src/jarabe/model/shell.py
@@ -23,12 +23,14 @@ import wnck
 import gobject
 import gtk
 import dbus
+import simplejson
 
 from sugar import wm
 from sugar import dispatch
 from sugar.graphics.xocolor import XoColor
 from sugar.presence import presenceservice
 
+from jarabe.model import feedback_collector
 from jarabe.model.bundleregistry import get_registry
 
 _SERVICE_NAME = "org.laptop.Activity"
@@ -220,6 +222,12 @@ class Activity(gobject.GObject):
         else:
             return self._activity_info.get_path()
 
+    def get_bundle_id(self):
+        if self._activity_info is None:
+            return None
+        else:
+            return self._activity_info.get_bundle_id()
+
     def get_activity_name(self):
         """Returns the activity's bundle name"""
         if self._activity_info is None:
@@ -618,6 +626,14 @@ class ShellModel(gobject.GObject):
             logging.error('Model for activity id %s does not exist.',
                 activity_id)
 
+    def notify_feedback(self, activity_id, report, log_file):
+        home_activity = self.get_activity_by_id(activity_id)
+        if home_activity is not None:
+            feedback_collector.update(home_activity.get_bundle_id(),
+                    simplejson.loads(report), log_file)
+        else:
+            logging.error('No %s activity for sending feedback', activity_id)
+
     def _check_activity_launched(self, activity_id):
         home_activity = self.get_activity_by_id(activity_id)
 
diff --git a/src/jarabe/view/service.py b/src/jarabe/view/service.py
index bb71694..a576eb9 100644
--- a/src/jarabe/view/service.py
+++ b/src/jarabe/view/service.py
@@ -97,6 +97,11 @@ class UIService(dbus.service.Object):
     def NotifyLaunchFailure(self, activity_id):
         shell.get_model().notify_launch_failed(activity_id)
 
+    @dbus.service.method(_DBUS_SHELL_IFACE,
+                         in_signature="sss", out_signature="")
+    def Feedback(self, activity_id, report, log_file):
+        shell.get_model().notify_feedback(activity_id, report, log_file)
+
     @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s")
     def ColorChanged(self, color):
         pass
-- 
1.7.3.4



More information about the Dextrose mailing list