summaryrefslogtreecommitdiff
path: root/_zeitgeist/engine/notify.py
blob: 4ad85142f20c61b6706ebd2252a9558da82e6504 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# -.- coding: utf-8 -.-

# Zeitgeist
#
# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
#
# This program 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 3 of the License, or
# (at your option) any later version.
#
# This program 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 program.  If not, see <http://www.gnu.org/licenses/>.

import dbus
import logging

from zeitgeist.datamodel import TimeRange

logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger("zeitgeist.notify")

class _MonitorProxy (dbus.Interface):
	"""
	Connection to a org.gnome.zeitgeist.Monitor interface running on some
	client to the Zeitgeist engine.
	"""
	
	def __init__ (self, owner, monitor_path, time_range, event_templates):
		"""
		Create a new MonitorProxy for the unique DBus name *owner* on the
		path *monitor_path*. Note that the path points to an object
		living under *owner* and not necessarily inside the current
		process.
		
		:param owner: Unique DBus name of the process owning the monitor
		:param monitor_path: The DBus object path to the monitor object
			in the client process
		:param time_range: a 
			:class:`TimeRange <zeitgeist.datamodel.TimeRange` instance
			the monitored events must lie within
		:param event_templates: List of event templates that any events
			must match in order to notify this monitor
		:type event_templates: list of
			:class:`Events <zeitgeist.datamodel.Event>`
		"""
		bus = dbus.SessionBus()
		proxy = bus.get_object(owner, monitor_path)
		dbus.Interface.__init__(
				self,
				proxy,
				"org.gnome.zeitgeist.Monitor"
		)
		
		self._owner = owner
		self._path = monitor_path
		self._time_range = time_range
		self._templates = event_templates
	
	def __str__ (self):
		return "%s%s" % (self.owner, self.path)
	
	def get_owner (self) : return self._owner
	owner = property(get_owner, doc="Read only property with the unique DBus name of the process owning the monitor")
	
	def get_path (self) : return self._path
	path = property(get_path, doc="Read only property with the object path of the monitor in the process owning the monitor")
	
	def get_time_range (self) : return self._time_range
	time_range = property(get_time_range, doc="Read only property with matched :class:`TimeRange` of the monitor")
	
	def __hash__ (self):
		return hash(_MonitorProxy.hash(self._owner, self._path))
	
	def matches (self, event):
		"""
		Returns True of this monitor has a template matching *event*
		
		:param event: The event to check against the monitor's templates
		:type event: :class:`Event <zeitgeist.datamodel.Event>`
		"""
		if not event.in_time_range(self._time_range):
			return False
		
		if len(self._templates) == 0:
			return True
		
		for tmpl in self._templates:
			if event.matches_template(tmpl) : return True
		return False
	
	def notify_insert (self, time_range, events):
		"""
		Asynchronously notify the monitor that a collection of matching events has been inserted
		
		The events will *not* be filtered through the :meth:`matches`
		method. It is the responsability of the caller to do that.
		"""
		for ev in events : ev._make_dbus_sendable()
		self.NotifyInsert(time_range, events,
					reply_handler=self._notify_reply_handler,
					error_handler=self._notify_error_handler)
	
	def notify_delete (self, time_range, event_ids):
		"""
		Asynchronously notify the monitor that a collection of events has been deleted
		
		Only the event ids are passed back to the monitor. Note this
		method does not check that *time_range* is within the monitor's
		time range. That is the responsibility of the caller
		"""
		self.NotifyDelete(time_range, event_ids,
					reply_handler=self._notify_reply_handler,
					error_handler=self._notify_error_handler)
	
	@staticmethod
	def hash(owner, path):
		"""
		Calculate an integer uniquely identifying the monitor based on
		the DBus name of the owner and object path of the monitor itself
		"""
		return hash("%s#%s" % (owner, path))
	
	def _notify_reply_handler (self):
		"""
		Async reply handler for invoking Notify() over DBus
		"""
		pass
	
	def _notify_error_handler (self, error):
		"""
		Async error handler for invoking Notify() over DBus
		"""
		log.warn("Failed to deliver notification: %s" % error)
		
