1
# -.- coding: utf-8 -.-
5
# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License as published by
9
# the Free Software Foundation, either version 2.1 of the License, or
10
# (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Lesser General Public License for more details.
17
# You should have received a copy of the GNU Lesser General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
23
from zeitgeist.datamodel import TimeRange
25
log = logging.getLogger("zeitgeist.notify")
27
class _MonitorProxy (dbus.Interface):
29
Connection to a org.gnome.zeitgeist.Monitor interface running on some
30
client to the Zeitgeist engine.
33
def __init__ (self, owner, monitor_path, time_range, event_templates):
35
Create a new MonitorProxy for the unique DBus name *owner* on the
36
path *monitor_path*. Note that the path points to an object
37
living under *owner* and not necessarily inside the current
40
:param owner: Unique DBus name of the process owning the monitor
41
:param monitor_path: The DBus object path to the monitor object
44
:class:`TimeRange <zeitgeist.datamodel.TimeRange` instance
45
the monitored events must lie within
46
:param event_templates: List of event templates that any events
47
must match in order to notify this monitor
48
:type event_templates: list of
49
:class:`Events <zeitgeist.datamodel.Event>`
51
bus = dbus.SessionBus()
52
proxy = bus.get_object(owner, monitor_path)
53
dbus.Interface.__init__(
56
"org.gnome.zeitgeist.Monitor"
60
self._path = monitor_path
61
self._time_range = time_range
62
self._templates = event_templates
65
return "%s%s" % (self.owner, self.path)
67
def get_owner (self) : return self._owner
68
owner = property(get_owner, doc="Read only property with the unique DBus name of the process owning the monitor")
70
def get_path (self) : return self._path
71
path = property(get_path, doc="Read only property with the object path of the monitor in the process owning the monitor")
73
def get_time_range (self) : return self._time_range
74
time_range = property(get_time_range, doc="Read only property with matched :class:`TimeRange` of the monitor")
77
return hash(_MonitorProxy.hash(self._owner, self._path))
79
def matches (self, event):
81
Returns True of this monitor has a template matching *event*
83
:param event: The event to check against the monitor's templates
84
:type event: :class:`Event <zeitgeist.datamodel.Event>`
86
if not event.in_time_range(self._time_range):
89
if len(self._templates) == 0:
92
for tmpl in self._templates:
93
if event.matches_template(tmpl) : return True
96
def notify_insert (self, time_range, events):
98
Asynchronously notify the monitor that a collection of matching events has been inserted
100
The events will *not* be filtered through the :meth:`matches`
101
method. It is the responsability of the caller to do that.
103
for ev in events : ev._make_dbus_sendable()
104
self.NotifyInsert(time_range, events,
105
reply_handler=self._notify_reply_handler,
106
error_handler=self._notify_error_handler)
108
def notify_delete (self, time_range, event_ids):
110
Asynchronously notify the monitor that a collection of events has been deleted
112
Only the event ids are passed back to the monitor. Note this
113
method does not check that *time_range* is within the monitor's
114
time range. That is the responsibility of the caller
116
self.NotifyDelete(time_range, event_ids,
117
reply_handler=self._notify_reply_handler,
118
error_handler=self._notify_error_handler)
121
def hash(owner, path):
123
Calculate an integer uniquely identifying the monitor based on
124
the DBus name of the owner and object path of the monitor itself
126
return hash("%s#%s" % (owner, path))
128
def _notify_reply_handler (self):
130
Async reply handler for invoking Notify() over DBus
134
def _notify_error_handler (self, error):
136
Async error handler for invoking Notify() over DBus
138
log.warn("Failed to deliver notification: %s" % error)
140
class MonitorManager:
143
self._monitors = {} # hash -> Monitor
144
self._connections = {} # owner -> list of paths
146
# Listen for disconnecting clients to clean up potential dangling monitors
147
dbus.SessionBus().add_signal_receiver (self._name_owner_changed,
148
signal_name="NameOwnerChanged",
149
dbus_interface=dbus.BUS_DAEMON_IFACE,
152
def install_monitor (self, owner, monitor_path, time_range, event_templates):
154
Install a :class:`MonitorProxy` and set it up to receive
155
notifications when events are pushed into the :meth;`dispatch`
158
Monitors will automatically be cleaned up if :const:`monitor.owner`
159
disconnects from the bus. To manually remove a monitor call the
160
:meth:`remove_monitor` on this object passing in
161
:const:`monitor.owner` and :const:`monitor.path`.
163
:param owner: Unique DBus name of the process owning the monitor
165
:param monitor_path: The DBus object path for the monitor object
166
in the client process
167
:type monitor_path: String or :class:`dbus.ObjectPath`
169
:class:`TimeRange <zeitgeist.datamodel.TimeRange` instance
170
the monitored events must lie within
171
:param event_templates: A list of
172
:class:`Event <zeitgeist.datamodel.Event>` templates to match
173
:returns: This method has no return value
175
# Check that we don't already have the monitor, so we don't
176
# needlesly set up a full dbus Proxy
177
monitor_key = _MonitorProxy.hash(owner, monitor_path)
178
if monitor_key in self._monitors:
179
raise KeyError("Monitor for %s already installed at path %s" % (owner, monitor_path))
181
monitor = _MonitorProxy(owner, monitor_path, time_range, event_templates)
182
self._monitors[monitor_key] = monitor
184
if not monitor.owner in self._connections:
185
self._connections[owner] = set()
187
self._connections[owner].add(monitor.path)
189
def remove_monitor (self, owner, monitor_path):
191
Remove an installed monitor.
193
:param owner: Unique DBus name of the process owning the monitor
195
:param monitor_path: The DBus object path for the monitor object
196
in the client process
197
:type monitor_path: String or :class:`dbus.ObjectPath`
199
log.debug("Removing monitor %s%s", owner, monitor_path)
200
mon = self._monitors.pop(_MonitorProxy.hash(owner, monitor_path))
203
raise KeyError("Unknown monitor %s for owner %s" % (monitor_path, owner))
205
conn = self._connections[owner]
206
if conn : conn.remove(monitor_path)
208
def notify_insert (self, time_range, events):
210
Send events to matching monitors.
211
The monitors will only be notified about the events for which
212
they have a matching template, ie. :meth:`MonitorProxy.matches`
215
:param events: The events to check against the monitor templates
216
:type events: list of :class:`Events <zeitgeist.datamodel.Event>`
218
for mon in self._monitors.itervalues():
219
log.debug("Checking monitor %s" % mon)
224
matching_events.append(ev)
227
log.debug("Notifying %s about %s insertions" % (mon, len(matching_events)))
229
mon.notify_insert(time_range.intersect(mon.time_range), matching_events)
231
log.exception("Failed to notify monitor: %s" % mon)
233
def notify_delete (self, time_range, event_ids):
235
Notify monitors with matching time ranges that the events with the given ids have been deleted from the log
237
:param time_range: A TimeRange instance that spans the deleted events
238
:param event_ids: A list of event ids
240
for mon in self._monitors.itervalues():
241
log.debug("Checking monitor %s" % mon)
242
intersecting_range = mon.time_range.intersect(time_range)
244
if intersecting_range:
245
log.debug("Notifying %s about %s deletions" % (mon, len(event_ids)))
247
mon.notify_delete(intersecting_range, event_ids)
249
log.exception("Failed to notify monitor: %s" % mon)
251
def _name_owner_changed (self, owner, old, new):
253
Clean up monitors for processes disconnecting from the bus
255
# Don't proceed if this is a disconnect of an unknown connection
256
if not owner in self._connections :
259
conn = self._connections[owner]
261
log.debug("Client disconnected %s" % owner)
262
# we need to copy the conn set here, otherwise self.remove()
263
# will possibly change size of self._connection during iteration
264
# which results in a RuntimeError (thekorn)
265
for path in conn.copy():
266
self.remove_monitor(owner, path)
268
self._connections.pop(owner)