1
# -.- coding: utf-8 -.-
5
# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
6
# Copyright © 2011 Canonical Ltd
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.
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.
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/>.
28
from zeitgeist.datamodel import Event
29
from _zeitgeist.engine.extension import Extension
30
from _zeitgeist.engine import constants
32
from zeitgeist.datamodel import StorageState
33
from _zeitgeist.engine.sql import get_default_cursor
35
log = logging.getLogger("zeitgeist.storagemonitor")
38
# Storage mediums we need to handle:
44
# - Networked file systems
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
50
# We can not obtain UUIDs for all of the listed gio.Volumes, so we need a
51
# fallback chain of identifiers
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
68
# FIXME: We can not guess what the correct ID of CDs and DVDs were when they
69
# are ejected, and also guess "unknown"
72
STORAGE_MONITOR_DBUS_OBJECT_PATH = "/org/gnome/zeitgeist/storagemonitor"
73
STORAGE_MONITOR_DBUS_INTERFACE = "org.gnome.zeitgeist.StorageMonitor"
75
class StorageMonitor(Extension, dbus.service.Object):
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.
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.
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.
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`.
102
def __init__ (self, engine):
103
Extension.__init__(self, engine)
104
dbus.service.Object.__init__(self, dbus.SessionBus(),
105
STORAGE_MONITOR_DBUS_OBJECT_PATH)
107
self._db = get_default_cursor()
108
mon = gio.VolumeMonitor()
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())
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)
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"))
127
log.info("No network monitoring system found (Connman or NetworkManager)."
128
"Network monitoring disabled")
130
def pre_insert_event (self, event, dbus_sender):
132
On-the-fly add subject.storage to events if it is not set
134
for subj in event.subjects:
136
storage = self._find_storage(subj.uri)
137
#log.debug("Subject %s resides on %s" % (subj.uri, storage))
138
subj.storage = storage
141
def _find_storage (self, uri):
143
Given a URI find the name of the storage medium it resides on
145
uri_scheme = uri.rpartition("://")[0]
146
if uri_scheme in ["http", "https", "ftp", "sftp", "ssh", "mailto"]:
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
157
mount = gio.File(uri=uri).find_enclosing_mount()
160
if mount is None: return "unknown"
161
return self._get_volume_id(mount.get_volume())
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]
169
self.add_storage_medium (self._get_volume_id(volume), icon_name, volume.get_name())
171
def _on_volume_removed (self, mon, volume):
172
self.remove_storage_medium (self._get_volume_id(volume))
174
def _get_volume_id (self, volume):
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.
180
volume_id = volume.get_uuid()
181
if volume_id : return volume_id
183
volume_id = volume.get_identifier("uuid")
184
if volume_id : return volume_id
186
volume_id = volume.get_identifier("label")
187
if volume_id : return volume_id
189
volume_id = volume.get_name()
190
if volume_id : return volume_id
194
def add_storage_medium (self, medium_name, icon, display_name):
196
Mark storage medium as available in the Zeitgeist DB
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"
203
log.debug("Setting storage medium %s '%s' as available" % (medium_name, display_name))
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:
209
self._db.execute("UPDATE storage SET state=?, icon=?, display_name=? WHERE value=?", (StorageState.Available, icon, display_name, medium_name))
211
log.warn("Error updating storage state for '%s': %s" % (medium_name, e))
214
self._db.connection.commit()
216
# Notify DBus that the storage is available
217
self.StorageAvailable(medium_name, { "available" : True,
219
"display-name" : display_name or ""})
221
def remove_storage_medium (self, medium_name):
223
Mark storage medium as `not` available in the Zeitgeist DB
226
log.debug("Setting storage medium %s as not available" % medium_name)
229
self._db.execute("INSERT INTO storage (value, state) VALUES (?, ?)", (medium_name, StorageState.NotAvailable))
230
except sqlite3.IntegrityError, e:
232
self._db.execute("UPDATE storage SET state=? WHERE value=?", (StorageState.NotAvailable, medium_name))
234
log.warn("Error updating storage state for '%s': %s" % (medium_name, e))
237
self._db.connection.commit()
239
# Notify DBus that the storage is unavailable
240
self.StorageUnavailable(medium_name)
242
@dbus.service.method(STORAGE_MONITOR_DBUS_INTERFACE,
243
out_signature="a(sa{sv})")
244
def GetStorages (self):
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.
254
The DBus signature of the return value of this method is :const:`a(sa{sv})`.
257
storage_data = self._db.execute("SELECT value, state, icon, display_name FROM storage").fetchall()
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 ""}))
266
return storage_mediums
268
@dbus.service.signal(STORAGE_MONITOR_DBUS_INTERFACE,
270
def StorageAvailable (self, storage_id, storage_description):
272
The Zeitgeist daemon emits this signal when the storage medium with id
273
:const:`storage_id` has become available.
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.
281
The DBus signature of this signal is :const:`sa{sv}`.
285
@dbus.service.signal(STORAGE_MONITOR_DBUS_INTERFACE,
287
def StorageUnavailable (self, storage_id):
289
The Zeitgeist daemon emits this signal when the storage medium with id
290
:const:`storage_id` is no longer available.
292
The DBus signature of this signal is :const:`s`.
296
class NMNetworkMonitor:
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
302
NM_BUS_NAME = "org.freedesktop.NetworkManager"
303
NM_IFACE = "org.freedesktop.NetworkManager"
304
NM_OBJECT_PATH = "/org/freedesktop/NetworkManager"
308
NM_STATE_CONNECTING = 2
309
NM_STATE_CONNECTED = 3
310
NM_STATE_DISCONNECTED = 4
312
def __init__ (self, on_network_up, on_network_down):
313
log.debug("Creating NetworkManager network monitor")
314
if not callable(on_network_up):
316
"First argument to NMNetworkMonitor constructor "
317
"must be callable, found %s" % on_network_up))
318
if not callable(on_network_down):
320
"Second argument to NMNetworkMonitor constructor "
321
"must be callable, found %s" % on_network_up))
323
self._up = on_network_up
324
self._down = on_network_down
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)
332
# Register the initial state
333
state = self._props.Get(NMNetworkMonitor.NM_IFACE, "State")
334
self._on_state_changed(state)
336
def _on_state_changed(self, state):
337
log.debug("NetworkManager network state: %s" % state)
338
if state == NMNetworkMonitor.NM_STATE_CONNECTED:
343
class ConnmanNetworkMonitor:
345
Checks whether there is a funtioning network interface via Connman
347
CM_BUS_NAME = "net.connman"
348
CM_IFACE = "net.connman.Manager"
351
def __init__ (self, on_network_up, on_network_down):
352
log.debug("Creating Connman network monitor")
353
if not callable(on_network_up):
355
"First argument to ConnmanNetworkMonitor constructor "
356
"must be callable, found %s" % on_network_up))
357
if not callable(on_network_down):
359
"Second argument to ConnmanNetworkMonitor constructor "
360
"must be callable, found %s" % on_network_up))
362
self._up = on_network_up
363
self._down = on_network_down
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)
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 :-)
376
# Register the initial state
377
state = self._cm.GetState()
378
self._on_state_changed(state)
380
def _on_state_changed(self, state):
381
log.debug("Connman network state is '%s'" % state)
382
if state == "online":