Cleanup the source structure
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
sugardir = $(pythondir)/sugar/activity
|
||||
sugar_PYTHON = \
|
||||
__init__.py \
|
||||
activity.py \
|
||||
activityfactory.py \
|
||||
activityhandle.py \
|
||||
activityservice.py \
|
||||
bundlebuilder.py \
|
||||
registry.py
|
||||
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2006-2007, Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
"""Activity implementation code for Sugar-based activities
|
||||
|
||||
Each activity within the OLPC environment must provide two
|
||||
dbus services. The first, patterned after the
|
||||
|
||||
sugar.activity.activityfactory.ActivityFactory
|
||||
|
||||
class is responsible for providing a "create" method which
|
||||
takes a small dictionary with values corresponding to a
|
||||
|
||||
sugar.activity.activityhandle.ActivityHandle
|
||||
|
||||
describing an individual instance of the activity.
|
||||
|
||||
Each activity so registered is described by a
|
||||
|
||||
sugar.activity.bundle.Bundle
|
||||
|
||||
instance, which parses a specially formatted activity.info
|
||||
file (stored in the activity directory's ./activity
|
||||
subdirectory). The
|
||||
|
||||
sugar.activity.bundlebuilder
|
||||
|
||||
module provides facilities for the standard setup.py module
|
||||
which produces and registers bundles from activity source
|
||||
directories.
|
||||
|
||||
Once instantiated by the ActivityFactory's create method,
|
||||
each activity must provide an introspection API patterned
|
||||
after the
|
||||
|
||||
sugar.activity.activityservice.ActivityService
|
||||
|
||||
class. This class allows for querying the ID of the root
|
||||
window, requesting sharing across the network, and basic
|
||||
"what type of application are you" queries.
|
||||
"""
|
||||
from sugar.activity.registry import ActivityRegistry
|
||||
from sugar.activity.registry import get_registry
|
||||
from sugar.activity.registry import ActivityInfo
|
||||
@@ -0,0 +1,633 @@
|
||||
"""Base class for Python-coded activities
|
||||
|
||||
This is currently the only reference for what an
|
||||
activity must do to participate in the Sugar desktop.
|
||||
"""
|
||||
# Copyright (C) 2006-2007 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
from hashlib import sha1
|
||||
|
||||
import gtk, gobject
|
||||
import dbus
|
||||
import json
|
||||
|
||||
from sugar import util
|
||||
from sugar.presence import presenceservice
|
||||
from sugar.activity.activityservice import ActivityService
|
||||
from sugar.graphics import style
|
||||
from sugar.graphics.window import Window
|
||||
from sugar.graphics.toolbox import Toolbox
|
||||
from sugar.graphics.toolbutton import ToolButton
|
||||
from sugar.graphics.toolcombobox import ToolComboBox
|
||||
from sugar.datastore import datastore
|
||||
from sugar import wm
|
||||
from sugar import profile
|
||||
from sugar import _sugarbaseext
|
||||
|
||||
SCOPE_PRIVATE = "private"
|
||||
SCOPE_INVITE_ONLY = "invite" # shouldn't be shown in UI, it's implicit when you invite somebody
|
||||
SCOPE_NEIGHBORHOOD = "public"
|
||||
|
||||
class ActivityToolbar(gtk.Toolbar):
|
||||
def __init__(self, activity):
|
||||
gtk.Toolbar.__init__(self)
|
||||
|
||||
self._activity = activity
|
||||
self._updating_share = False
|
||||
|
||||
activity.connect('shared', self._activity_shared_cb)
|
||||
activity.connect('joined', self._activity_shared_cb)
|
||||
activity.connect('notify::max_participants',
|
||||
self._max_participants_changed_cb)
|
||||
|
||||
if activity.metadata:
|
||||
self.title = gtk.Entry()
|
||||
self.title.set_size_request(int(gtk.gdk.screen_width() / 6), -1)
|
||||
self.title.set_text(activity.metadata['title'])
|
||||
self.title.connect('changed', self._title_changed_cb)
|
||||
self._add_widget(self.title)
|
||||
|
||||
activity.metadata.connect('updated', self._jobject_updated_cb)
|
||||
|
||||
separator = gtk.SeparatorToolItem()
|
||||
separator.props.draw = False
|
||||
separator.set_expand(True);
|
||||
self.insert(separator, -1)
|
||||
separator.show()
|
||||
|
||||
self.share = ToolComboBox(label_text=_('Share with:'))
|
||||
self.share.combo.connect('changed', self._share_changed_cb)
|
||||
self.share.combo.append_item(SCOPE_PRIVATE, _('Private'),
|
||||
'zoom-home-mini')
|
||||
self.share.combo.append_item(SCOPE_NEIGHBORHOOD, _('My Neighborhood'),
|
||||
'zoom-neighborhood-mini')
|
||||
self.insert(self.share, -1)
|
||||
self.share.show()
|
||||
|
||||
self._update_share()
|
||||
|
||||
self.keep = ToolButton('document-save')
|
||||
self.keep.set_tooltip(_('Keep'))
|
||||
self.keep.connect('clicked', self._keep_clicked_cb)
|
||||
self.insert(self.keep, -1)
|
||||
self.keep.show()
|
||||
|
||||
self.stop = ToolButton('activity-stop')
|
||||
self.stop.set_tooltip(_('Stop'))
|
||||
self.stop.connect('clicked', self._stop_clicked_cb)
|
||||
self.insert(self.stop, -1)
|
||||
self.stop.show()
|
||||
|
||||
self._update_title_sid = None
|
||||
|
||||
def _update_share(self):
|
||||
self._updating_share = True
|
||||
|
||||
if self._activity.props.max_participants == 1:
|
||||
self.share.hide()
|
||||
|
||||
if self._activity.get_shared():
|
||||
self.share.set_sensitive(False)
|
||||
self.share.combo.set_active(1)
|
||||
else:
|
||||
self.share.set_sensitive(True)
|
||||
self.share.combo.set_active(0)
|
||||
|
||||
self._updating_share = False
|
||||
|
||||
def _share_changed_cb(self, combo):
|
||||
if self._updating_share:
|
||||
return
|
||||
|
||||
model = self.share.combo.get_model()
|
||||
it = self.share.combo.get_active_iter()
|
||||
(scope, ) = model.get(it, 0)
|
||||
if scope == SCOPE_NEIGHBORHOOD:
|
||||
self._activity.share()
|
||||
|
||||
def _keep_clicked_cb(self, button):
|
||||
self._activity.copy()
|
||||
|
||||
def _stop_clicked_cb(self, button):
|
||||
self._activity.close()
|
||||
|
||||
def _jobject_updated_cb(self, jobject):
|
||||
self.title.set_text(jobject['title'])
|
||||
|
||||
def _title_changed_cb(self, entry):
|
||||
if not self._update_title_sid:
|
||||
self._update_title_sid = gobject.timeout_add(1000, self._update_title_cb)
|
||||
|
||||
def _update_title_cb(self):
|
||||
title = self.title.get_text()
|
||||
|
||||
self._activity.metadata['title'] = title
|
||||
self._activity.metadata['title_set_by_user'] = '1'
|
||||
self._activity.save()
|
||||
|
||||
shared_activity = self._activity._shared_activity
|
||||
if shared_activity:
|
||||
shared_activity.props.name = title
|
||||
|
||||
self._update_title_sid = None
|
||||
return False
|
||||
|
||||
def _add_widget(self, widget, expand=False):
|
||||
tool_item = gtk.ToolItem()
|
||||
tool_item.set_expand(expand)
|
||||
|
||||
tool_item.add(widget)
|
||||
widget.show()
|
||||
|
||||
self.insert(tool_item, -1)
|
||||
tool_item.show()
|
||||
|
||||
def _activity_shared_cb(self, activity):
|
||||
self._update_share()
|
||||
|
||||
def _max_participants_changed_cb(self, activity, pspec):
|
||||
self._update_share()
|
||||
|
||||
class EditToolbar(gtk.Toolbar):
|
||||
def __init__(self):
|
||||
gtk.Toolbar.__init__(self)
|
||||
|
||||
self.undo = ToolButton('edit-undo')
|
||||
self.undo.set_tooltip(_('Undo'))
|
||||
self.insert(self.undo, -1)
|
||||
self.undo.show()
|
||||
|
||||
self.redo = ToolButton('edit-redo')
|
||||
self.redo.set_tooltip(_('Redo'))
|
||||
self.insert(self.redo, -1)
|
||||
self.redo.show()
|
||||
|
||||
self.separator = gtk.SeparatorToolItem()
|
||||
self.separator.set_draw(True)
|
||||
self.insert(self.separator, -1)
|
||||
self.separator.show()
|
||||
|
||||
self.copy = ToolButton('edit-copy')
|
||||
self.copy.set_tooltip(_('Copy'))
|
||||
self.insert(self.copy, -1)
|
||||
self.copy.show()
|
||||
|
||||
self.paste = ToolButton('edit-paste')
|
||||
self.paste.set_tooltip(_('Paste'))
|
||||
self.insert(self.paste, -1)
|
||||
self.paste.show()
|
||||
|
||||
class ActivityToolbox(Toolbox):
|
||||
def __init__(self, activity):
|
||||
Toolbox.__init__(self)
|
||||
|
||||
self._activity_toolbar = ActivityToolbar(activity)
|
||||
self.add_toolbar('Activity', self._activity_toolbar)
|
||||
self._activity_toolbar.show()
|
||||
|
||||
def get_activity_toolbar(self):
|
||||
return self._activity_toolbar
|
||||
|
||||
class Activity(Window, gtk.Container):
|
||||
"""Base Activity class that all other Activities derive from."""
|
||||
__gtype_name__ = 'SugarActivity'
|
||||
|
||||
__gsignals__ = {
|
||||
'shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
|
||||
'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([]))
|
||||
}
|
||||
|
||||
__gproperties__ = {
|
||||
'active' : (bool, None, None, False,
|
||||
gobject.PARAM_READWRITE),
|
||||
'max-participants': (int, None, None, 0, 1000, 0,
|
||||
gobject.PARAM_READWRITE)
|
||||
}
|
||||
|
||||
def __init__(self, handle, create_jobject=True):
|
||||
"""Initialise the Activity
|
||||
|
||||
handle -- sugar.activity.activityhandle.ActivityHandle
|
||||
instance providing the activity id and access to the
|
||||
presence service which *may* provide sharing for this
|
||||
application
|
||||
|
||||
create_jobject -- boolean
|
||||
define if it should create a journal object if we are
|
||||
not resuming
|
||||
|
||||
Side effects:
|
||||
|
||||
Sets the gdk screen DPI setting (resolution) to the
|
||||
Sugar screen resolution.
|
||||
|
||||
Connects our "destroy" message to our _destroy_cb
|
||||
method.
|
||||
|
||||
Creates a base gtk.Window within this window.
|
||||
|
||||
Creates an ActivityService (self._bus) servicing
|
||||
this application.
|
||||
"""
|
||||
Window.__init__(self)
|
||||
|
||||
# process titles will only show 15 characters
|
||||
# but they get truncated anyway so if more characters
|
||||
# are supported in the future we will get a better view
|
||||
# of the processes
|
||||
proc_title = "%s <%s>" % (get_bundle_name(), handle.activity_id)
|
||||
util.set_proc_title(proc_title)
|
||||
|
||||
self.connect('realize', self._realize_cb)
|
||||
self.connect('delete-event', self.__delete_event_cb)
|
||||
|
||||
self._active = False
|
||||
self._activity_id = handle.activity_id
|
||||
self._pservice = presenceservice.get_instance()
|
||||
self._shared_activity = None
|
||||
self._share_id = None
|
||||
self._join_id = None
|
||||
self._preview = None
|
||||
self._updating_jobject = False
|
||||
self._closing = False
|
||||
self._deleting = False
|
||||
self._max_participants = 0
|
||||
self._invites_queue = []
|
||||
|
||||
self._bus = ActivityService(self)
|
||||
self._owns_file = False
|
||||
|
||||
share_scope = SCOPE_PRIVATE
|
||||
|
||||
if handle.object_id:
|
||||
self._jobject = datastore.get(handle.object_id)
|
||||
# TODO: Don't create so many objects until we have versioning
|
||||
# support in the datastore
|
||||
#self._jobject.object_id = ''
|
||||
#del self._jobject.metadata['ctime']
|
||||
del self._jobject.metadata['mtime']
|
||||
|
||||
self.set_title(self._jobject.metadata['title'])
|
||||
|
||||
if self._jobject.metadata.has_key('share-scope'):
|
||||
share_scope = self._jobject.metadata['share-scope']
|
||||
|
||||
elif create_jobject:
|
||||
logging.debug('Creating a jobject.')
|
||||
self._jobject = datastore.create()
|
||||
self._jobject.metadata['title'] = _('%s Activity') % get_bundle_name()
|
||||
self.set_title(self._jobject.metadata['title'])
|
||||
self._jobject.metadata['title_set_by_user'] = '0'
|
||||
self._jobject.metadata['activity'] = self.get_bundle_id()
|
||||
self._jobject.metadata['activity_id'] = self.get_id()
|
||||
self._jobject.metadata['keep'] = '0'
|
||||
self._jobject.metadata['preview'] = ''
|
||||
self._jobject.metadata['share-scope'] = SCOPE_PRIVATE
|
||||
|
||||
if self._shared_activity is not None:
|
||||
icon_color = self._shared_activity.props.color
|
||||
else:
|
||||
icon_color = profile.get_color().to_string()
|
||||
|
||||
self._jobject.metadata['icon-color'] = icon_color
|
||||
|
||||
self._jobject.file_path = ''
|
||||
datastore.write(self._jobject,
|
||||
reply_handler=self._internal_jobject_create_cb,
|
||||
error_handler=self._internal_jobject_error_cb)
|
||||
else:
|
||||
self._jobject = None
|
||||
|
||||
# handle activity share/join
|
||||
mesh_instance = self._pservice.get_activity(self._activity_id)
|
||||
logging.debug("*** Act %s, mesh instance %r, scope %s" % (self._activity_id, mesh_instance, share_scope))
|
||||
if mesh_instance:
|
||||
# There's already an instance on the mesh, join it
|
||||
logging.debug("*** Act %s joining existing mesh instance" % self._activity_id)
|
||||
self._shared_activity = mesh_instance
|
||||
self._shared_activity.connect('notify::private',
|
||||
self._privacy_changed_cb)
|
||||
self._join_id = self._shared_activity.connect("joined", self._internal_joined_cb)
|
||||
if not self._shared_activity.props.joined:
|
||||
self._shared_activity.join()
|
||||
else:
|
||||
self._internal_joined_cb(self._shared_activity, True, None)
|
||||
elif share_scope != SCOPE_PRIVATE:
|
||||
logging.debug("*** Act %s no existing mesh instance, but used to be shared, will share" % self._activity_id)
|
||||
# no existing mesh instance, but activity used to be shared, so
|
||||
# restart the share
|
||||
if share_scope == SCOPE_INVITE_ONLY:
|
||||
self.share(private=True)
|
||||
elif share_scope == SCOPE_NEIGHBORHOOD:
|
||||
self.share(private=False)
|
||||
else:
|
||||
logging.debug("Unknown share scope %r" % share_scope)
|
||||
|
||||
def do_set_property(self, pspec, value):
|
||||
if pspec.name == 'active':
|
||||
if self._active != value:
|
||||
self._active = value
|
||||
if not self._active and self._jobject:
|
||||
self.save()
|
||||
elif pspec.name == 'max-participants':
|
||||
self._max_participants = value
|
||||
|
||||
def do_get_property(self, pspec):
|
||||
if pspec.name == 'active':
|
||||
return self._active
|
||||
elif pspec.name == 'max-participants':
|
||||
return self._max_participants
|
||||
|
||||
def get_id(self):
|
||||
return self._activity_id
|
||||
|
||||
def get_bundle_id(self):
|
||||
return _sugarbaseext.get_prgname()
|
||||
|
||||
def set_canvas(self, canvas):
|
||||
Window.set_canvas(self, canvas)
|
||||
canvas.connect('map', self._canvas_map_cb)
|
||||
|
||||
def _canvas_map_cb(self, canvas):
|
||||
if self._jobject and self._jobject.file_path:
|
||||
self.read_file(self._jobject.file_path)
|
||||
|
||||
def _internal_jobject_create_cb(self):
|
||||
pass
|
||||
|
||||
def _internal_jobject_error_cb(self, err):
|
||||
logging.debug("Error creating activity datastore object: %s" % err)
|
||||
|
||||
def get_activity_root(self):
|
||||
"""
|
||||
Return the appropriate location in the fs where to store activity related
|
||||
data that doesn't pertain to the current execution of the activity and
|
||||
thus cannot go into the DataStore.
|
||||
"""
|
||||
if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \
|
||||
os.environ['SUGAR_ACTIVITY_ROOT']:
|
||||
return os.environ['SUGAR_ACTIVITY_ROOT']
|
||||
else:
|
||||
return '/'
|
||||
|
||||
def read_file(self, file_path):
|
||||
"""
|
||||
Subclasses implement this method if they support resuming objects from
|
||||
the journal. 'file_path' is the file to read from.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def write_file(self, file_path):
|
||||
"""
|
||||
Subclasses implement this method if they support saving data to objects
|
||||
in the journal. 'file_path' is the file to write to.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _internal_save_cb(self):
|
||||
logging.debug('Activity._internal_save_cb')
|
||||
self._updating_jobject = False
|
||||
if self._closing:
|
||||
self._cleanup_jobject()
|
||||
self.destroy()
|
||||
|
||||
def _internal_save_error_cb(self, err):
|
||||
logging.debug('Activity._internal_save_error_cb')
|
||||
self._updating_jobject = False
|
||||
if self._closing:
|
||||
self._cleanup_jobject()
|
||||
self.destroy()
|
||||
logging.debug("Error saving activity object to datastore: %s" % err)
|
||||
|
||||
def _cleanup_jobject(self):
|
||||
if self._jobject:
|
||||
if self._owns_file and os.path.isfile(self._jobject.file_path):
|
||||
logging.debug('_cleanup_jobject: removing %r' % self._jobject.file_path)
|
||||
os.remove(self._jobject.file_path)
|
||||
self._owns_file = False
|
||||
self._jobject.destroy()
|
||||
self._jobject = None
|
||||
|
||||
def _get_preview(self):
|
||||
preview_pixbuf = self.get_canvas_screenshot()
|
||||
if preview_pixbuf is None:
|
||||
return None
|
||||
preview_pixbuf = preview_pixbuf.scale_simple(style.zoom(300),
|
||||
style.zoom(225),
|
||||
gtk.gdk.INTERP_BILINEAR)
|
||||
|
||||
# TODO: Find a way of taking a png out of the pixbuf without saving to a temp file.
|
||||
# Impementing gtk.gdk.Pixbuf.save_to_buffer in pygtk would solve this.
|
||||
fd, file_path = tempfile.mkstemp('.png')
|
||||
del fd
|
||||
preview_pixbuf.save(file_path, 'png')
|
||||
f = open(file_path)
|
||||
try:
|
||||
preview_data = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
os.remove(file_path)
|
||||
|
||||
return preview_data
|
||||
|
||||
def _get_buddies(self):
|
||||
if self._shared_activity is not None:
|
||||
buddies = {}
|
||||
for buddy in self._shared_activity.get_joined_buddies():
|
||||
if not buddy.props.owner:
|
||||
buddy_id = sha1(buddy.props.key).hexdigest()
|
||||
buddies[buddy_id] = [buddy.props.nick, buddy.props.color]
|
||||
return buddies
|
||||
else:
|
||||
return {}
|
||||
|
||||
def save(self):
|
||||
"""Request that the activity is saved to the Journal."""
|
||||
|
||||
logging.debug('Activity.save: %r' % self._jobject.object_id)
|
||||
|
||||
if self._updating_jobject:
|
||||
logging.info('Activity.save: still processing a previous request.')
|
||||
return
|
||||
|
||||
buddies_dict = self._get_buddies()
|
||||
if buddies_dict:
|
||||
self.metadata['buddies_id'] = json.write(buddies_dict.keys())
|
||||
self.metadata['buddies'] = json.write(self._get_buddies())
|
||||
|
||||
if self._preview is None:
|
||||
self.metadata['preview'] = ''
|
||||
else:
|
||||
self.metadata['preview'] = dbus.ByteArray(self._preview)
|
||||
|
||||
try:
|
||||
if self._jobject.file_path:
|
||||
self.write_file(self._jobject.file_path)
|
||||
else:
|
||||
file_path = os.path.join(tempfile.gettempdir(), '%i' % time.time())
|
||||
self.write_file(file_path)
|
||||
self._owns_file = True
|
||||
self._jobject.file_path = file_path
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
# Cannot call datastore.write async for creates: https://dev.laptop.org/ticket/3071
|
||||
if self._jobject.object_id is None:
|
||||
datastore.write(self._jobject, transfer_ownership=True)
|
||||
else:
|
||||
self._updating_jobject = True
|
||||
datastore.write(self._jobject,
|
||||
transfer_ownership=True,
|
||||
reply_handler=self._internal_save_cb,
|
||||
error_handler=self._internal_save_error_cb)
|
||||
|
||||
def copy(self):
|
||||
logging.debug('Activity.copy: %r' % self._jobject.object_id)
|
||||
self._preview = self._get_preview()
|
||||
self.save()
|
||||
self._jobject.object_id = None
|
||||
|
||||
def _privacy_changed_cb(self, shared_activity, param_spec):
|
||||
if shared_activity.props.private:
|
||||
self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY
|
||||
else:
|
||||
self._jobject.metadata['share-scope'] = SCOPE_NEIGHBORHOOD
|
||||
|
||||
def _internal_joined_cb(self, activity, success, err):
|
||||
"""Callback when join has finished"""
|
||||
self._shared_activity.disconnect(self._join_id)
|
||||
self._join_id = None
|
||||
if not success:
|
||||
logging.debug("Failed to join activity: %s" % err)
|
||||
return
|
||||
|
||||
self.present()
|
||||
self.emit('joined')
|
||||
self._privacy_changed_cb(self._shared_activity, None)
|
||||
|
||||
def get_shared(self):
|
||||
"""Returns TRUE if the activity is shared on the mesh."""
|
||||
if not self._shared_activity:
|
||||
return False
|
||||
return self._shared_activity.props.joined
|
||||
|
||||
def _internal_share_cb(self, ps, success, activity, err):
|
||||
self._pservice.disconnect(self._share_id)
|
||||
self._share_id = None
|
||||
if not success:
|
||||
logging.debug('Share of activity %s failed: %s.' % (self._activity_id, err))
|
||||
return
|
||||
|
||||
logging.debug('Share of activity %s successful.' % self._activity_id)
|
||||
|
||||
activity.props.name = self._jobject.metadata['title']
|
||||
|
||||
self._shared_activity = activity
|
||||
self._shared_activity.connect('notify::private',
|
||||
self._privacy_changed_cb)
|
||||
self.emit('shared')
|
||||
self._privacy_changed_cb(self._shared_activity, None)
|
||||
|
||||
self._send_invites()
|
||||
|
||||
def _invite_response_cb(self, error):
|
||||
if error:
|
||||
logging.error('Invite failed: %s' % error)
|
||||
|
||||
def _send_invites(self):
|
||||
while self._invites_queue:
|
||||
buddy_key = self._invites_queue.pop()
|
||||
buddy = self._pservice.get_buddy(buddy_key)
|
||||
if buddy:
|
||||
self._shared_activity.invite(buddy, '', self._invite_response_cb)
|
||||
else:
|
||||
logging.error('Cannot invite %s, no such buddy.' % buddy_key)
|
||||
|
||||
def invite(self, buddy_key):
|
||||
self._invites_queue.append(buddy_key)
|
||||
|
||||
if (self._shared_activity is None
|
||||
or not self._shared_activity.props.joined):
|
||||
self.share(True)
|
||||
else:
|
||||
self._send_invites()
|
||||
|
||||
def share(self, private=False):
|
||||
"""Request that the activity be shared on the network.
|
||||
|
||||
private -- bool: True to share by invitation only,
|
||||
False to advertise as shared to everyone.
|
||||
|
||||
Once the activity is shared, its privacy can be changed by setting
|
||||
its 'private' property.
|
||||
"""
|
||||
# FIXME: Make private=True to turn on the by-invitation-only scope
|
||||
if self._shared_activity and self._shared_activity.props.joined:
|
||||
raise RuntimeError("Activity %s already shared." %
|
||||
self._activity_id)
|
||||
verb = private and 'private' or 'public'
|
||||
logging.debug('Requesting %s share of activity %s.' %
|
||||
(verb, self._activity_id))
|
||||
self._share_id = self._pservice.connect("activity-shared",
|
||||
self._internal_share_cb)
|
||||
self._pservice.share_activity(self, private=private)
|
||||
|
||||
def close(self):
|
||||
self._preview = self._get_preview()
|
||||
|
||||
self.save()
|
||||
|
||||
if self._shared_activity:
|
||||
self._shared_activity.leave()
|
||||
|
||||
if self._updating_jobject:
|
||||
self._closing = True
|
||||
else:
|
||||
self.destroy()
|
||||
|
||||
def _realize_cb(self, window):
|
||||
wm.set_bundle_id(window.window, self.get_bundle_id())
|
||||
wm.set_activity_id(window.window, self._activity_id)
|
||||
|
||||
def __delete_event_cb(self, widget, event):
|
||||
self.close()
|
||||
return True
|
||||
|
||||
def get_metadata(self):
|
||||
if self._jobject:
|
||||
return self._jobject.metadata
|
||||
else:
|
||||
return None
|
||||
|
||||
metadata = property(get_metadata, None)
|
||||
|
||||
def get_bundle_name():
|
||||
"""Return the bundle name for the current process' bundle
|
||||
"""
|
||||
return _sugarbaseext.get_application_name()
|
||||
|
||||
def get_bundle_path():
|
||||
"""Return the bundle path for the current process' bundle
|
||||
"""
|
||||
return os.environ['SUGAR_BUNDLE_PATH']
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
"""Shell side object which manages request to start activity"""
|
||||
# Copyright (C) 2006-2007 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
import dbus
|
||||
import gobject
|
||||
import gtk
|
||||
|
||||
from sugar.presence import presenceservice
|
||||
from sugar.activity.activityhandle import ActivityHandle
|
||||
from sugar.activity import registry
|
||||
from sugar.datastore import datastore
|
||||
from sugar import util
|
||||
from sugar import env
|
||||
|
||||
import os
|
||||
|
||||
# #3903 - this constant can be removed and assumed to be 1 when dbus-python
|
||||
# 0.82.3 is the only version used
|
||||
if dbus.version >= (0, 82, 3):
|
||||
DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1
|
||||
else:
|
||||
DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000
|
||||
|
||||
_SHELL_SERVICE = "org.laptop.Shell"
|
||||
_SHELL_PATH = "/org/laptop/Shell"
|
||||
_SHELL_IFACE = "org.laptop.Shell"
|
||||
|
||||
_DS_SERVICE = "org.laptop.sugar.DataStore"
|
||||
_DS_INTERFACE = "org.laptop.sugar.DataStore"
|
||||
_DS_PATH = "/org/laptop/sugar/DataStore"
|
||||
|
||||
_ACTIVITY_FACTORY_INTERFACE = "org.laptop.ActivityFactory"
|
||||
|
||||
_RAINBOW_SERVICE_NAME = "org.laptop.security.Rainbow"
|
||||
_RAINBOW_ACTIVITY_FACTORY_PATH = "/"
|
||||
_RAINBOW_ACTIVITY_FACTORY_INTERFACE = "org.laptop.security.Rainbow"
|
||||
|
||||
def create_activity_id():
|
||||
"""Generate a new, unique ID for this activity"""
|
||||
pservice = presenceservice.get_instance()
|
||||
|
||||
# create a new unique activity ID
|
||||
i = 0
|
||||
act_id = None
|
||||
while i < 10:
|
||||
act_id = util.unique_id()
|
||||
i += 1
|
||||
|
||||
# check through network activities
|
||||
found = False
|
||||
activities = pservice.get_activities()
|
||||
for act in activities:
|
||||
if act_id == act.props.id:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
return act_id
|
||||
raise RuntimeError("Cannot generate unique activity id.")
|
||||
|
||||
def get_environment(activity):
|
||||
environ = os.environ.copy()
|
||||
|
||||
bin_path = os.path.join(activity.path, 'bin')
|
||||
environ['SUGAR_BUNDLE_PATH'] = activity.path
|
||||
environ['PATH'] = bin_path + ':' + environ['PATH']
|
||||
|
||||
return environ
|
||||
|
||||
def get_command(activity, activity_id=None, object_id=None, uri=None):
|
||||
if not activity_id:
|
||||
activity_id = create_activity_id()
|
||||
|
||||
command = activity.command
|
||||
command += ' -b %s' % activity.bundle_id
|
||||
command += ' -a %s' % activity_id
|
||||
|
||||
if object_id is not None:
|
||||
command += ' -o %s' % object_id
|
||||
if uri is not None:
|
||||
command += ' -u %s' % uri
|
||||
|
||||
return command
|
||||
|
||||
def open_log_file(activity, activity_id):
|
||||
for i in range(1, 100):
|
||||
path = env.get_logs_path('%s-%s.log' % (activity.bundle_id, i))
|
||||
if not os.path.exists(path):
|
||||
return open(path, 'w')
|
||||
|
||||
class ActivityCreationHandler(gobject.GObject):
|
||||
"""Sugar-side activity creation interface
|
||||
|
||||
This object uses a dbus method on the ActivityFactory
|
||||
service to create the new activity. It generates
|
||||
GObject events in response to the success/failure of
|
||||
activity startup using callbacks to the service's
|
||||
create call.
|
||||
"""
|
||||
|
||||
def __init__(self, service_name, handle):
|
||||
"""Initialise the handler
|
||||
|
||||
service_name -- the service name of the bundle factory
|
||||
activity_handle -- stores the values which are to
|
||||
be passed to the service to uniquely identify
|
||||
the activity to be created and the sharing
|
||||
service that may or may not be connected with it
|
||||
|
||||
sugar.activity.activityhandle.ActivityHandle instance
|
||||
|
||||
calls the "create" method on the service for this
|
||||
particular activity type and registers the
|
||||
_reply_handler and _error_handler methods on that
|
||||
call's results.
|
||||
|
||||
The specific service which creates new instances of this
|
||||
particular type of activity is created during the activity
|
||||
registration process in shell bundle registry which creates
|
||||
service definition files for each registered bundle type.
|
||||
|
||||
If the file '/etc/olpc-security' exists, then activity launching
|
||||
will be delegated to the prototype 'Rainbow' security service.
|
||||
"""
|
||||
gobject.GObject.__init__(self)
|
||||
self._service_name = service_name
|
||||
self._handle = handle
|
||||
|
||||
bus = dbus.SessionBus()
|
||||
|
||||
bus_object = bus.get_object(_SHELL_SERVICE, _SHELL_PATH)
|
||||
self._shell = dbus.Interface(bus_object, _SHELL_IFACE)
|
||||
|
||||
if handle.activity_id is not None and \
|
||||
handle.object_id is None:
|
||||
datastore = dbus.Interface(
|
||||
bus.get_object(_DS_SERVICE, _DS_PATH), _DS_INTERFACE)
|
||||
datastore.find({ 'activity_id': self._handle.activity_id }, [],
|
||||
reply_handler=self._find_object_reply_handler,
|
||||
error_handler=self._find_object_error_handler)
|
||||
else:
|
||||
self._launch_activity()
|
||||
|
||||
def _launch_activity(self):
|
||||
if self._handle.activity_id != None:
|
||||
self._shell.ActivateActivity(self._handle.activity_id,
|
||||
reply_handler=self._activate_reply_handler,
|
||||
error_handler=self._activate_error_handler)
|
||||
else:
|
||||
self._create_activity()
|
||||
|
||||
def _create_activity(self):
|
||||
if self._handle.activity_id is None:
|
||||
self._handle.activity_id = create_activity_id()
|
||||
|
||||
self._shell.NotifyLaunch(
|
||||
self._service_name, self._handle.activity_id,
|
||||
reply_handler=self._no_reply_handler,
|
||||
error_handler=self._notify_launch_error_handler)
|
||||
|
||||
if not os.path.exists('/etc/olpc-security'):
|
||||
activity_registry = registry.get_registry()
|
||||
activity = activity_registry.get_activity(self._service_name)
|
||||
if activity:
|
||||
env = get_environment(activity)
|
||||
log_file = open_log_file(activity, self._handle.activity_id)
|
||||
command = get_command(activity, self._handle.activity_id,
|
||||
self._handle.object_id,
|
||||
self._handle.uri)
|
||||
process = subprocess.Popen(command, env=env, shell=True,
|
||||
cwd=activity.path, stdout=log_file,
|
||||
stderr=log_file)
|
||||
else:
|
||||
system_bus = dbus.SystemBus()
|
||||
factory = system_bus.get_object(_RAINBOW_SERVICE_NAME,
|
||||
_RAINBOW_ACTIVITY_FACTORY_PATH)
|
||||
stdio_paths = {'stdout': '/logs/stdout', 'stderr': '/logs/stderr'}
|
||||
factory.CreateActivity(
|
||||
self._service_name,
|
||||
self._handle.get_dict(),
|
||||
stdio_paths,
|
||||
timeout=120 * DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND,
|
||||
reply_handler=self._create_reply_handler,
|
||||
error_handler=self._create_error_handler,
|
||||
dbus_interface=_RAINBOW_ACTIVITY_FACTORY_INTERFACE)
|
||||
|
||||
def _no_reply_handler(self, *args):
|
||||
pass
|
||||
|
||||
def _notify_launch_failure_error_handler(self, err):
|
||||
logging.error('Notify launch failure failed %s' % err)
|
||||
|
||||
def _notify_launch_error_handler(self, err):
|
||||
logging.debug('Notify launch failed %s' % err)
|
||||
|
||||
def _activate_reply_handler(self, activated):
|
||||
if not activated:
|
||||
self._create_activity()
|
||||
|
||||
def _activate_error_handler(self, err):
|
||||
logging.error("Activity activation request failed %s" % err)
|
||||
|
||||
def _create_reply_handler(self, xid):
|
||||
logging.debug("Activity created %s (%s)." %
|
||||
(self._handle.activity_id, self._service_name))
|
||||
|
||||
def _create_error_handler(self, err):
|
||||
logging.error("Couldn't create activity %s (%s): %s" %
|
||||
(self._handle.activity_id, self._service_name, err))
|
||||
self._shell.NotifyLaunchFailure(
|
||||
self._handle.activity_id, reply_handler=self._no_reply_handler,
|
||||
error_handler=self._notify_launch_failure_error_handler)
|
||||
|
||||
def _find_object_reply_handler(self, jobjects, count):
|
||||
if count > 0:
|
||||
if count > 1:
|
||||
logging.debug("Multiple objects has the same activity_id.")
|
||||
self._handle.object_id = jobjects[0]['uid']
|
||||
self._create_activity()
|
||||
|
||||
def _find_object_error_handler(self, err):
|
||||
logging.error("Datastore find failed %s" % err)
|
||||
self._create_activity()
|
||||
|
||||
def create(service_name, activity_handle=None):
|
||||
"""Create a new activity from its name."""
|
||||
if not activity_handle:
|
||||
activity_handle = ActivityHandle()
|
||||
return ActivityCreationHandler(service_name, activity_handle)
|
||||
|
||||
def create_with_uri(service_name, uri):
|
||||
"""Create a new activity and pass the uri as handle."""
|
||||
activity_handle = ActivityHandle(uri=uri)
|
||||
return ActivityCreationHandler(service_name, activity_handle)
|
||||
|
||||
def create_with_object_id(service_name, object_id):
|
||||
"""Create a new activity and pass the object id as handle."""
|
||||
activity_handle = ActivityHandle(object_id=object_id)
|
||||
return ActivityCreationHandler(service_name, activity_handle)
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (C) 2006-2007 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
from sugar.presence import presenceservice
|
||||
|
||||
class ActivityHandle(object):
|
||||
"""Data structure storing simple activity metadata"""
|
||||
def __init__(
|
||||
self, activity_id=None, object_id=None, uri=None
|
||||
):
|
||||
"""Initialise the handle from activity_id
|
||||
|
||||
activity_id -- unique id for the activity to be
|
||||
created
|
||||
object_id -- identity of the journal object
|
||||
associated with the activity. It was used by
|
||||
the journal prototype implementation, might
|
||||
change when we do the real one.
|
||||
|
||||
When you resume an activity from the journal
|
||||
the object_id will be passed in. It's optional
|
||||
since new activities does not have an
|
||||
associated object (yet).
|
||||
|
||||
XXX Not clear how this relates to the activity
|
||||
id yet, i.e. not sure we really need both. TBF
|
||||
uri -- URI associated with the activity. Used when
|
||||
opening an external file or resource in the
|
||||
activity, rather than a journal object
|
||||
(downloads stored on the file system for
|
||||
example or web pages)
|
||||
"""
|
||||
self.activity_id = activity_id
|
||||
self.object_id = object_id
|
||||
self.uri = uri
|
||||
|
||||
def get_dict(self):
|
||||
"""Retrieve our settings as a dictionary"""
|
||||
result = { 'activity_id' : self.activity_id }
|
||||
if self.object_id:
|
||||
result['object_id'] = self.object_id
|
||||
if self.uri:
|
||||
result['uri'] = self.uri
|
||||
|
||||
return result
|
||||
|
||||
def create_from_dict(handle_dict):
|
||||
"""Create a handle from a dictionary of parameters"""
|
||||
result = ActivityHandle(
|
||||
handle_dict['activity_id'],
|
||||
object_id = handle_dict.get('object_id'),
|
||||
uri = handle_dict.get('uri'),
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2006-2007 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
import logging
|
||||
|
||||
import dbus
|
||||
import dbus.service
|
||||
|
||||
_ACTIVITY_SERVICE_NAME = "org.laptop.Activity"
|
||||
_ACTIVITY_SERVICE_PATH = "/org/laptop/Activity"
|
||||
_ACTIVITY_INTERFACE = "org.laptop.Activity"
|
||||
|
||||
class ActivityService(dbus.service.Object):
|
||||
"""Base dbus service object that each Activity uses to export dbus methods.
|
||||
|
||||
The dbus service is separate from the actual Activity object so that we can
|
||||
tightly control what stuff passes through the dbus python bindings."""
|
||||
|
||||
def __init__(self, activity):
|
||||
"""Initialise the service for the given activity
|
||||
|
||||
activity -- sugar.activity.activity.Activity instance
|
||||
|
||||
Creates dbus services that use the instance's activity_id
|
||||
as discriminants among all active services
|
||||
of this type. That is, the services are all available
|
||||
as names/paths derived from the instance's activity_id.
|
||||
|
||||
The various methods exposed on dbus are just forwarded
|
||||
to the client Activity object's equally-named methods.
|
||||
"""
|
||||
activity.realize()
|
||||
|
||||
activity_id = activity.get_id()
|
||||
service_name = _ACTIVITY_SERVICE_NAME + activity_id
|
||||
object_path = _ACTIVITY_SERVICE_PATH + "/" + activity_id
|
||||
|
||||
bus = dbus.SessionBus()
|
||||
bus_name = dbus.service.BusName(service_name, bus=bus)
|
||||
dbus.service.Object.__init__(self, bus_name, object_path)
|
||||
|
||||
self._activity = activity
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE)
|
||||
def SetActive(self, active):
|
||||
logging.debug('ActivityService.set_active: %s.' % active)
|
||||
self._activity.props.active = active
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE)
|
||||
def Invite(self, buddy_key):
|
||||
self._activity.invite(buddy_key)
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
# Copyright (C) 2006-2007 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
import gettext
|
||||
|
||||
from sugar import env
|
||||
from sugar.bundle.activitybundle import ActivityBundle
|
||||
|
||||
class _SvnFileList(list):
|
||||
def __init__(self):
|
||||
f = os.popen('svn list -R')
|
||||
for line in f.readlines():
|
||||
filename = line.strip()
|
||||
if os.path.isfile(filename):
|
||||
self.append(filename)
|
||||
f.close()
|
||||
|
||||
class _GitFileList(list):
|
||||
def __init__(self):
|
||||
f = os.popen('git-ls-files')
|
||||
for line in f.readlines():
|
||||
filename = line.strip()
|
||||
if not filename.startswith('.'):
|
||||
self.append(filename)
|
||||
f.close()
|
||||
|
||||
class _DefaultFileList(list):
|
||||
def __init__(self):
|
||||
for name in os.listdir('activity'):
|
||||
if name.endswith('.svg'):
|
||||
self.append(os.path.join('activity', name))
|
||||
|
||||
self.append('activity/activity.info')
|
||||
|
||||
if os.path.isfile(_get_source_path('NEWS')):
|
||||
self.append('NEWS')
|
||||
|
||||
class _ManifestFileList(_DefaultFileList):
|
||||
def __init__(self, manifest):
|
||||
_DefaultFileList.__init__(self)
|
||||
self.append(manifest)
|
||||
|
||||
f = open(manifest,'r')
|
||||
for line in f.readlines():
|
||||
stripped_line = line.strip()
|
||||
if stripped_line and not stripped_line in self:
|
||||
self.append(stripped_line)
|
||||
f.close()
|
||||
|
||||
def _extract_bundle(source_file, dest_dir):
|
||||
if not os.path.exists(dest_dir):
|
||||
os.mkdir(dest_dir)
|
||||
|
||||
zf = zipfile.ZipFile(source_file)
|
||||
|
||||
for i, name in enumerate(zf.namelist()):
|
||||
path = os.path.join(dest_dir, name)
|
||||
|
||||
if not os.path.exists(os.path.dirname(path)):
|
||||
os.makedirs(os.path.dirname(path))
|
||||
|
||||
outfile = open(path, 'wb')
|
||||
outfile.write(zf.read(name))
|
||||
outfile.flush()
|
||||
outfile.close()
|
||||
|
||||
def _get_source_path(path=None):
|
||||
if path:
|
||||
return os.path.join(os.getcwd(), path)
|
||||
else:
|
||||
return os.getcwd()
|
||||
|
||||
def _get_bundle_dir():
|
||||
bundle_name = os.path.basename(_get_source_path())
|
||||
return bundle_name + '.activity'
|
||||
|
||||
def _get_install_dir(prefix):
|
||||
return os.path.join(prefix, 'share/activities')
|
||||
|
||||
def _get_package_name(bundle_name):
|
||||
bundle = ActivityBundle(_get_source_path())
|
||||
zipname = '%s-%d.xo' % (bundle_name, bundle.get_activity_version())
|
||||
return zipname
|
||||
|
||||
def _delete_backups(arg, dirname, names):
|
||||
for name in names:
|
||||
if name.endswith('~') or name.endswith('pyc'):
|
||||
os.remove(os.path.join(dirname, name))
|
||||
|
||||
def _get_bundle_id():
|
||||
bundle = ActivityBundle(_get_source_path())
|
||||
return bundle.get_bundle_id()
|
||||
|
||||
def cmd_help():
|
||||
print 'Usage: \n\
|
||||
setup.py dev - setup for development \n\
|
||||
setup.py dist - create a bundle package \n\
|
||||
setup.py install [dirname] - install the bundle \n\
|
||||
setup.py uninstall [dirname] - uninstall the bundle \n\
|
||||
setup.py genpot - generate the gettext pot file \n\
|
||||
setup.py genl10n - generate localization files \n\
|
||||
setup.py clean - clean the directory \n\
|
||||
setup.py release - do a new release of the bundle \n\
|
||||
setup.py help - print this message \n\
|
||||
'
|
||||
|
||||
def cmd_dev():
|
||||
bundle_path = env.get_user_activities_path()
|
||||
if not os.path.isdir(bundle_path):
|
||||
os.mkdir(bundle_path)
|
||||
bundle_path = os.path.join(bundle_path, _get_bundle_dir())
|
||||
try:
|
||||
os.symlink(_get_source_path(), bundle_path)
|
||||
except OSError:
|
||||
if os.path.islink(bundle_path):
|
||||
print 'ERROR - The bundle has been already setup for development.'
|
||||
else:
|
||||
print 'ERROR - A bundle with the same name is already installed.'
|
||||
|
||||
def _get_file_list(manifest):
|
||||
if os.path.isfile(manifest):
|
||||
return _ManifestFileList(manifest)
|
||||
elif os.path.isdir('.git'):
|
||||
return _GitFileList()
|
||||
elif os.path.isdir('.svn'):
|
||||
return _SvnFileList()
|
||||
else:
|
||||
return _DefaultFileList()
|
||||
|
||||
def _get_po_list(manifest):
|
||||
file_list = {}
|
||||
|
||||
po_regex = re.compile("po/(.*)\.po$")
|
||||
for file_name in _get_file_list(manifest):
|
||||
match = po_regex.match(file_name)
|
||||
if match:
|
||||
file_list[match.group(1)] = file_name
|
||||
|
||||
return file_list
|
||||
|
||||
def _get_l10n_list(manifest):
|
||||
l10n_list = []
|
||||
|
||||
for lang in _get_po_list(manifest).keys():
|
||||
filename = _get_bundle_id() + '.mo'
|
||||
l10n_list.append(os.path.join('locale', lang, 'LC_MESSAGES', filename))
|
||||
l10n_list.append(os.path.join('locale', lang, 'activity.linfo'))
|
||||
|
||||
return l10n_list
|
||||
|
||||
def _get_activity_name():
|
||||
info_path = os.path.join(_get_source_path(), 'activity', 'activity.info')
|
||||
f = open(info_path,'r')
|
||||
info = f.read()
|
||||
f.close()
|
||||
match = re.search('^name\s*=\s*(.*)$', info, flags = re.MULTILINE)
|
||||
return match.group(1)
|
||||
|
||||
def cmd_dist(bundle_name, manifest):
|
||||
cmd_genl10n(bundle_name, manifest)
|
||||
file_list = _get_file_list(manifest)
|
||||
|
||||
zipname = _get_package_name(bundle_name)
|
||||
bundle_zip = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED)
|
||||
base_dir = bundle_name + '.activity'
|
||||
|
||||
for filename in file_list:
|
||||
bundle_zip.write(filename, os.path.join(base_dir, filename))
|
||||
|
||||
for filename in _get_l10n_list(manifest):
|
||||
bundle_zip.write(filename, os.path.join(base_dir, filename))
|
||||
|
||||
bundle_zip.close()
|
||||
|
||||
def cmd_install(bundle_name, manifest, prefix):
|
||||
cmd_dist(bundle_name, manifest)
|
||||
cmd_uninstall(prefix)
|
||||
|
||||
_extract_bundle(_get_package_name(bundle_name),
|
||||
_get_install_dir(prefix))
|
||||
|
||||
def cmd_uninstall(prefix):
|
||||
path = os.path.join(_get_install_dir(prefix), _get_bundle_dir())
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
def cmd_genpot(bundle_name, manifest):
|
||||
po_path = os.path.join(_get_source_path(), 'po')
|
||||
if not os.path.isdir(po_path):
|
||||
os.mkdir(po_path)
|
||||
|
||||
python_files = []
|
||||
file_list = _get_file_list(manifest)
|
||||
for file_name in file_list:
|
||||
if file_name.endswith('.py'):
|
||||
python_files.append(file_name)
|
||||
|
||||
# First write out a stub .pot file containing just the translated
|
||||
# activity name, then have xgettext merge the rest of the
|
||||
# translations into that. (We can't just append the activity name
|
||||
# to the end of the .pot file afterwards, because that might
|
||||
# create a duplicate msgid.)
|
||||
pot_file = os.path.join('po', '%s.pot' % bundle_name)
|
||||
activity_name = _get_activity_name()
|
||||
escaped_name = re.sub('([\\\\"])', '\\\\\\1', activity_name)
|
||||
f = open(pot_file, 'w')
|
||||
f.write('#: activity/activity.info:2\n')
|
||||
f.write('msgid "%s"\n' % escaped_name)
|
||||
f.write('msgstr ""\n')
|
||||
f.close()
|
||||
|
||||
args = [ 'xgettext', '--join-existing', '--language=Python',
|
||||
'--keyword=_', '--add-comments=TRANS:', '--output=%s' % pot_file ]
|
||||
|
||||
args += python_files
|
||||
retcode = subprocess.call(args)
|
||||
if retcode:
|
||||
print 'ERROR - xgettext failed with return code %i.' % retcode
|
||||
|
||||
for file_name in _get_po_list(manifest).values():
|
||||
args = [ 'msgmerge', '-U', file_name, pot_file ]
|
||||
retcode = subprocess.call(args)
|
||||
if retcode:
|
||||
print 'ERROR - msgmerge failed with return code %i.' % retcode
|
||||
|
||||
def cmd_genl10n(bundle_name, manifest):
|
||||
source_path = _get_source_path()
|
||||
activity_name = _get_activity_name()
|
||||
|
||||
po_list = _get_po_list(manifest)
|
||||
for lang in po_list.keys():
|
||||
file_name = po_list[lang]
|
||||
|
||||
localedir = os.path.join(source_path, 'locale', lang)
|
||||
mo_path = os.path.join(localedir, 'LC_MESSAGES')
|
||||
if not os.path.isdir(mo_path):
|
||||
os.makedirs(mo_path)
|
||||
|
||||
mo_file = os.path.join(mo_path, "%s.mo" % _get_bundle_id())
|
||||
args = ["msgfmt", "--output-file=%s" % mo_file, file_name]
|
||||
retcode = subprocess.call(args)
|
||||
if retcode:
|
||||
print 'ERROR - msgfmt failed with return code %i.' % retcode
|
||||
|
||||
cat = gettext.GNUTranslations(open(mo_file, 'r'))
|
||||
translated_name = cat.gettext(activity_name)
|
||||
linfo_file = os.path.join(localedir, 'activity.linfo')
|
||||
f = open(linfo_file, 'w')
|
||||
f.write('[Activity]\nname = %s\n' % translated_name)
|
||||
f.close()
|
||||
|
||||
def cmd_release(bundle_name, manifest):
|
||||
if not os.path.isdir('.git'):
|
||||
print 'ERROR - this command works only for git repositories'
|
||||
|
||||
retcode = subprocess.call(['git', 'pull'])
|
||||
if retcode:
|
||||
print 'ERROR - cannot pull from git'
|
||||
|
||||
print 'Bumping activity version...'
|
||||
|
||||
info_path = os.path.join(_get_source_path(), 'activity', 'activity.info')
|
||||
f = open(info_path,'r')
|
||||
info = f.read()
|
||||
f.close()
|
||||
|
||||
exp = re.compile('activity_version\s?=\s?([0-9]*)')
|
||||
match = re.search(exp, info)
|
||||
version = int(match.group(1)) + 1
|
||||
info = re.sub(exp, 'activity_version = %d' % version, info)
|
||||
|
||||
f = open(info_path, 'w')
|
||||
f.write(info)
|
||||
f.close()
|
||||
|
||||
news_path = os.path.join(_get_source_path(), 'NEWS')
|
||||
|
||||
if os.environ.has_key('SUGAR_NEWS'):
|
||||
print 'Update NEWS.sugar...'
|
||||
|
||||
sugar_news_path = os.environ['SUGAR_NEWS']
|
||||
if os.path.isfile(sugar_news_path):
|
||||
f = open(sugar_news_path,'r')
|
||||
sugar_news = f.read()
|
||||
f.close()
|
||||
else:
|
||||
sugar_news = ''
|
||||
|
||||
sugar_news += '%s - %d\n\n' % (bundle_name, version)
|
||||
|
||||
f = open(news_path,'r')
|
||||
for line in f.readlines():
|
||||
if len(line.strip()) > 0:
|
||||
sugar_news += line
|
||||
else:
|
||||
break
|
||||
f.close()
|
||||
|
||||
sugar_news += '\n'
|
||||
|
||||
f = open(sugar_news_path, 'w')
|
||||
f.write(sugar_news)
|
||||
f.close()
|
||||
|
||||
print 'Update NEWS...'
|
||||
|
||||
f = open(news_path,'r')
|
||||
news = f.read()
|
||||
f.close()
|
||||
|
||||
news = '%d\n\n' % version + news
|
||||
|
||||
f = open(news_path, 'w')
|
||||
f.write(news)
|
||||
f.close()
|
||||
|
||||
print 'Committing to git...'
|
||||
|
||||
changelog = 'Release version %d.' % version
|
||||
retcode = subprocess.call(['git', 'commit', '-a', '-m % s' % changelog])
|
||||
if retcode:
|
||||
print 'ERROR - cannot commit to git'
|
||||
|
||||
retcode = subprocess.call(['git', 'push'])
|
||||
if retcode:
|
||||
print 'ERROR - cannot push to git'
|
||||
|
||||
print 'Creating the bundle...'
|
||||
cmd_dist(bundle_name, manifest)
|
||||
|
||||
if os.environ.has_key('ACTIVITIES_REPOSITORY'):
|
||||
print 'Uploading to the activities repository...'
|
||||
repo = os.environ['ACTIVITIES_REPOSITORY']
|
||||
|
||||
server, path = repo.split(':')
|
||||
retcode = subprocess.call(['ssh', server, 'rm',
|
||||
'%s/%s*' % (path, bundle_name)])
|
||||
if retcode:
|
||||
print 'ERROR - cannot remove old bundles from the repository.'
|
||||
|
||||
bundle_path = os.path.join(_get_source_path(),
|
||||
_get_package_name(bundle_name))
|
||||
retcode = subprocess.call(['scp', bundle_path, repo])
|
||||
if retcode:
|
||||
print 'ERROR - cannot upload the bundle to the repository.'
|
||||
|
||||
print 'Done.'
|
||||
|
||||
def cmd_clean():
|
||||
os.path.walk('.', _delete_backups, None)
|
||||
|
||||
def sanity_check():
|
||||
if not os.path.isfile(_get_source_path('NEWS')):
|
||||
print 'WARNING: NEWS file is missing.'
|
||||
|
||||
def start(bundle_name, manifest='MANIFEST'):
|
||||
sanity_check()
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
cmd_help()
|
||||
elif sys.argv[1] == 'build':
|
||||
pass
|
||||
elif sys.argv[1] == 'dev':
|
||||
cmd_dev()
|
||||
elif sys.argv[1] == 'dist':
|
||||
cmd_dist(bundle_name, manifest)
|
||||
elif sys.argv[1] == 'install' and len(sys.argv) == 3:
|
||||
cmd_install(bundle_name, manifest, sys.argv[2])
|
||||
elif sys.argv[1] == 'uninstall' and len(sys.argv) == 3:
|
||||
cmd_uninstall(sys.argv[2])
|
||||
elif sys.argv[1] == 'genpot':
|
||||
cmd_genpot(bundle_name, manifest)
|
||||
elif sys.argv[1] == 'genl10n':
|
||||
cmd_genl10n(bundle_name, manifest)
|
||||
elif sys.argv[1] == 'clean':
|
||||
cmd_clean()
|
||||
elif sys.argv[1] == 'release':
|
||||
cmd_release(bundle_name, manifest)
|
||||
else:
|
||||
cmd_help()
|
||||
|
||||
if __name__ == '__main__':
|
||||
start()
|
||||
@@ -0,0 +1,156 @@
|
||||
# Copyright (C) 2006-2007 Red Hat, Inc.
|
||||
# Copyright (C) 2007 One Laptop Per Child
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
# Boston, MA 02111-1307, USA.
|
||||
|
||||
import logging
|
||||
|
||||
import dbus
|
||||
import gobject
|
||||
|
||||
_ACTIVITY_REGISTRY_SERVICE_NAME = 'org.laptop.ActivityRegistry'
|
||||
_ACTIVITY_REGISTRY_IFACE = 'org.laptop.ActivityRegistry'
|
||||
_ACTIVITY_REGISTRY_PATH = '/org/laptop/ActivityRegistry'
|
||||
|
||||
def _activity_info_from_dict(info_dict):
|
||||
if not info_dict:
|
||||
return None
|
||||
return ActivityInfo(info_dict['name'], info_dict['icon'],
|
||||
info_dict['bundle_id'], info_dict['path'],
|
||||
info_dict['show_launcher'], info_dict['command'])
|
||||
|
||||
class ActivityInfo(object):
|
||||
def __init__(self, name, icon, bundle_id,
|
||||
path, show_launcher, command):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.bundle_id = bundle_id
|
||||
self.path = path
|
||||
self.command = command
|
||||
self.show_launcher = show_launcher
|
||||
|
||||
class ActivityRegistry(gobject.GObject):
|
||||
__gsignals__ = {
|
||||
'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
||||
([gobject.TYPE_PYOBJECT])),
|
||||
'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
||||
([gobject.TYPE_PYOBJECT]))
|
||||
}
|
||||
def __init__(self):
|
||||
gobject.GObject.__init__(self)
|
||||
|
||||
bus = dbus.SessionBus()
|
||||
|
||||
# NOTE: We need to follow_name_owner_changes here
|
||||
# because we can not connect to a signal unless
|
||||
# we follow the changes or we start the service
|
||||
# before we connect. Starting the service here
|
||||
# causes a major bottleneck during startup
|
||||
bus_object = bus.get_object(_ACTIVITY_REGISTRY_SERVICE_NAME,
|
||||
_ACTIVITY_REGISTRY_PATH,
|
||||
follow_name_owner_changes = True)
|
||||
self._registry = dbus.Interface(bus_object, _ACTIVITY_REGISTRY_IFACE)
|
||||
self._registry.connect_to_signal('ActivityAdded', self._activity_added_cb)
|
||||
self._registry.connect_to_signal('ActivityRemoved', self._activity_removed_cb)
|
||||
|
||||
# Two caches fo saving some travel across dbus.
|
||||
self._service_name_to_activity_info = {}
|
||||
self._mime_type_to_activities = {}
|
||||
|
||||
def _convert_info_list(self, info_list):
|
||||
result = []
|
||||
|
||||
for info_dict in info_list:
|
||||
result.append(_activity_info_from_dict(info_dict))
|
||||
|
||||
return result
|
||||
|
||||
def get_activities(self):
|
||||
info_list = self._registry.GetActivities()
|
||||
return self._convert_info_list(info_list)
|
||||
|
||||
def _get_activities_cb(self, reply_handler, info_list):
|
||||
result = []
|
||||
i = 0
|
||||
for info_dict in info_list:
|
||||
result.append(_activity_info_from_dict(info_dict))
|
||||
|
||||
reply_handler(result)
|
||||
|
||||
def _get_activities_error_cb(self, error_handler, e):
|
||||
if error_handler:
|
||||
error_handler(e)
|
||||
else:
|
||||
logging.error('Error getting activities async: %s' % str(e))
|
||||
|
||||
def get_activities_async(self, reply_handler=None, error_handler=None):
|
||||
if not reply_handler:
|
||||
logging.error('Function get_activities_async called without a reply handler. Can not run.')
|
||||
return
|
||||
|
||||
self._registry.GetActivities(
|
||||
reply_handler=lambda info_list:self._get_activities_cb(reply_handler, info_list),
|
||||
error_handler=lambda e:self._get_activities_error_cb(error_handler, e))
|
||||
|
||||
def get_activity(self, service_name):
|
||||
if self._service_name_to_activity_info.has_key(service_name):
|
||||
return self._service_name_to_activity_info[service_name]
|
||||
|
||||
info_dict = self._registry.GetActivity(service_name)
|
||||
activity_info = _activity_info_from_dict(info_dict)
|
||||
|
||||
self._service_name_to_activity_info[service_name] = activity_info
|
||||
return activity_info
|
||||
|
||||
def find_activity(self, name):
|
||||
info_list = self._registry.FindActivity(name)
|
||||
return self._convert_info_list(info_list)
|
||||
|
||||
def get_activities_for_type(self, mime_type):
|
||||
if self._mime_type_to_activities.has_key(mime_type):
|
||||
return self._mime_type_to_activities[mime_type]
|
||||
|
||||
info_list = self._registry.GetActivitiesForType(mime_type)
|
||||
activities = self._convert_info_list(info_list)
|
||||
|
||||
self._mime_type_to_activities[mime_type] = activities
|
||||
return activities
|
||||
|
||||
def add_bundle(self, bundle_path):
|
||||
return self._registry.AddBundle(bundle_path)
|
||||
|
||||
def _activity_added_cb(self, info_dict):
|
||||
logging.debug('ActivityRegistry._activity_added_cb: flushing caches')
|
||||
self._service_name_to_activity_info.clear()
|
||||
self._mime_type_to_activities.clear()
|
||||
self.emit('activity-added', _activity_info_from_dict(info_dict))
|
||||
|
||||
def remove_bundle(self, bundle_path):
|
||||
return self._registry.RemoveBundle(bundle_path)
|
||||
|
||||
def _activity_removed_cb(self, info_dict):
|
||||
logging.debug('ActivityRegistry._activity_removed_cb: flushing caches')
|
||||
self._service_name_to_activity_info.clear()
|
||||
self._mime_type_to_activities.clear()
|
||||
self.emit('activity-removed', _activity_info_from_dict(info_dict))
|
||||
|
||||
_registry = None
|
||||
|
||||
def get_registry():
|
||||
global _registry
|
||||
if not _registry:
|
||||
_registry = ActivityRegistry()
|
||||
return _registry
|
||||
Reference in New Issue
Block a user