class MonitorManager:
	
	def __init__ (self):
		self._monitors = {} # hash -> Monitor
		self._connections = {} # owner -> list of paths
		
		# Listen for disconnecting clients to clean up potential dangling monitors
		dbus.SessionBus().add_signal_receiver (self._name_owner_changed,
											   signal_name="NameOwnerChanged",
											   dbus_interface=dbus.BUS_DAEMON_IFACE,
											   arg2="")
	
	def install_monitor (self, owner, monitor_path, time_range, event_templates):
		"""
		Install a :class:`MonitorProxy` and set it up to receive
		notifications when events are pushed into the :meth;`dispatch`
		method.
		
		Monitors will automatically be cleaned up if :const:`monitor.owner`
		disconnects from the bus. To manually remove a monitor call the
		:meth:`remove_monitor` on this object passing in
		:const:`monitor.owner` and :const:`monitor.path`.
		
		:param owner: Unique DBus name of the process owning the monitor
		:type owner: string
		:param monitor_path: The DBus object path for the monitor object
			in the client process
		:type monitor_path: String or :class:`dbus.ObjectPath`
		:param time_range: a 
			:class:`TimeRange <zeitgeist.datamodel.TimeRange` instance
			the monitored events must lie within
		:param event_templates: A list of
			:class:`Event <zeitgeist.datamodel.Event>` templates to match
		:returns: This method has no return value
		"""
		# Check that we don't already have the monitor, so we don't
		# needlesly set up a full dbus Proxy
		monitor_key = _MonitorProxy.hash(owner, monitor_path)
		if monitor_key in self._monitors:
			raise KeyError("Monitor for %s already installed at path %s" % (owner, monitor_path))
		
		monitor = _MonitorProxy(owner, monitor_path, time_range, event_templates)
		self._monitors[monitor_key] = monitor
		
		if not monitor.owner in self._connections:
			self._connections[owner] = set()
		
		self._connections[owner].add(monitor.path)
	
	def remove_monitor (self, owner, monitor_path):
		"""
		Remove an installed monitor.
		
		:param owner: Unique DBus name of the process owning the monitor
		:type owner: string
		:param monitor_path: The DBus object path for the monitor object
			in the client process
		:type monitor_path: String or :class:`dbus.ObjectPath`
		"""
		log.debug("Removing monitor %s%s", owner, monitor_path)
		mon = self._monitors.pop(_MonitorProxy.hash(owner, monitor_path))
		
		if not mon:
			raise KeyError("Unknown monitor %s for owner %s" % (monitor_path, owner))
		
		conn = self._connections[owner]
		if conn : conn.remove(monitor_path)
	
	def notify_insert (self, time_range, events):
		"""
		Send events to matching monitors.
		The monitors will only be notified about the events for which
		they have a matching template, ie. :meth:`MonitorProxy.matches`
		returns True.
		
		:param events: The events to check against the monitor templates
		:type events: list of :class:`Events <zeitgeist.datamodel.Event>`
		"""
		for mon in self._monitors.itervalues():
			log.debug("Checking monitor %s" % mon)
			matching_events = []
			
			for ev in events:
				if mon.matches(ev):
					matching_events.append(ev)
			
			if matching_events :
				log.debug("Notifying %s about %s insertions" % (mon, len(matching_events)))
				mon.notify_insert(time_range.intersect(mon.time_range), matching_events)
	
	def notify_delete (self, time_range, event_ids):
		"""
		Notify monitors with matching time ranges that the events with the given ids have been deleted from the log
		
		:param time_range: A TimeRange instance that spans the deleted events
		:param event_ids: A list of event ids
		"""
		for mon in self._monitors.itervalues():
			log.debug("Checking monitor %s" % mon)
			intersecting_range = mon.time_range.intersect(time_range)
			
			if intersecting_range:
				log.debug("Notifying %s about %s deletions" % (mon, len(event_ids)))
				mon.notify_delete(intersecting_range, event_ids)
		
	def _name_owner_changed (self, owner, old, new):
		"""
		Clean up monitors for processes disconnecting from the bus
		"""
		# Don't proceed if this is a disconnect of an unknown connection
		if not owner in self._connections :
			return
		
		conn = self._connections[owner]
		
		log.debug("Client disconnected %s" % owner)
		# we need to copy the conn set here, otherwise self.remove()
		# will possibly change size of self._connection during iteration
		# which results in a RuntimeError (thekorn)
		for path in conn.copy():
			self.remove_monitor(owner, path)
		
		self._connections.pop(owner)