Low-level Activity API
Sugar activities are usually written in Python using the Python Activity API. This page documents the underlying mechanism that all activities need to conform to. Activities can be written in any language, as long as it can connect to DBus and provide an X11 interface. The discussion below tries to be language-agnostic.
This documentation effort was started by Bert while implementing the Squeak-based Etoys activity. Please fill in missing pieces and correct mistakes!
A library for creating XO activities using C++ and Qt is under development by Ark Linux. See the Qt page for details.
It is possible to use Mono to write an xo activity, too. See Mono page for further details.
Overview
An Activity is basically a regular X11 program which communicates with the special Sugar services via DBus.
The Activity Bundle specifies an executable. For each activity instance, that executable is run with arguments specifying the bundle id (taken from the bundle) and activity id (generated by Sugar). The instance opens an X window, putting these ids into window properties. It also needs to provide a DBus service to receive messages from Sugar. An activity must retrieve and store its state in the datastore, implement sharing on the mesh network, and be security compliant.
Activity Instance
When the activity instance is executed, the current working directory will be set to the bundle directory (e.g., ~/Activities/MyActivity.activity) so resource files can be accessed using relative pathes. Also, its "bin" subdirectory is added to the PATH. The following arguments are passed to the executable:
- -b, --bundle-id
- Identifier of the activity bundle
- -a, --activity-id
- Unique identifier of the activity instance.
- -o, --object-id
- (optional) Identity of the journal object associated with the activity instance. When you resume an activity from the journal the object id will be passed in (see datastore).
- -u, --uri
- (optional) 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).
Environment Variables
Some environment variables are setup before the activity is launched:
SUGAR_ACTIVITY_ROOT
Writable space for the activity, see security. Activities are prohibited from writing anywhere else in the file system.
SUGAR_BUNDLE_PATH
Path to the current activity bundle (e.g., /sugar/share/activities/MyActivity.activity or /home/olpc/Activities/MyActivity.activity). This is also the current working directory when the activity is started, so relative paths can be used to access files inside the bundle, rather than constructing absolute paths using this variable.
X Properties
The activity instance needs to set some properties on its top-level window, before the window is shown on the screen (see #5271):
_SUGAR_BUNDLE_ID
The bundle id (e.g., my.organization.MyActivity) of type STRING.
_SUGAR_ACTIVITY_ID
The activity id (e.g., 6f7f3acacca87886332f50bdd522d805f0abbf1f) of type STRING.
- The above properties need to be on the window before it pops up. This is easy when programming with raw libX11, but often difficult with high-level toolkits. E.g., in GTK you can use the "realize" event. The toolkit is likely to create and pop up the window in one operation, so you don't get a chance to set the properties. A workable solution is to piggyback on a function within the toolkit. For example, you can implement XChangeProperty in your activity. Using dlsym() with the RTLD_NEXT flag, you can obtain a function pointer to the normal XChangeProperty function in libX11. Your implementation normally just calls that. The first time your implementation is called though, it also sets up the sugar-specific properties. Essentially you are supplying a callback function to a toolkit that was never intended to call one. Once #5271 is settled, this hack is not necessary anymore
Also, some Window Manager hints need to be set:
_NET_WM_NAME
should be set to the current activity title. It usually corresponds to the title which is displayed in the journal and advertised on the network for shared activities. See Freedesktop specification.
_NET_WM_PID
must be set to the activity's process id so the shell can associate memory usage with an activity. See Freedesktop specification.
DBus Methods
An activity instance needs to create a DBus service:
Service name: org.laptop.Activity6f7f3acacca87886332f50bdd522d805f0abbf1f Object path: /org/laptop/Activity/6f7f3acacca87886332f50bdd522d805f0abbf1f Interface: org.laptop.Activity
(where 6f7f3acacca87886332f50bdd522d805f0abbf1f is the activity id as passed on the cmd line)
It must support the following methods:
org.laptop.Activity.SetActive(b: active)
Activate or passivate an activity. Passive activities must release resources like sound, camera etc.
org.laptop.Activity.TakeScreenshot()
Hint to the activity that it might be a good idea to take a preview screenshot now, because its window is going to be obscured as soon as the method returns (e.g., by the frame). Should not take more than 0.1 seconds.
org.laptop.Activity.Invite(s: buddy_key)
If not yet shared, share this activity privately because the user chose "invite" from the mesh view. Then, invite the buddy (see below).
Datastore
An Activity instance must store its complete state in the central datastore so it appears in the Journal and can be resumed later. It needs to connect to the datastore service:
Service name: org.laptop.sugar.DataStore Object path: /org/laptop/sugar/DataStore Interface: org.laptop.sugar.DataStore
Meta Data
An item in the datastore is referenced by an object_id, it has a dictionary of properties, and possibly a file. The properties have String keys but Variant values. Here are a few properties:
'activity': 'my.organization.MyActivity' # bundle id 'activity_id': '6f7f3acacca87886332f50bdd522d805f0abbf1f' 'title': 'My new project' 'title_set_by_user': '0' # '1' if not default title 'keep': '0' # '1' if marked as "favorite" (star) 'ctime': '1972-05-12T18:41:08' # created 'mtime': '2007-06-16T03:42:33' # modified 'timestamp': 1192715145 # modified, in seconds since the UNIX epoch 'preview': ByteArray(png file data, 300x225 px) 'icon-color': '#ff0000,#ffff00' # owner buddy or shared activity color 'mime_type': 'application/x-my-activity' 'share-scope': # if shared 'buddies': '{}' # buddies in a shared activity as JSON 'summary:text': 'text I want to be indexed' # properties with key ending in ":text" will be searched in fulltext search
Due to bug #4662 only some known properties are retained! The list is at the bottom of datastore/model.py
And custom properties must have String values for now (bug #5134).
Keeping and Resuming
To create an item in the datastore, call create():
object_id = datastore.create(properties, filename)
If filename is not empty, the file will be copied to the datastore. The activity should delete the file once the call completes. The returned id will be a string like '4543af91-7be9-404e-b2f1-3e27cb15a15d'.
To update an item use update():
datastore.update(object_id, properties, filename)
Again, if a filename was given, it should be deleted when the call returns.
To retrieve an object's properties and file:
properties = datastore.get_properties(object_id) filename = datastore.get_filename(object_id)
The returned temp file should be deleted by the activity as soon as possible, latest when the activity quits.
Querying
Activities may query the datastore:
(results,count) = datastore.find(query)
It returns the results as array of properties and a count of matching items (the array may have fewer items if the query was limited). In addition to the usual metadata items, the properties will include the object id at key 'uid', the mountpoint of the item at key 'mountpoint', and possibly a 'filename' if requested.
The query can be a:
- string: fulltext search
- the given string is searched in all text properties
- dictionary: structured query
- the key-value pairs in the dictionary specify the value (or array of values, or dictionary specifying range) for a specific property, e.g.:
- 'title' = 'First Project'
- 'mime_type' = ['image/png', 'image/jpeg']
- 'mtime' = {'start' = '2007-07-01T00:00:00', 'end' = '2007-08-01T00:00:00'}
- also, there are a few specific keys to adjust the query:
- 'query': fulltext search term
- 'order_by': key (or array of keys) to order results by, to reverse order use '-key'
- 'limit', 'offset': return only limit results starting at offset
- 'mountpoints': array of mountpoint ids to search (or all if not specified)
- 'include_files': if true, generate files as if get_filename() had been called for each item. In results, a property 'filename' will be added.
- the key-value pairs in the dictionary specify the value (or array of values, or dictionary specifying range) for a specific property, e.g.:
You can also retrieve an array of unique values for a field:
values = datastore.get_uniquevaluesfor(property, query)
Note that currently (2007-07-25) the query is ignored in this call, it looks for all values in all entries.
Mount Points
Devices are represented as mount points in the datastore. If no mountpoint is explicitely specified, the main datastore (Journal) is used.
mounts = datastore.mounts()
Returns an array of mount point descriptiors where each descriptor is a dictionary containing at least the following keys:
- 'id': the id used to refer explicitly to the mount point
- 'title': Human readable identifier for the mountpoint
- 'uri': The uri which triggered the mount
Mount points can be specified when creating an object (using a 'mountpoint' key and id value in the properties), and when querying the datastore (by adding a 'mountpoints' query option).
Security
Activities will be isolated and will not have the same permissions as you would expect in a non-restricted Linux environment (see Bitfrost and Rainbow).
File Access
Writable Directories
All writing to the file system is restricted to subdirectories of the path given in the SUGAR_ACTIVITY_ROOT environment variable. This directory has three (virtual) subdirectories with different policies:
$SUGAR_ACTIVITY_ROOT/data # in Flash, persistent, shared between instances, group-writable $SUGAR_ACTIVITY_ROOT/instance # in Flash, volatile, unique per instance $SUGAR_ACTIVITY_ROOT/tmp # in RAM, small and fast, volatile, unique per instance
The "data/" directory is meant for persistent activity data such as configuration files, make sure files in there are group readable and writable (see below). The "instance/" directory is used for transfer to and from the datastore (see above). The "tmp/" directory may be as small as 1 MB and is RAM backed.
- FIXME: How volatile is volatile? When an activity is restarted via the journal, does that count as the same instance? (does "instance" survive?)
- I think it is deleted as soon as the instance is stopped, and will not be available on resume. It's basically a larger "tmp", "scratch" might be a more appropriate name. Some Rainbow developer should confirm this. -- Bert 11:12, 26 November 2007 (EST)
- In the current design, the "tmp/" dir disappears as soon as all children of the activity's first process die. --Michael Stone 17:41, 29 November 2007 (EST)
- I think it is deleted as soon as the instance is stopped, and will not be available on resume. It's basically a larger "tmp", "scratch" might be a more appropriate name. Some Rainbow developer should confirm this. -- Bert 11:12, 26 November 2007 (EST)
- FIXME: How persistent is persistent? When an activity is upgraded, does that count as the same activity? (does "data" survive?)
- I'm pretty sure data will survive an upgrade. Again, some Rainbow developer should confirm. -- Bert 11:12, 26 November 2007 (EST)
- As of today, it does not survive an upgrade but this is considered to be a bug (#5033). --Michael Stone 17:41, 29 November 2007 (EST)
- I'm pretty sure data will survive an upgrade. Again, some Rainbow developer should confirm. -- Bert 11:12, 26 November 2007 (EST)
Users and Groups
Each activity instance is run with a unique user id. All instance of the same activity get the same unique group id. This means files to be shared for all instances of an activity must be made group-accessible.
to be detailed
Concurrency
Multiple instances of an activity may communicate with one another through their shared 'data' directory; however, since each instance runs as a different user, some care must be taken (#5476) when sending messages to other activities through this shared medium.
Signing
An activity will have to be cryptographically signed to allow secure activity upgrades once they are on the machines. Tools for this will be provided soon.
to be detailed
Permissions Declarations
Permission declarations will enumerate which special permissions (camera access? microphone access? non-Tubes network access? etc.) your activity may need for its normal operation. See Bitfrost.
to be detailed
Presence
Collaboration plays a large role in Sugar. Still, the presence and sharing APIs are still somewhat rough. There are attempts to explain sharing, see Shared Sugar Activities and Activity Sharing. The following are the bare essentials.
General
Activities must support sharing using the Presence Service (PS). It is accessible on the DBus:
Service: org.laptop.Sugar.Presence Interface: org.laptop.Sugar.Presence Object Path: /org/laptop/Sugar/Presence
Sharing
If the activity was not yet shared but the user clicked the Share button, sharing is initiated by calling ShareActivity():
activity = PS.ShareActivity(activity_id, bundle_id, name, properties)
The bundle id is used for the icon and to launch the same activity when someone joins it. The name will be shown in the mesh view and should generally be the same as the title of the datastore object (see above). The properties argument is not used currently (but see #4660) and should be an empty dictionary.
Note that sharing will be private (invitation-only) by default, that is, the icon will not be visible in the mesh. To share publicly, set the 'private' property to False:
activity.SetProperties({'private': False})
Inviting
Another way of starting a shared session is by inviting a buddy from the mesh view. The Invite() method of the activity is called (see above). Then the activity should be shared privately, and the buddy must be invited using the key that was passed to Invite():
buddy = PS.GetBuddyByPublicKey(buddy_key) activity.Invite(buddy, message)
Joining
When launching, the PS must be consulted to see if this instance was shared by someone else, meaning it was launched by the user is trying to join it:
activity = PS.GetActivityById(activity_id)
This yields an error if this instance (identified by its activity id) was not shared, in which case a regular non-shared startup should be performed. Otherwise, the activity object held by the PS is returned, and this activity instance needs to join:
activity.Join()
It should continue by establishing a communication channel with the originating instance (see below)
Leaving
To leave a shared activity (e.g. because it is closing) you need to inform the PS:
activity.Leave()
Buddies
The activity object created by either sharing the current activity or joining an existing activity is used to establish means of communication between these instances. The joined XOs can be accessed to start communicating with them:
buddies = activity.GetJoinedBuddies()
To get notified of buddies joining or leaving, listen to these signals:
BuddyJoined (o: buddy) BuddyLeft (o: buddy)
Tubes
"Tubes" are the transport medium of choice on the XO, provided by the Telepathy framework. There are "D-Bus Tubes" allowing remote D-Bus calls, and "Stream Tubes" which forward sockets (similar to ssh forwarding). Tubes are collected in a "Channel", and channels are associated with a "room", one per shared activity instance.
First, get the Telepathy connection from the shared activity object:
(tp_service, tp_connection, channels) = activity.GetChannels()
where tp_service and tp_connection is the DBus service name and object path for the Telepathy connection. An array of channels pre-created in the activity room is returned, too. There is at least a text chat and a tubes channel at
Service: (tp_service) Object path: (channels[i]) Interface: 'org.freedesktop.Telepathy.Channel'
Use GetChannelType() to tell the channels apart:
if (channel.GetChannelType() == 'org.freedesktop.Telepathy.Channel.Type.Tubes') ... elseif (channel.GetChannelType() == 'org.freedesktop.Telepathy.Channel.Type.Text') ...
Stream Tubes
A stream tube forwards a socket to a remote host (similar to ssh forwarding, but not encrypted). The activity can set up a listening socket by whatever means and then create a tube to forward it:
tube_id = channel.OfferStreamTube( # sa{sv}uvuv -> u 'my-activity-xttp', # unique service name {}, # dict of params 2, # socket type (0=Unix, 2=IPv4, 3=IPv6) ('127.0.0.1', 12345), # socket address (depends on type) 0, 0) # access control & params
You can forward Unix, IPv4, or IPv6 sockets. Access control is restricted to localhost by default.
New tubes are announced by a signal:
NewTube ( u: tube_id, u: initiator, u: type, s: service, a{sv}: parameters, u: state )
On the remote host you can connect to that tube:
address = channel.AcceptStreamTube( # uuuv -> v tube_id, 2, # socket type to return (0=Unix, 2=IPv4, 3=IPv6) 0, 0) # access control & params
which returns an address struct of the specified type, e.g., ('127.0.0.1', 45679). Then just connect a socket to that address and you are ready to share data.
D-Bus Tubes
to be continued. In the mean time, see Presence Service DBus API and Tubes