[Dextrose] [PATCH] Resolution for ticket au#887.
Ajay Garg
ajaygargnsit at gmail.com
Tue Sep 20 07:52:56 EDT 2011
=== Usability notes ===
1. Now, besides an interactive 'Software Update', a background
thread also checks for available activity updates. To prevent
possible collisions in a rare situation when the interactive
session and the background thread might run at the same time,
synchronization has been used.
2. When a user enters into 'Software Update' section-view,
exclusive control is gained. The exclusive control is
retained unless and until this section-view is exited
(via 'Cancel' or 'Ok' toolbar buttons).
If the background thread wishes to run, it waits,
while the user interacts with the section-view.
3. If the background thread is running, it has exclusive control.
If the user wishes to enter into an interactive session (by
clicking onto the 'Software Update' section-view-icon),
following happens :
a. The section-view shows 'Getting ready...' markup,
with the message 'Waiting for activity-update-
notifier thread to relinquish control...'
b. The (interactive) thread waits, until the background
thread relinquishes control.
c. After the (exclusive) control is retained, the
interactive thread proceeds normally, checking
for updates, followed by user-wish of installing
updates.
=== Implementation notes ===
For this, comments have been added into the code appropriately.
=== Configuration notes ===
1. Following gconf keys can be used to configure this feature.
a. KEY : /desktop/sugar/activity_notifier_enabled
PURPOSE : Enable/Disable the notifier background thread
TYPE : boolean
DEFAULT : true (enables the notifier background thread)
b. KEY : /desktop/sugar/activity_notifier_interval_first_time
PURPOSE : Interval after which the background thread runs for
the first time
TYPE : int
DEFAULT : 120 (seconds)
c. KEY : /desktop/sugar/activity_notifier_interval_second_time_onwards
PURPOSE : Interval for the subsequen runs
TYPE : int
DEFAULT : 604800 (seconds). This is equivalent to 7 days.
2. Note that these keys would be used on a pre-configured basis.
No change-notifier callbacks have been added into the code,
to detect key-changes during runtime.
=== Design enhancements/changes ===
1. A mechanism for loading "daemon" classes has been added into
"src/jarabe/controlpanel/homewindow.py" file.
It searches for an attribute "DAEMON" in the "__init__.py" files
of "extensions/cpsection" modules. If the attribute is found, the
corresponding class is loaded.
Currently, only "Software Update" module has the attribute "DAEMON"
in "__init__.py". The corresponding class - "ActivityNotifier" is loaded.
Being a subclass of "threading.Thread", this class runs as a daemon.
Note that the loading of these "daemon" classes occurs during the loading
of home-window, since that is the first thing that happens after boot.
Signed-off-by: Ajay Garg <ajay at sugarlabs.org>
---
data/sugar.schemas.in | 36 +++++++++
extensions/cpsection/updater/Makefile.am | 3 +-
extensions/cpsection/updater/__init__.py | 1 +
extensions/cpsection/updater/daemon.py | 93 +++++++++++++++++++++++
extensions/cpsection/updater/model.py | 24 ++++++
extensions/cpsection/updater/view.py | 118 +++++++++++++++++++++++------
src/jarabe/controlpanel/gui.py | 20 +++++
src/jarabe/controlpanel/sectionview.py | 32 ++++++++
src/jarabe/desktop/homewindow.py | 35 +++++++++
9 files changed, 336 insertions(+), 26 deletions(-)
create mode 100644 extensions/cpsection/updater/daemon.py
diff --git a/data/sugar.schemas.in b/data/sugar.schemas.in
index aa031da..54ba4e2 100644
--- a/data/sugar.schemas.in
+++ b/data/sugar.schemas.in
@@ -129,6 +129,42 @@
</schema>
<schema>
+ <key>/schemas/desktop/sugar/activity_notifier_enabled</key>
+ <applyto>/desktop/sugar/activity_notifier_enabled</applyto>
+ <owner>sugar</owner>
+ <type>bool</type>
+ <default>true</default>
+ <locale name="C">
+ <short>Flag to turn on/off the activity-update-notifier.</short>
+ <long>This key contains the flag, which can be used to turn on/off the activity-update-notifier.</long>
+ </locale>
+ </schema>
+
+ <schema>
+ <key>/schemas/desktop/sugar/activity_notifier_interval_first_time</key>
+ <applyto>/desktop/sugar/activity_notifier_interval_first_time</applyto>
+ <owner>sugar</owner>
+ <type>int</type>
+ <default>120</default>
+ <locale name="C">
+ <short>Interval for activity-update-notifier timer (in seconds), for first run</short>
+ <long>This key contains the interval, after which, the activity-update-notifier runs for the first time. Default value is 120 seconds.</long>
+ </locale>
+ </schema>
+
+ <schema>
+ <key>/schemas/desktop/sugar/activity_notifier_interval_second_time_onwards</key>
+ <applyto>/desktop/sugar/activity_notifier_interval_second_time_onwards</applyto>
+ <owner>sugar</owner>
+ <type>int</type>
+ <default>604800</default>
+ <locale name="C">
+ <short>Interval for activity-update-notifier timer (in seconds), for second run onwards</short>
+ <long>This key contains the interval, for activity-update-notifier, for second and subsequent runs. Default value is 604800 seconds (~ 7 days)</long>
+ </locale>
+ </schema>
+
+ <schema>
<key>/schemas/desktop/sugar/backup_url</key>
<applyto>/desktop/sugar/backup_url</applyto>
<owner>sugar</owner>
diff --git a/extensions/cpsection/updater/Makefile.am b/extensions/cpsection/updater/Makefile.am
index 897dbf3..d346cac 100644
--- a/extensions/cpsection/updater/Makefile.am
+++ b/extensions/cpsection/updater/Makefile.am
@@ -5,4 +5,5 @@ sugardir = $(pkgdatadir)/extensions/cpsection/updater
sugar_PYTHON = \
__init__.py \
model.py \
- view.py
+ view.py \
+ daemon.py
diff --git a/extensions/cpsection/updater/__init__.py b/extensions/cpsection/updater/__init__.py
index 6010615..7d4b432 100644
--- a/extensions/cpsection/updater/__init__.py
+++ b/extensions/cpsection/updater/__init__.py
@@ -17,6 +17,7 @@
from gettext import gettext as _
CLASS = 'ActivityUpdater'
+DAEMON = 'ActivityNotifier'
ICON = 'module-updater'
TITLE = _('Software update')
KEYWORDS = ['software', 'activity', 'update']
diff --git a/extensions/cpsection/updater/daemon.py b/extensions/cpsection/updater/daemon.py
new file mode 100644
index 0000000..a1b54e1
--- /dev/null
+++ b/extensions/cpsection/updater/daemon.py
@@ -0,0 +1,93 @@
+# Copyright (C) 2011, Ajay Garg
+#
+# 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 logging
+import threading
+import time
+
+from gettext import gettext as _
+
+import gconf
+import gobject
+
+from jarabe import frame
+from model import UpdateModel
+from model import ModelAttributesHolder
+
+gobject.threads_init()
+
+client = gconf.client_get_default()
+NOTIFIER_ENABLED = \
+ client.get_bool('/desktop/sugar/activity_notifier_enabled')
+NOTIFIER_INTERVAL_FIRST_TIME = \
+ client.get_int('/desktop/sugar/activity_notifier_interval_first_time')
+NOTIFIER_INTERVAL_SECOND_TIME_ONWARDS = \
+ client.get_int('/desktop/sugar/activity_notifier_interval_second_time_onwards')
+
+
+class ActivityNotifier(threading.Thread):
+
+
+ def __init__(self):
+ threading.Thread.__init__(self)
+ self.start()
+
+
+ def run(self):
+ if NOTIFIER_ENABLED:
+ self._model = UpdateModel()
+ self._model.connect('progress', self._progress_cb)
+
+ time.sleep(NOTIFIER_INTERVAL_FIRST_TIME)
+
+ while 1:
+ self._timer_cb()
+ time.sleep(NOTIFIER_INTERVAL_SECOND_TIME_ONWARDS)
+
+
+ def _timer_cb(self):
+ self.timer_cb_execute()
+ return True
+
+
+ def timer_cb_execute(self):
+ ModelAttributesHolder.grab_exclusive_control(True)
+
+ self._model.check_updates()
+ return True
+
+
+ def _progress_cb(self, model, action, bundle_name, current, total):
+ if current == total and action == UpdateModel.ACTION_CHECKING:
+ self._finished_checking()
+
+
+ def _finished_checking(self):
+ available_updates = len(self._model.updates)
+
+ ModelAttributesHolder.yield_exclusive_control()
+
+ if available_updates:
+ title = _('Activity Updates available')
+ msg = _('%d updates available.\n'
+ 'Please run \'Software Update\' '
+ 'to install these updates.' % available_updates)
+ gobject.idle_add(lambda:
+ frame.get_view().add_message(summary=title,
+ body=msg))
+ else:
+ # Do nothing. No updates available.
+ logging.debug('No activity-updates found. Not showing any'
+ 'notification')
diff --git a/extensions/cpsection/updater/model.py b/extensions/cpsection/updater/model.py
index 39d0b1f..5e6fc73 100755
--- a/extensions/cpsection/updater/model.py
+++ b/extensions/cpsection/updater/model.py
@@ -38,6 +38,30 @@ from sugar.bundle.bundleversion import NormalizedVersion
from jarabe.model import bundleregistry
from backends import microformat
+from threading import Condition
+
+
+class ModelAttributesHolder():
+
+ background_action = {}
+ flag = True
+ condition = Condition()
+
+ @staticmethod
+ def grab_exclusive_control(is_bg_action):
+ ModelAttributesHolder.condition.acquire()
+ while not ModelAttributesHolder.flag:
+ ModelAttributesHolder.condition.wait()
+ ModelAttributesHolder.flag = False
+ ModelAttributesHolder.background_action = is_bg_action
+ ModelAttributesHolder.condition.release()
+
+ @staticmethod
+ def yield_exclusive_control():
+ ModelAttributesHolder.condition.acquire()
+ ModelAttributesHolder.flag = True
+ ModelAttributesHolder.condition.notify()
+ ModelAttributesHolder.condition.release()
class MetaBundle():
diff --git a/extensions/cpsection/updater/view.py b/extensions/cpsection/updater/view.py
index d257b56..a67b894 100644
--- a/extensions/cpsection/updater/view.py
+++ b/extensions/cpsection/updater/view.py
@@ -29,12 +29,62 @@ from sugar.graphics.icon import Icon, CellRendererIcon
from jarabe.controlpanel.sectionview import SectionView
from model import UpdateModel
+from model import ModelAttributesHolder
_DEBUG_VIEW_ALL = True
class ActivityUpdater(SectionView):
+ """
+ Notes:
+
+ 1. This class, represents the 'Software Update' section-view.
+ A new instance of ActivityUpdater is created, everytime user
+ enters into the 'Software Update' section-view.
+
+ 2. Now, besides an interactive 'Software Update', a background
+ thread also checks for available activity updates. (See
+ 'daemon.py'). To prevent possible collisions in a rare
+ situation when the interactive session and the background
+ thread might run at the same time, synchronization has
+ been used.
+ (
+ If both are allowed to run simultaneously, I was getting
+ errors in gio.File.read_async()
+ )
+
+ 3. When a user enters into 'Software Update' section-view,
+ exclusive control is gained. The exclusive control is
+ retained unless and until this section-view is exited
+ (via 'Cancel' or 'Ok' toolbar buttons).
+
+ See overridden 'self.post_show_section_view()' and
+ 'self.post_exit()' methods.
+
+ If the background thread wishes to run, it waits,
+ while the user interacts with the section-view.
+
+
+ 4. If the background thread is running, it has exclusive control.
+ If the user wishes to enter into an interactive session (by
+ clicking onto the 'Software Update' section-view-icon),
+ following happens :
+
+ a. The section-view shows 'Getting ready...' markup,
+ with the message 'Waiting for activity-update-
+ notifier thread to relinquish control...'
+ b. The (interactive) thread waits, until the background
+ thread relinquishes control.
+ c. After the (exclusive) control is retained, the
+ interactive thread proceeds normally, checking
+ for updates, followed by user-wish of installing
+ updates.
+
+ See overridden 'self.post_show_section_view()' method.
+ """
+
+
def __init__(self, model, alerts):
SectionView.__init__(self)
@@ -68,7 +118,13 @@ class ActivityUpdater(SectionView):
self._update_box = None
self._progress_pane = None
- self._refresh()
+ # Set up an inital set-up for section-view.
+ top_message = _('Getting ready...')
+ self._top_label.set_markup('<big>%s</big>' % top_message)
+ self.__progress_cb('progress', UpdateModel.ACTION_CHECKING,
+ 'Waiting for '
+ 'activity-update-notifier thread to '
+ 'relinquish control', 0, 3, False)
def _switch_to_update_box(self):
if self._update_box in self.get_children():
@@ -113,7 +169,8 @@ class ActivityUpdater(SectionView):
self.remove(self._update_box)
self._update_box = None
- def __progress_cb(self, model, action, bundle_name, current, total):
+ def __progress_cb(self, model, action, bundle_name, current, total,
+ check_for_background_action=True):
if current == total and action == UpdateModel.ACTION_CHECKING:
self._finished_checking()
return
@@ -121,36 +178,41 @@ class ActivityUpdater(SectionView):
self._finished_updating(int(current))
return
- if action == UpdateModel.ACTION_CHECKING:
- message = _('%s...') % bundle_name
- elif action == UpdateModel.ACTION_DOWNLOADING:
- message = _('Downloading %s...') % bundle_name
- elif action == UpdateModel.ACTION_UPDATING:
- message = _('Updating %s...') % bundle_name
+ if (not check_for_background_action) or \
+ ((check_for_background_action) and \
+ (not ModelAttributesHolder.background_action)):
+ if action == UpdateModel.ACTION_CHECKING:
+ message = _('%s...') % bundle_name
+ elif action == UpdateModel.ACTION_DOWNLOADING:
+ message = _('Downloading %s...') % bundle_name
+ elif action == UpdateModel.ACTION_UPDATING:
+ message = _('Updating %s...') % bundle_name
- self._switch_to_progress_pane()
- self._progress_pane.set_message(message)
- self._progress_pane.set_progress(current / float(total))
+ self._switch_to_progress_pane()
+ self._progress_pane.set_message(message)
+ self._progress_pane.set_progress(current / float(total))
def _finished_checking(self):
logging.debug('ActivityUpdater._finished_checking')
available_updates = len(self._model.updates)
- if not available_updates:
- top_message = _('Your software is up-to-date')
- else:
- top_message = ngettext('You can install %s update',
- 'You can install %s updates',
- available_updates)
- top_message = top_message % available_updates
- top_message = gobject.markup_escape_text(top_message)
- self._top_label.set_markup('<big>%s</big>' % top_message)
+ if not ModelAttributesHolder.background_action:
+ if not available_updates:
+ top_message = _('Your software is up-to-date')
+ else:
+ top_message = ngettext('You can install %s update',
+ 'You can install %s updates',
+ available_updates)
+ top_message = top_message % available_updates
+ top_message = gobject.markup_escape_text(top_message)
+
+ self._top_label.set_markup('<big>%s</big>' % top_message)
- if not available_updates:
- self._clear_center()
- else:
- self._switch_to_update_box()
- self._update_box.refresh()
+ if not available_updates:
+ self._clear_center()
+ else:
+ self._switch_to_update_box()
+ self._update_box.refresh()
def __refresh_button_clicked_cb(self, button):
self._refresh()
@@ -180,6 +242,12 @@ class ActivityUpdater(SectionView):
def undo(self):
self._model.cancel()
+ def post_show_section_view(self):
+ ModelAttributesHolder.grab_exclusive_control(False)
+ self._refresh()
+
+ def post_exit(self):
+ ModelAttributesHolder.yield_exclusive_control()
class ProgressPane(gtk.VBox):
"""Container which replaces the `ActivityPane` during refresh or
diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py
index b4879c6..f420a96 100644
--- a/src/jarabe/controlpanel/gui.py
+++ b/src/jarabe/controlpanel/gui.py
@@ -177,6 +177,18 @@ class ControlPanel(gtk.Window):
self._options[option]['button'] = sectionicon
def _show_main_view(self):
+
+ # 1. This method has two callers.
+ # 2. One, when the main_view is shown upon clicking on
+ # 'My Settings' from the home-window buddy-menu.
+ # 3. Second, when the main_view is shown, after exiting
+ # from a section_view.
+ # 4. In the second case, when we are coming out of the
+ # section-view, call the 'post_exit' method for the section-view
+ if self._section_view:
+ self._section_view.post_exit()
+
+
self._set_toolbar(self._main_toolbar)
self._main_toolbar.show()
self._set_canvas(self._scrolledwindow)
@@ -244,6 +256,14 @@ class ControlPanel(gtk.Window):
self._main_view.modify_bg(gtk.STATE_NORMAL,
style.COLOR_BG_CP.get_gdk_color())
+
+ # 1. Adding 'post_show_section_view()' method.
+ # 2. Please see 'jarabe.controlpanel.sectionview.SectionView'
+ # for definition.
+ # 3. This needs to done in 'idle' mode, to prevent
+ # UI freeze.
+ gobject.idle_add(self._section_view.post_show_section_view)
+
def set_section_view_auto_close(self):
"""Automatically close the control panel if there is "nothing to do"
"""
diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py
index 4b5751f..acd06fc 100644
--- a/src/jarabe/controlpanel/sectionview.py
+++ b/src/jarabe/controlpanel/sectionview.py
@@ -52,3 +52,35 @@ class SectionView(gtk.VBox):
def undo(self):
"""Undo here the changes that have been made in this section."""
pass
+
+ def post_show_section_view(self):
+ """
+ This method performs actions, after ::
+
+ 1. the initial set-up for the section-view has been set up
+ (in '__init__()' of subclasses of
+ 'jarabe.controlpanel.sectionview.SectionView')
+ 2. the section-view 'shown' (in 'show_section_view()' of
+ 'jarabe.controlpanel.gui.ControlPanel')
+
+ By default, nothing generic needs to be done, as generally, the
+ initial set-up is good enough (when no user-interaction is
+ required).
+
+ For a class that actually does something specific, please see
+ this over-ridden method in
+ 'extensions.cpcsection.updater.view.ActivityUpdater'
+ """
+ pass
+
+ def post_exit(self):
+ """
+ This method does the cleaning-up stuff, just as we are about
+ to leave the section-view.
+
+ By default, nothing generic needs to be done.
+
+ Any subclasses (eg. - 'extensions.cpcsection.updater.view.ActivityUpdater'),
+ may override this method.
+ """
+ pass
diff --git a/src/jarabe/desktop/homewindow.py b/src/jarabe/desktop/homewindow.py
index f101757..0bd9d05 100644
--- a/src/jarabe/desktop/homewindow.py
+++ b/src/jarabe/desktop/homewindow.py
@@ -16,6 +16,7 @@
import logging
+import os
import gobject
import gtk
import dbus
@@ -24,6 +25,7 @@ from gettext import gettext as _
from sugar.graphics import style
from sugar.graphics import palettegroup
+from jarabe import config
from jarabe.desktop.meshbox import MeshBox
from jarabe.desktop.homebox import HomeBox
from jarabe.desktop.groupbox import GroupBox
@@ -31,6 +33,7 @@ from jarabe.desktop.transitionbox import TransitionBox
from jarabe.model.shell import ShellModel
from jarabe.model import shell
from jarabe.model import notifications
+from jarabe.controlpanel.gui import ModelWrapper
_HOME_PAGE = 0
@@ -98,6 +101,8 @@ class HomeWindow(gtk.Window):
systembus.add_signal_receiver(self.__relogin_cb, 'Relogin',
_DBUS_SYSTEM_IFACE)
+ gobject.idle_add(self.load_cpsection_daemon_classes)
+
def _system_alert(self, replaces_id, app_icon, message):
service = notifications.get_service()
service.notification_received.send(self,app_name='system',
@@ -237,6 +242,36 @@ class HomeWindow(gtk.Window):
self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
gobject.idle_add(action_wrapper, old_cursor)
+ def load_cpsection_daemon_classes(self):
+ """Get the available option information from the extensions
+ """
+ options = {}
+
+ path = os.path.join(config.ext_path, 'cpsection')
+ folder = os.listdir(path)
+
+ for item in folder:
+ if os.path.isdir(os.path.join(path, item)) and \
+ os.path.exists(os.path.join(path, item, '__init__.py')):
+ try:
+ mod = __import__('.'.join(('cpsection', item)),
+ globals(), locals(), [item])
+ daemon_class = getattr(mod, 'DAEMON', None)
+ if daemon_class is not None:
+ options[item] = {}
+ options[item]['daemon'] = daemon_class
+ except Exception:
+ logging.exception('Exception while loading extension:')
+
+
+ option_keys = options.keys()
+ for key in option_keys:
+ mod = __import__('.'.join(('cpsection', key, 'daemon')),
+ globals(), locals(), ['daemon'])
+ if mod:
+ daemon_class = getattr(mod, options[key]['daemon'], None)
+ daemon_class()
+
def get_instance():
global _instance
--
1.7.4.4
More information about the Dextrose
mailing list