~ubuntu-branches/ubuntu/quantal/zeitgeist/quantal

« back to all changes in this revision

Viewing changes to _zeitgeist/engine/extensions/storagemonitor.py

  • Committer: Bazaar Package Importer
  • Author(s): Siegfried-Angel Gevatter Pujals
  • Date: 2011-05-07 20:45:57 UTC
  • mfrom: (1.1.9 upstream) (6.1.5 sid)
  • Revision ID: james.westby@ubuntu.com-20110507204557-31lr591fyj9ha83j
Tags: 0.8.0-1
* New upstream release. Some of the changes are:
 - Fixed secondary sorting by timestamp for most ResultTypes (LP: #772041).
 - Enabled filtering by availability in FindEvents.
 - Added event origin and current_uri properties (LP: #425258, ...) with
   corresponding ResultTypes and MoveEvent handling (LP: #602211).
 - Fixed inconsistencies caused by the internal cache not being updated when
   events were deleted (LP: #598666).
 - Added a Storage Monitor extension which tracks network connectivity and
   removable devices (LP: #489194).
 - Fixed datahub launching to avoid zombie processes (LP: #739780).
 - Replaced the Blacklist extension giving it a more capable API
   (LP: #612344).
 - Simplified log output (LP: #744818, ...).
 - Fixed bug in the connection (to Zeitgeist) recovery code (LP: #771970).
* debian/control:
 - Bump Standards-Version to 3.9.2.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -.- coding: utf-8 -.-
 
2
 
 
3
# Zeitgeist
 
4
#
 
5
# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
 
6
# Copyright © 2011 Canonical Ltd
 
7
#
 
8
# This program is free software: you can redistribute it and/or modify
 
9
# it under the terms of the GNU Lesser General Public License as published by
 
10
# the Free Software Foundation, either version 2.1 of the License, or
 
11
# (at your option) any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
# GNU Lesser General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU Lesser General Public License
 
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
20
 
 
21
import os
 
22
import dbus
 
23
import dbus.service
 
24
import sqlite3
 
25
import gio
 
26
import logging
 
27
 
 
28
from zeitgeist.datamodel import Event
 
29
from _zeitgeist.engine.extension import Extension
 
30
from _zeitgeist.engine import constants
 
31
 
 
32
from zeitgeist.datamodel import StorageState
 
33
from _zeitgeist.engine.sql import get_default_cursor
 
34
 
 
35
log = logging.getLogger("zeitgeist.storagemonitor")
 
36
 
 
37
#
 
38
# Storage mediums we need to handle:
 
39
#
 
40
# - USB drives
 
41
# - Data CD/DVDs
 
42
# - Audio CDs
 
43
# - Video DVD
 
44
# - Networked file systems
 
45
# - Online resources
 
46
#
 
47
# A storage medium is  gio.Volume (since this is a physical entity for the user)
 
48
# or a network interface - how ever NetworkManager/ConnMan model these
 
49
#
 
50
# We can not obtain UUIDs for all of the listed gio.Volumes, so we need a
 
51
# fallback chain of identifiers
 
52
#
 
53
# DB schema: 
 
54
# It may be handy for app authors to have the human-readable
 
55
# description at hand. We can not currently easily do this in the
 
56
# current db... We may be able to do this in a new table, not
 
57
# breaking compat with the log db. We might also want a formal type
 
58
# associated with the storage so apps can use an icon for it.
 
59
# A new table and a new object+interface on DBus could facilitate this
 
60
#
 
61
# 'storage' table
 
62
#   id
 
63
#   name
 
64
#   state
 
65
#   +type
 
66
#   +display_name
 
67
#
 
68
# FIXME: We can not guess what the correct ID of CDs and DVDs were when they
 
69
#        are ejected, and also guess "unknown"
 
70
#
 
71
 
 
72
STORAGE_MONITOR_DBUS_OBJECT_PATH = "/org/gnome/zeitgeist/storagemonitor"
 
73
STORAGE_MONITOR_DBUS_INTERFACE = "org.gnome.zeitgeist.StorageMonitor"
 
74
 
 
75
class StorageMonitor(Extension, dbus.service.Object):
 
76
        """
 
77
        The Storage Monitor monitors the availability of network interfaces and
 
78
        storage devices and updates the Zeitgeist database with this information so
 
79
        clients can efficiently query based on the storage identifier and availability
 
80
        of the storage media the event subjects reside on.
 
81
        
 
82
        For storage devices the monitor will use the UUID of the partition that a
 
83
        subject reside on as storage id. For network URIs the storage monitor will
 
84
        use the fixed identifier :const:`net`. For subjects residing on persistent,
 
85
        but unidentifiable, media attached to the computer the id :const:`local`
 
86
        will be used. For URIs that can't be handled the storage id will be set
 
87
        to :const:`unknown`. The :const:`local` and :const:`unknown` storage media
 
88
        are considered to be always in an available state. To determine the
 
89
        availability of the :const:`net` media the monitor will use either Connman
 
90
        or NetworkManager - what ever is available on the host system.
 
91
 
 
92
        For subjects being inserted into the log that doesn't have a storage id set
 
93
        on them this extension will try and figure it out on the fly and update
 
94
        the subject appropriately before its inserted into the log.
 
95
        
 
96
        The storage monitor of the Zeitgeist engine has DBus object path
 
97
        :const:`/org/gnome/zeitgeist/storagemonitor` under the bus name
 
98
        :const:`org.gnome.zeitgeist.Engine`.
 
99
        """
 
100
        PUBLIC_METHODS = []
 
101
        
 
102
        def __init__ (self, engine):            
 
103
                Extension.__init__(self, engine)
 
104
                dbus.service.Object.__init__(self, dbus.SessionBus(),
 
105
                                             STORAGE_MONITOR_DBUS_OBJECT_PATH)
 
106
                
 
107
                self._db = get_default_cursor()
 
108
                mon = gio.VolumeMonitor()
 
109
                
 
110
                # Update DB with all current states
 
111
                for vol in mon.get_volumes():
 
112
                        self.add_storage_medium(self._get_volume_id(vol), vol.get_icon().to_string(), vol.get_name())
 
113
                
 
114
                # React to volumes comming and going
 
115
                mon.connect("volume-added", self._on_volume_added)
 
116
                mon.connect("volume-removed", self._on_volume_removed)
 
117
                
 
118
                # Write connectivity to the DB. Dynamically decide whether to use
 
119
                # Connman or NetworkManager
 
120
                if dbus.SystemBus().name_has_owner ("net.connman"):
 
121
                        self._network = ConnmanNetworkMonitor(lambda: self.add_storage_medium("net", "stock_internet", "Internet"),
 
122
                                                              lambda: self.remove_storage_medium("net"))
 
123
                elif dbus.SystemBus().name_has_owner ("org.freedesktop.NetworkManager"):
 
124
                        self._network = NMNetworkMonitor(lambda: self.add_storage_medium("net", "stock_internet", "Internet"),
 
125
                                                         lambda: self.remove_storage_medium("net"))
 
126
                else:
 
127
                        log.info("No network monitoring system found (Connman or NetworkManager)."
 
128
                                 "Network monitoring disabled")
 
129
        
 
130
        def pre_insert_event (self, event, dbus_sender):
 
131
                """
 
132
                On-the-fly add subject.storage to events if it is not set
 
133
                """
 
134
                for subj in event.subjects:
 
135
                        if not subj.storage:
 
136
                                storage = self._find_storage(subj.uri)
 
137
                                #log.debug("Subject %s resides on %s" % (subj.uri, storage))
 
138
                                subj.storage = storage
 
139
                return event
 
140
        
 
141
        def _find_storage (self, uri):
 
142
                """
 
143
                Given a URI find the name of the storage medium it resides on
 
144
                """
 
145
                uri_scheme = uri.rpartition("://")[0]
 
146
                if uri_scheme in ["http", "https", "ftp", "sftp", "ssh", "mailto"]:
 
147
                        return "net"
 
148
                elif uri_scheme == "file":
 
149
                        # Note: gio.File.find_enclosing_mount() does not behave
 
150
                        #       as documented, but throws errors when no
 
151
                        #       gio.Mount is found.
 
152
                        #       Cases where we have no mount often happens when
 
153
                        #       we are on a non-removable drive , and this is
 
154
                        #       the assumption here. We use the stora medium
 
155
                        #       'local' for this situation
 
156
                        try:
 
157
                                mount = gio.File(uri=uri).find_enclosing_mount()
 
158
                        except gio.Error:
 
159
                                return "local"
 
160
                        if mount is None: return "unknown"
 
161
                        return self._get_volume_id(mount.get_volume())
 
162
        
 
163
        def _on_volume_added (self, mon, volume):
 
164
                icon = volume.get_icon()
 
165
                if isinstance(icon, gio.ThemedIcon):
 
166
                        icon_name = icon.get_names()[0]
 
167
                else:
 
168
                        icon_name = ""
 
169
                self.add_storage_medium (self._get_volume_id(volume), icon_name, volume.get_name())
 
170
        
 
171
        def _on_volume_removed (self, mon, volume):
 
172
                self.remove_storage_medium (self._get_volume_id(volume))
 
173
 
 
174
        def _get_volume_id (self, volume):
 
175
                """
 
176
                Get a string identifier for a gio.Volume. The id is constructed
 
177
                as a "best effort" since we can not always uniquely identify
 
178
                volumes, especially audio- and data CDs are problematic.
 
179
                """
 
180
                volume_id = volume.get_uuid()
 
181
                if volume_id : return volume_id
 
182
                
 
183
                volume_id = volume.get_identifier("uuid")
 
184
                if volume_id : return volume_id
 
185
                
 
186
                volume_id = volume.get_identifier("label")
 
187
                if volume_id : return volume_id
 
188
                
 
189
                volume_id = volume.get_name()
 
190
                if volume_id : return volume_id
 
191
                
 
192
                return "unknown"
 
193
                
 
194
        def add_storage_medium (self, medium_name, icon, display_name):
 
195
                """
 
196
                Mark storage medium as available in the Zeitgeist DB
 
197
                """
 
198
                if isinstance(icon,gio.Icon):
 
199
                        icon = icon.to_string()
 
200
                elif not isinstance(icon, basestring):
 
201
                        raise TypeError, "The 'icon' argument must be a gio.Icon or a string"
 
202
                
 
203
                log.debug("Setting storage medium %s '%s' as available" % (medium_name, display_name))
 
204
                
 
205
                try:
 
206
                        self._db.execute("INSERT INTO storage (value, state, icon, display_name) VALUES (?, ?, ?, ?)", (medium_name, StorageState.Available, icon, display_name))
 
207
                except sqlite3.IntegrityError, e:
 
208
                        try:
 
209
                                self._db.execute("UPDATE storage SET state=?, icon=?, display_name=? WHERE value=?", (StorageState.Available, icon, display_name, medium_name))
 
210
                        except Exception, e:
 
211
                                log.warn("Error updating storage state for '%s': %s" % (medium_name, e))
 
212
                                return
 
213
                
 
214
                self._db.connection.commit()
 
215
                
 
216
                # Notify DBus that the storage is available
 
217
                self.StorageAvailable(medium_name, { "available" : True,
 
218
                                                     "icon" : icon or "",
 
219
                                                     "display-name" : display_name or ""})
 
220
                
 
221
        def remove_storage_medium (self, medium_name):
 
222
                """
 
223
                Mark storage medium  as `not` available in the Zeitgeist DB
 
224
                """
 
225
                
 
226
                log.debug("Setting storage medium %s as not available" % medium_name)
 
227
                
 
228
                try:
 
229
                        self._db.execute("INSERT INTO storage (value, state) VALUES (?, ?)", (medium_name, StorageState.NotAvailable))
 
230
                except sqlite3.IntegrityError, e:
 
231
                        try:
 
232
                                self._db.execute("UPDATE storage SET state=? WHERE value=?", (StorageState.NotAvailable, medium_name))
 
233
                        except Exception, e:
 
234
                                log.warn("Error updating storage state for '%s': %s" % (medium_name, e))
 
235
                                return
 
236
                
 
237
                self._db.connection.commit()
 
238
                
 
239
                # Notify DBus that the storage is unavailable
 
240
                self.StorageUnavailable(medium_name)
 
241
        
 
242
        @dbus.service.method(STORAGE_MONITOR_DBUS_INTERFACE,
 
243
                             out_signature="a(sa{sv})")
 
244
        def GetStorages (self):
 
245
                """
 
246
                Retrieve a list describing all storage media known by the Zeitgeist daemon.
 
247
                A storage medium is identified by a key - as set in the subject
 
248
                :const:`storage` field. For each storage id there is a dict of properties
 
249
                that will minimally include the following: :const:`available` with a boolean
 
250
                value, :const:`icon` a string with the name of the icon to use for the
 
251
                storage medium, and :const:`display-name` a string with a human readable
 
252
                name for the storage medium.
 
253
                
 
254
                The DBus signature of the return value of this method is :const:`a(sa{sv})`.
 
255
                """
 
256
                storage_mediums = []
 
257
                storage_data = self._db.execute("SELECT value, state, icon, display_name FROM storage").fetchall()
 
258
                
 
259
                for row in storage_data:
 
260
                        if not row[0] : continue
 
261
                        storage_mediums.append((row[0],
 
262
                                               { "available" : bool(row[1]),
 
263
                                                 "icon" : row[2] or "",
 
264
                                                 "display-name" : row[3] or ""}))
 
265
                
 
266
                return storage_mediums
 
267
        
 
268
        @dbus.service.signal(STORAGE_MONITOR_DBUS_INTERFACE,
 
269
                             signature="sa{sv}")
 
270
        def StorageAvailable (self, storage_id, storage_description):
 
271
                """
 
272
                The Zeitgeist daemon emits this signal when the storage medium with id
 
273
                :const:`storage_id` has become available.
 
274
                
 
275
                The second parameter for this signal is a dictionary containing string
 
276
                keys and variant values. The keys that are guaranteed to be there are
 
277
                :const:`available` with a boolean value, :const:`icon` a string with the
 
278
                name of the icon to use for the storage medium, and :const:`display-name`
 
279
                a string with a human readable name for the storage medium.
 
280
                
 
281
                The DBus signature of this signal is :const:`sa{sv}`.
 
282
                """
 
283
                pass
 
284
        
 
285
        @dbus.service.signal(STORAGE_MONITOR_DBUS_INTERFACE,
 
286
                             signature="s")
 
287
        def StorageUnavailable (self, storage_id):
 
288
                """
 
289
                The Zeitgeist daemon emits this signal when the storage medium with id
 
290
                :const:`storage_id` is no longer available.
 
291
                
 
292
                The DBus signature of this signal is :const:`s`.
 
293
                """
 
294
                pass
 
295
 
 
296
class NMNetworkMonitor:
 
297
        """
 
298
        Checks whether there is a funtioning network interface via
 
299
        NetworkManager (requires NM >= 0.8).
 
300
        See http://projects.gnome.org/NetworkManager/developers/spec-08.html
 
301
        """
 
302
        NM_BUS_NAME = "org.freedesktop.NetworkManager"
 
303
        NM_IFACE = "org.freedesktop.NetworkManager"
 
304
        NM_OBJECT_PATH = "/org/freedesktop/NetworkManager"
 
305
        
 
306
        NM_STATE_UNKNOWN = 0
 
307
        NM_STATE_ASLEEP = 1
 
308
        NM_STATE_CONNECTING = 2
 
309
        NM_STATE_CONNECTED = 3
 
310
        NM_STATE_DISCONNECTED = 4
 
311
        
 
312
        def __init__ (self, on_network_up, on_network_down):
 
313
                log.debug("Creating NetworkManager network monitor")
 
314
                if not callable(on_network_up):
 
315
                        raise TypeError((
 
316
                                "First argument to NMNetworkMonitor constructor "
 
317
                                "must be callable, found %s" % on_network_up))
 
318
                if not callable(on_network_down):
 
319
                        raise TypeError((
 
320
                                "Second argument to NMNetworkMonitor constructor "
 
321
                                "must be callable, found %s" % on_network_up))
 
322
                
 
323
                self._up = on_network_up
 
324
                self._down = on_network_down
 
325
                
 
326
                proxy = dbus.SystemBus().get_object(NMNetworkMonitor.NM_BUS_NAME,
 
327
                                                    NMNetworkMonitor.NM_OBJECT_PATH)
 
328
                self._props = dbus.Interface(proxy, dbus.PROPERTIES_IFACE)
 
329
                self._nm = dbus.Interface(proxy, NMNetworkMonitor.NM_IFACE)
 
330
                self._nm.connect_to_signal("StateChanged", self._on_state_changed)
 
331
                
 
332
                # Register the initial state
 
333
                state = self._props.Get(NMNetworkMonitor.NM_IFACE, "State")
 
334
                self._on_state_changed(state)
 
335
                
 
336
        def _on_state_changed(self, state):
 
337
                log.debug("NetworkManager network state: %s" % state)
 
338
                if state == NMNetworkMonitor.NM_STATE_CONNECTED:
 
339
                        self._up ()
 
340
                else:
 
341
                        self._down()
 
342
 
 
343
class ConnmanNetworkMonitor:
 
344
        """
 
345
        Checks whether there is a funtioning network interface via Connman
 
346
        """
 
347
        CM_BUS_NAME = "net.connman"
 
348
        CM_IFACE = "net.connman.Manager"
 
349
        CM_OBJECT_PATH = "/"
 
350
        
 
351
        def __init__ (self, on_network_up, on_network_down):
 
352
                log.debug("Creating Connman network monitor")
 
353
                if not callable(on_network_up):
 
354
                        raise TypeError((
 
355
                                "First argument to ConnmanNetworkMonitor constructor "
 
356
                                "must be callable, found %s" % on_network_up))
 
357
                if not callable(on_network_down):
 
358
                        raise TypeError((
 
359
                                "Second argument to ConnmanNetworkMonitor constructor "
 
360
                                "must be callable, found %s" % on_network_up))
 
361
                
 
362
                self._up = on_network_up
 
363
                self._down = on_network_down
 
364
                
 
365
                proxy = dbus.SystemBus().get_object(ConnmanNetworkMonitor.CM_BUS_NAME,
 
366
                                                    ConnmanNetworkMonitor.CM_OBJECT_PATH)
 
367
                self._cm = dbus.Interface(proxy, ConnmanNetworkMonitor.CM_IFACE)
 
368
                self._cm.connect_to_signal("StateChanged", self._on_state_changed)
 
369
                #
 
370
                # ^^ There is a bug in some Connman versions causing it to not emit the
 
371
                #    net.connman.Manager.StateChanged signal. We take our chances this
 
372
                #    instance is working properly :-)
 
373
                #
 
374
 
 
375
                
 
376
                # Register the initial state
 
377
                state = self._cm.GetState()
 
378
                self._on_state_changed(state)
 
379
                
 
380
        def _on_state_changed(self, state):
 
381
                log.debug("Connman network state is '%s'" % state)
 
382
                if state == "online":
 
383
                        self._up ()
 
384
                else:
 
385
                        self._down()