Exemplo n.º 1
0
    def __init__(self, pubsub=None):
        tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
        self._note_limit = core.FC_TokenBucket(
            tick=optz.tbf_tick,
            burst=optz.tbf_size,
            tick_strangle=lambda x: min(x * optz.tbf_inc, tick_strangle_max),
            tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1))
        self._note_buffer = core.RRQ(optz.queue_len)
        self._note_history = core.RRQ(optz.history_len)
        self._note_windows = dict()
        self._note_id_pool = it.chain.from_iterable(
            it.imap(ft.partial(xrange, 1), it.repeat(2**30)))
        self._renderer = NotificationDisplay(
            optz.layout_margin,
            optz.layout_anchor,
            optz.layout_direction,
            icon_scale=optz.icon_scale,
            markup_default=not optz.markup_disable,
            markup_warn=optz.markup_warn_on_err,
            markup_strip=optz.markup_strip_on_err)
        self._activity_event()

        self.pubsub = pubsub
        if pubsub:
            GLib.io_add_watch(pubsub.fileno(), GLib.PRIORITY_DEFAULT,
                              GLib.IO_IN | GLib.IO_PRI, self._notify_pubsub)

        if optz.test_message:
            # Also test crazy web-of-90s markup here :P
            summary = 'Notification daemon started <small><tt>¯\(°_o)/¯</tt></small>'
            body = (
              'Desktop notification daemon started successfully on host: <u>{host}</u>'
              '\nVersion: <span stretch="extraexpanded">{v}</span>'
              '\nCode path: <small>{code}</small>'
              '\nSound enabled: <span color="{sound_color}">{sound}</span>'
              '\nPubSub enabled: <span color="{pubsub_color}">{pubsub}</span>' )\
             .format(
              host=os.uname()[1], v=core.__version__,
              sound_color='green' if optz.filter_sound else 'red',
              sound=unicode(bool(optz.filter_sound)).lower(),
              pubsub_color='green' if pubsub else 'red',
              pubsub=unicode(bool(pubsub)).lower(),
              code=os.path.abspath(os.path.dirname(core.__file__)) )
            if not self._renderer.markup_default:
                summary, body = it.imap(strip_markup, [summary, body])
            self.display(summary, body)
        if optz.test_sound and optz.filter_sound:
            optz.filter_sound['play'](optz.test_sound)
Exemplo n.º 2
0
 def __init__(self, *argz, **kwz):
     tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
     super(NotificationDaemon, self).__init__(*argz, **kwz)
     self._note_limit = core.FC_TokenBucket(
         tick=optz.tbf_tick,
         burst=optz.tbf_size,
         tick_strangle=lambda x: min(x * optz.tbf_inc, tick_strangle_max),
         tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1))
     self._note_buffer = core.RRQ(optz.queue_len)
     self._note_windows = dict()
     self._note_id_pool = it.chain.from_iterable(
         it.imap(ft.partial(xrange, 1), it.repeat(2**30)))
     self._renderer = NotificationDisplay(optz.layout_margin,
                                          optz.layout_anchor,
                                          optz.layout_direction, optz.img_w,
                                          optz.img_h)
     self._activity_event()
Exemplo n.º 3
0
    def __init__(self, pubsub=None):
        tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
        self._note_limit = core.FC_TokenBucket(
            tick=optz.tbf_tick,
            burst=optz.tbf_size,
            tick_strangle=lambda x: min(x * optz.tbf_inc, tick_strangle_max),
            tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1),
        )
        self._note_buffer = core.RRQ(optz.queue_len)
        self._note_history = core.RRQ(optz.history_len)
        self._note_windows = dict()
        self._note_id_pool = it.chain.from_iterable(it.imap(ft.partial(xrange, 1), it.repeat(2 ** 30)))
        self._renderer = NotificationDisplay(
            optz.layout_margin,
            optz.layout_anchor,
            optz.layout_direction,
            icon_scale=optz.icon_scale,
            markup_default=not optz.markup_disable,
            markup_warn=optz.markup_warn_on_err,
            markup_strip=optz.markup_strip_on_err,
        )
        self._activity_event()

        self.pubsub = pubsub
        if pubsub:
            GLib.io_add_watch(pubsub.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_PRI, self._notify_pubsub)

        if optz.test_message:
            # Also test crazy web-of-90s markup here :P
            summary = "Notification daemon started <small><tt>¯\(°_o)/¯</tt></small>"
            body = (
                "Desktop notification daemon started successfully on host: <u>{host}</u>"
                '\nVersion: <span stretch="extraexpanded">{v}</span>'
                "\nCode path: <small>{code}</small>"
                '\nSound enabled: <span color="{sound_color}">{sound}</span>'
                '\nPubSub enabled: <span color="{pubsub_color}">{pubsub}</span>'
            ).format(
                host=os.uname()[1],
                v=core.__version__,
                sound_color="green" if optz.filter_sound else "red",
                sound=unicode(bool(optz.filter_sound)).lower(),
                pubsub_color="green" if pubsub else "red",
                pubsub=unicode(bool(pubsub)).lower(),
                code=os.path.abspath(os.path.dirname(core.__file__)),
            )
            if not self._renderer.markup_default:
                summary, body = it.imap(strip_markup, [summary, body])
            self.display(summary, body)
        if optz.test_sound and optz.filter_sound:
            optz.filter_sound["play"](optz.test_sound)
Exemplo n.º 4
0
	def __init__(self, *argz, **kwz):
		tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
		super(NotificationDaemon, self).__init__(*argz, **kwz)
		self._note_limit = core.FC_TokenBucket(
			tick=optz.tbf_tick, burst=optz.tbf_size,
			tick_strangle=lambda x: min(x*optz.tbf_inc, tick_strangle_max),
			tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1) )
		self._note_buffer = core.RRQ(optz.queue_len)
		self._note_windows = dict()
		self._note_id_pool = it.chain.from_iterable(
			it.imap(ft.partial(xrange, 1), it.repeat(2**30)) )
		self._renderer = NotificationDisplay(
			optz.layout_margin, optz.layout_anchor, optz.layout_direction, optz.img_w, optz.img_h)
		self._activity_event()
Exemplo n.º 5
0
class NotificationMethods(object):
	plugged, timeout_cleanup = False, True
	_activity_timer = None

	def __init__(self, pubsub=None, logger=None):
		tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
		self._note_limit = core.FC_TokenBucket(
			tick=optz.tbf_tick, burst=optz.tbf_size,
			tick_strangle=lambda x: min(x*optz.tbf_inc, tick_strangle_max),
			tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1) )
		self._note_buffer = core.RRQ(optz.queue_len)
		self._note_history = core.RRQ(optz.history_len)
		self._note_windows = dict()
		self._note_id_pool = it.chain.from_iterable(
			it.imap(ft.partial(xrange, 1), it.repeat(2**30)) )
		self._renderer = NotificationDisplay(
			optz.layout_margin, optz.layout_anchor, optz.layout_direction,
			icon_scale=optz.icon_scale, markup_default=not optz.markup_disable,
			markup_warn=optz.markup_warn_on_err, markup_strip=optz.markup_strip_on_err )
		self._activity_event()

		self.pubsub = pubsub
		if pubsub:
			GLib.io_add_watch( pubsub.fileno(),
				GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_PRI, self._notify_pubsub )
		self.logger = logger

		if optz.test_message:
			# Also test crazy web-of-90s markup here :P
			summary = 'Notification daemon started <small><tt>¯\(°_o)/¯</tt></small>'
			body = (
					'Desktop notification daemon started successfully on host: <u>{host}</u>'
					'\nVersion: <span stretch="extraexpanded">{v}</span>'
					'\nCode path: <small>{code}</small>'
					'\nSound enabled: <span color="{sound_color}">{sound}</span>'
					'\nPubSub enabled: <span color="{pubsub_color}">{pubsub}</span>' )\
				.format(
					host=os.uname()[1], v=core.__version__,
					sound_color='green' if optz.filter_sound else 'red',
					sound=unicode(bool(optz.filter_sound)).lower(),
					pubsub_color='green' if pubsub else 'red',
					pubsub=unicode(bool(pubsub)).lower(),
					code=os.path.abspath(os.path.dirname(core.__file__)) )
			if not self._renderer.markup_default:
				summary, body = it.imap(strip_markup, [summary, body])
			self.display(summary, body)
		if optz.test_sound and optz.filter_sound: optz.filter_sound['play'](optz.test_sound)


	def exit(self, reason=None):
		log.debug('Exiting cleanly%s', ', reason: {}'.format(reason) if reason else '')
		sys.exit()

	def _activity_event(self, callback=False):
		if callback:
			if not self._note_windows:
				self.exit(reason='activity timeout ({}s)'.format(optz.activity_timeout))
			else:
				log.debug( 'Ignoring inacivity timeout event'
					' due to existing windows (retry in %ss).', optz.activity_timeout )
				self._activity_timer = None
		if self._activity_timer: GLib.source_remove(self._activity_timer)
		if optz.activity_timeout and optz.activity_timeout > 0:
			self._activity_timer = GLib.timeout_add_seconds(
				optz.activity_timeout, self._activity_event, True )


	def GetServerInformation(self):
		self._activity_event()
		return 'notification-thing', '*****@*****.**', 'git', '1.2'

	def GetCapabilities(self):
		# action-icons, actions, body, body-hyperlinks, body-images,
		#  body-markup, icon-multi, icon-static, persistence, sound
		self._activity_event()
		caps = ['body', 'persistence', 'icon-static']
		if not self._renderer.markup_default: caps.append('body-markup')
		return sorted(caps)

	def NotificationClosed(self, nid, reason=None):
		log.debug(
			'NotificationClosed signal (id: %s, reason: %s)',
			nid, close_reasons.by_id(reason) )

	def ActionInvoked(self, nid, action_key):
		log.debug('Um... some action invoked? Params: %s', [nid, action_key])


	def Get(self, iface, k):
		return self.GetAll(iface)[k]

	def GetAll(self, iface):
		if iface != self.dbus_interface:
			raise dbus.exceptions.DBusException(
				'This object does not implement the {!r} interface'.format(unicode(iface)) )
		self._activity_event()
		return dict( urgent=optz.urgency_check,
			plug=self.plugged, cleanup=self.timeout_cleanup )

	def Set(self, iface, k, v):
		if iface != self.dbus_interface:
			raise dbus.exceptions.DBusException(
				'This object does not implement the {!r} interface'.format(unicode(iface)) )
		self._activity_event()

		if isinstance(v, types.StringTypes):
			v = unicode(v)
			if v == 'toggle': k, v = '{}_toggle'.format(k), True
			elif v.lower() in {'n', 'no', 'false', 'disable', 'off', '-'}: v = False
		k, v = unicode(k), bool(v)
		if k.endswith('_toggle'):
			k = k[:-7]
			v = not self.Get(iface, k)

		log.debug('Property change: %s = %s', k, v)

		if k == 'urgent':
			if optz.urgency_check == v: return
			optz.urgency_check = v
			if optz.status_notify:
				self.display(
					'Urgent messages passthrough {}'.format(
						'enabled' if optz.urgency_check else 'disabled' ) )

		elif k == 'plug':
			if self.plugged == v: return
			if v:
				self.plugged = True
				log.debug('Notification queue plugged')
				if optz.status_notify:
					self.display( 'Notification proxy: queue is plugged',
						'Only urgent messages will be passed through'
							if optz.urgency_check else 'All messages will be stalled' )
			else:
				self.plugged = False
				log.debug('Notification queue unplugged')
				if optz.status_notify:
					self.display('Notification proxy: queue is unplugged')
				if self._note_buffer:
					log.debug('Flushing plugged queue')
					self.flush()

		elif k == 'cleanup':
			if self.timeout_cleanup == v: return
			self.timeout_cleanup = v
			log.debug('Cleanup timeout: %s', self.timeout_cleanup)
			if optz.status_notify:
				self.display( 'Notification proxy: cleanup timeout is {}'\
					.format('enabled' if self.timeout_cleanup else 'disabled') )

		elif optz.status_notify:
			self.display( 'notification-thing:'
				' unrecognized parameter', 'Key: {!r}, value: {!r}'.format(k, v) )
			return

		self.PropertiesChanged(iface, {k: v}, [])

	def PropertiesChanged(self, iface, props_changed, props_invalidated):
		log.debug( 'PropertiesChanged signal: %s',
			[iface, props_changed, props_invalidated] )


	def Flush(self):
		log.debug('Manual flush of the notification buffer')
		self._activity_event()
		return self.flush(force=True)

	def List(self):
		log.debug('NotificationList call')
		self._activity_event()
		return self._note_windows.keys()

	def Redisplay(self):
		log.debug('Redisplay call')
		self._activity_event()
		if not self._note_history: return 0
		note = self._note_history.pop()
		return self.display(note, redisplay=True)

	def Cleanup(self, timeout, max_count):
		log.debug( 'NotificationCleanup call'
			' (timeout=%.1fs, max_count=%s)', timeout, max_count )
		self._activity_event()
		if max_count <= 0: max_count = None
		ts_min = time() - timeout
		for nid, note in sorted(self._note_windows.viewitems(), key=lambda t: t[1].created):
			if note.created > ts_min: break
			self.close(nid, reason=close_reasons.closed)
			if max_count is not None:
				max_count -= 1
				if max_count <= 0: break


	def _notify_pubsub(self, _fd, _ev):
		try:
			while True:
				msg = self.pubsub.recv()
				if msg is None: break
				self._activity_event()
				note = msg.note
				prefix, ts_diff = msg.hostname, time() - msg.ts
				if ts_diff > 15 * 60: # older than 15min
					prefix = '{}[{}]'.format(prefix, ts_diff_format(ts_diff))
				note.summary = '{} // {}'.format(prefix, note.summary)
				note.hints['x-nt-from-remote'] = msg.hostname
				self.filter_display(note)
		except: log.exception('Unhandled error with remote notification')
		finally: return True # for glib to keep watcher

	def Notify(self, app_name, nid, icon, summary, body, actions, hints, timeout):
		self._activity_event()
		try:
			note = core.Notification.from_dbus(
				app_name, nid, icon, summary, body, actions, hints, timeout )
			if self.pubsub:
				try: self._note_plaintext(note) # make sure plain version is cached
				except Exception as err:
					log.info('Failed to attach plain version to net message: %s', err)
				self.pubsub.send(note)
			if nid: self.close(nid, reason=close_reasons.closed)
			return self.filter_display(note)
		except Exception:
			log.exception('Unhandled error')
			return 0

	def CloseNotification(self, nid):
		log.debug('CloseNotification call (id: %s)', nid)
		self._activity_event()
		self.close(nid, reason=close_reasons.closed)


	_filter_ts_chk = 0
	_filter_callback = None, 0

	def _notification_check(self, summary, body):
		(cb, mtime), ts = self._filter_callback, time()
		if self._filter_ts_chk < ts - poll_interval:
			self._filter_ts_chk = ts
			try: ts = int(os.stat(optz.filter_file).st_mtime)
			except (OSError, IOError): return True
			if ts > mtime:
				mtime = ts
				try: cb = core.get_filter(optz.filter_file, optz.filter_sound)
				except:
					ex, self._filter_callback = traceback.format_exc(), (None, 0)
					log.debug( 'Failed to load'
						' notification filters (from %s):\n%s', optz.filter_file, ex )
					if optz.status_notify:
						self.display('notification-thing: failed to load notification filters', ex)
					return True
				else:
					log.debug('(Re)Loaded notification filters')
					self._filter_callback = cb, mtime
		if cb is None: return True # no filtering defined
		elif not callable(cb): return bool(cb)
		try: return cb(summary, body)
		except:
			ex = traceback.format_exc()
			log.debug('Failed to execute notification filters:\n%s', ex)
			if optz.status_notify:
				self.display('notification-thing: notification filters failed', ex)
			return True

	def _note_plaintext(self, note):
		note_plain = note.get('plain')
		if note_plain: summary, body = note_plain
		else:
			summary, body = self._renderer.get_note_text(note)
			note.plain = summary, body
		return summary, body

	def _fullscreen_check(self, jitter=5):
		screen = Gdk.Screen.get_default()
		win = screen.get_active_window()
		if not win: return False
		win_state = win.get_state()
		w, h = win.get_width(), win.get_height()
		# get_geometry fails with "BadDrawable" from X if the window is closing,
		#  and x/y parameters there are not absolute and useful anyway.
		# x, y, w, h = win.get_geometry()
		return (win_state & win_state.FULLSCREEN)\
			or (w >= screen.get_width() - jitter and h >= screen.get_height() - jitter)

	def filter_display(self, note):
		'Main method which all notifications are passed to for processing/display.'
		note_summary, note_body = self._note_plaintext(note)
		filter_pass = self._notification_check(note_summary, note_body)
		try: urgency = int(note.hints['urgency'])
		except (KeyError, ValueError): urgency = None

		if self.logger and (filter_pass or optz.log_filtered):
			try: self.logger.write(note_summary, note_body, urgency=urgency, ts=note.created)
			except:
				ex = traceback.format_exc()
				log.debug('Notification logger failed:\n%s', ex)
				if optz.status_notify: self.display('notification-thing: notification logger failed', ex)

		if not filter_pass:
			log.debug('Dropped notification due to negative filtering result: %r', note_summary)
			return 0

		if optz.urgency_check and urgency == core.urgency_levels.critical:
			self._note_limit.consume()
			log.debug('Urgent message immediate passthru, tokens left: %s', self._note_limit.tokens)
			return self.display(note)

		plug = self.plugged or (optz.fs_check and self._fullscreen_check())
		if plug or not self._note_limit.consume(): # Delay notification
			to = self._note_limit.get_eta() if not plug else poll_interval
			self._note_buffer.append(note)
			log.debug( 'Queueing notification. Reason: %s. Flush attempt in %ss',
				'plug or fullscreen window detected' if plug else 'notification rate limit', to )
			self.flush(timeout=to)
			return 0

		if self._note_buffer:
			self._note_buffer.append(note)
			log.debug('Token-flush of notification buffer')
			self.flush()
			return 0
		else:
			log.debug('Token-pass, %s token(s) left', self._note_limit.tokens)
			return self.display(note)


	_flush_timer = _flush_id = None

	def flush(self, force=False, timeout=None):
		if self._flush_timer:
			GLib.source_remove(self._flush_timer)
			self._flush_timer = None
		if timeout:
			log.debug('Scheduled notification buffer flush in %ss', timeout)
			self._flush_timer = GLib.timeout_add(int(timeout * 1000), self.flush)
			return
		if not self._note_buffer:
			log.debug('Flush event with empty notification buffer')
			return

		log.debug(
			'Flushing notification buffer (%s msgs, %s dropped)',
			len(self._note_buffer), self._note_buffer.dropped )

		self._note_limit.consume(force=True)
		if not force:
			if optz.fs_check and (self.plugged or self._fullscreen_check()):
				log.debug( '%s detected, delaying buffer flush by %ss',
					('Fullscreen window' if not self.plugged else 'Plug'), poll_interval )
				self.flush(timeout=poll_interval)
				return

		if self._note_buffer:
			# Decided not to use replace_id here - several feeds are okay
			self._flush_id = self.display( self._note_buffer[0]\
				if len(self._note_buffer) == 1\
				else core.Notification.system_message(
					'Feed' if not self._note_buffer.dropped
						else 'Feed ({} dropped)'.format(self._note_buffer.dropped),
					'\n\n'.join(it.starmap( '--- {}\n  {}'.format,
						it.imap(op.itemgetter('summary', 'body'), self._note_buffer) )),
					app_name='notification-feed', icon=optz.feed_icon ) )
			self._note_buffer.flush()
			log.debug('Notification buffer flushed')


	def display(self, note_or_summary, body='', redisplay=False):
		if isinstance(note_or_summary, core.Notification):
			if body:
				raise TypeError('Either Notification object or summary/body should be passed, not both.')
			note = note_or_summary
		else:
			note = core.Notification.system_message(note_or_summary, body)

		if not redisplay:
			clone = note.clone()
			clone.display_time = time()
			self._note_history.append(clone)
		else:
			ts = getattr(note, 'display_time', None)
			if ts: note.body += '\n\n[from {}]'.format(ts_diff_format(time() - ts, add_ago=True))

		if note.replaces_id in self._note_windows:
			self.close(note.replaces_id, close_reasons.closed)
			note.id = self._note_windows[note.replaces_id]
		else: note.id = next(self._note_id_pool)
		nid = note.id

		self._renderer.display( note,
			cb_hover=ft.partial(self.close, delay=True),
			cb_leave=ft.partial(self.close, delay=False),
			cb_dismiss=ft.partial(self.close, reason=close_reasons.dismissed) )
		self._note_windows[nid] = note

		if self.timeout_cleanup and note.timeout > 0:
			note.timer_created, note.timer_left = time(), note.timeout / 1000.0
			note.timer_id = GLib.timeout_add(
				note.timeout, self.close, nid, close_reasons.expired )

		log.debug(
			'Created notification (id: %s, timeout: %s (ms))',
			nid, self.timeout_cleanup and note.timeout )
		return nid


	def close(self, nid=None, reason=close_reasons.undefined, delay=None):
		if nid:
			note = self._note_windows.get(nid, None)
			if note:
				if getattr(note, 'timer_id', None):
					GLib.source_remove(note.timer_id)

				if delay is None: del self._note_windows[nid]
				elif hasattr(note, 'timer_id'): # these get sent very often
					if delay:
						if note.timer_id:
							note.timer_id, note.timer_left = None,\
								note.timer_left - (time() - note.timer_created)
					else:
						note.timer_created = time()
						note.timer_id = GLib.timeout_add(
							int(max(note.timer_left, 1) * 1000),
							self.close, nid, close_reasons.expired )
					return

			if delay is None: # try it, even if there's no note object
				log.debug(
					'Closing notification(s) (id: %s, reason: %s)',
					nid, close_reasons.by_id(reason) )
				try: self._renderer.close(nid)
				except self._renderer.NoWindowError: pass # no such window
				else: self.NotificationClosed(nid, reason)

		else: # close all of them
			for nid in self._note_windows.keys(): self.close(nid, reason)
Exemplo n.º 6
0
class NotificationDaemon(dbus.service.Object):
    plugged, timeout_cleanup = False, True
    _activity_timer = None

    def __init__(self, *argz, **kwz):
        tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
        super(NotificationDaemon, self).__init__(*argz, **kwz)
        self._note_limit = core.FC_TokenBucket(
            tick=optz.tbf_tick,
            burst=optz.tbf_size,
            tick_strangle=lambda x: min(x * optz.tbf_inc, tick_strangle_max),
            tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1))
        self._note_buffer = core.RRQ(optz.queue_len)
        self._note_windows = dict()
        self._note_id_pool = it.chain.from_iterable(
            it.imap(ft.partial(xrange, 1), it.repeat(2**30)))
        self._renderer = NotificationDisplay(optz.layout_margin,
                                             optz.layout_anchor,
                                             optz.layout_direction, optz.img_w,
                                             optz.img_h)
        self._activity_event()

    def exit(self, reason=None):
        log.debug('Exiting cleanly{}'.format(
            ', reason: {}'.format(reason) if reason else ''))
        sys.exit()

    def _activity_event(self, callback=False):
        if callback:
            if not self._note_windows:
                self.exit(reason='activity timeout ({}s)'.format(
                    optz.activity_timeout))
            else:
                log.debug('Ignoring inacivity timeout event'
                          ' due to existing windows (retry in {}s).'.format(
                              optz.activity_timeout))
                self._activity_timer = None
        if self._activity_timer: GObject.source_remove(self._activity_timer)
        self._activity_timer = GObject.timeout_add_seconds(
            optz.activity_timeout, self._activity_event, True)

    _dbus_method = ft.partial(dbus.service.method, core.dbus_id)
    _dbus_signal = ft.partial(dbus.service.signal, core.dbus_id)

    @_dbus_method('', 'ssss')
    def GetServerInformation(self):
        self._activity_event()
        return 'Notifications', 'freedesktop.org', '0.1', '0.7.1'

    @_dbus_method('', 'as')
    def GetCapabilities(self):
        # action-icons, actions, body, body-hyperlinks, body-images,
        #  body-markup, icon-multi, icon-static, persistence, sound
        self._activity_event()
        return ['body', 'persistence', 'icon-static']

    @_dbus_signal('uu')
    def NotificationClosed(self, nid, reason):
        log.debug(
         'NotificationClosed signal (id: {}, reason: {})'\
         .format(nid, close_reasons.by_id(reason)) )

    @_dbus_signal('us')
    def ActionInvoked(self, nid, action_key):
        log.debug('Um... some action invoked? Params: {}'.format(
            [nid, action_key]))

    @_dbus_method('', '')
    def Flush(self):
        log.debug('Manual flush of the notification buffer')
        self._activity_event()
        return self.flush(force=True)

    @_dbus_method('a{sb}', '')
    def Set(self, params):
        self._activity_event()
        # Urgent-passthrough controls
        if params.pop('urgent_toggle', None):
            params['urgent'] = not optz.urgency_check
        try:
            val = params.pop('urgent')
        except KeyError:
            pass
        else:
            optz.urgency_check = val
            if optz.status_notify:
                self.display('Urgent messages passthrough {}'.format(
                    'enabled' if optz.urgency_check else 'disabled'))
        # Plug controls
        if params.pop('plug_toggle', None): params['plug'] = not self.plugged
        try:
            val = params.pop('plug')
        except KeyError:
            pass
        else:
            if val:
                self.plugged = True
                log.debug('Notification queue plugged')
                if optz.status_notify:
                    self.display(
                        'Notification proxy: queue is plugged',
                        'Only urgent messages will be passed through' if
                        optz.urgency_check else 'All messages will be stalled')
            else:
                self.plugged = False
                log.debug('Notification queue unplugged')
                if optz.status_notify:
                    self.display('Notification proxy: queue is unplugged')
                if self._note_buffer:
                    log.debug('Flushing plugged queue')
                    self.flush()
        # Timeout override
        if params.pop('cleanup_toggle', None):
            params['cleanup'] = not self.timeout_cleanup
        try:
            val = params.pop('cleanup')
        except KeyError:
            pass
        else:
            self.timeout_cleanup = val
            log.debug('Cleanup timeout: {}'.format(self.timeout_cleanup))
            if optz.status_notify:
                self.display( 'Notification proxy: cleanup timeout is {}'\
                 .format('enabled' if self.timeout_cleanup else 'disabled') )
        # Notify about malformed arguments, if any
        if params and optz.status_notify:
            self.display('Notification proxy: unrecognized parameters',
                         repr(params))

    @_dbus_method('susssasa{sv}i', 'u')
    def Notify(self, app_name, nid, icon, summary, body, actions, hints,
               timeout):
        self._activity_event()
        note = core.Notification.from_dbus(app_name, nid, icon, summary, body,
                                           actions, hints, timeout)
        if nid: self.close(nid, reason=close_reasons.closed)
        try:
            return self.filter(note)
        except Exception:
            log.exception('Unhandled error')
            return 0

    @_dbus_method('u', '')
    def CloseNotification(self, nid):
        log.debug('CloseNotification call (id: {})'.format(nid))
        self._activity_event()
        self.close(nid, reason=close_reasons.closed)

    _filter_ts_chk = 0
    _filter_callback = None, 0

    def _notification_check(self, summary, body):
        (cb, mtime), ts = self._filter_callback, time()
        if self._filter_ts_chk < ts - poll_interval:
            self._filter_ts_chk = ts
            try:
                ts = int(os.stat(optz.filter_file).st_mtime)
            except (OSError, IOError):
                return True
            if ts > mtime:
                mtime = ts
                try:
                    cb = core.get_filter(optz.filter_file)
                except:
                    ex, self._filter_callback = core.ext_traceback(), (None, 0)
                    log.debug('Failed to load'
                              ' notification filters (from {}):\n{}'.format(
                                  optz.filter_file, ex))
                    if optz.status_notify:
                        self.display(
                            'Notification proxy: failed to load notification filters',
                            ex)
                    return True
                else:
                    log.debug('(Re)Loaded notification filters')
                    self._filter_callback = cb, mtime
        if cb is None: return True  # no filtering defined
        elif not callable(cb): return bool(cb)
        try:
            return cb(summary, body)
        except:
            ex = core.ext_traceback()
            log.debug('Failed to execute notification filters:\n{}'.format(ex))
            if optz.status_notify:
                self.display('Notification proxy: notification filters failed',
                             ex)
            return True

    @property
    def _fullscreen_check(self, jitter=5):
        screen = Gdk.Screen.get_default()
        win = screen.get_active_window()
        win_state = win.get_state()
        x, y, w, h = win.get_geometry()
        return win_state & win_state.FULLSCREEN\
         or ( x <= jitter and y <= jitter
          and w >= screen.get_width() - jitter
          and h >= screen.get_height() - jitter )

    def filter(self, note):
        # TODO: also, just update timeout if content is the same as one of the displayed
        try:
            urgency = int(note.hints['urgency'])
        except KeyError:
            urgency = None

        plug = self.plugged or (optz.fs_check and self._fullscreen_check)
        urgent = optz.urgency_check and urgency == core.urgency_levels.critical

        if urgent:  # special case - no buffer checks
            self._note_limit.consume()
            log.debug('Urgent message immediate passthru'
                      ', tokens left: {}'.format(self._note_limit.tokens))
            return self.display(note)

        if not self._notification_check(note.summary, note.body):
            log.debug('Dropped notification due to negative filtering result')
            return 0

        if plug or not self._note_limit.consume():
            # Delay notification
            to = self._note_limit.get_eta() if not plug else poll_interval
            if to > 1:  # no need to bother otherwise, note that it'll be an extra token ;)
                self._note_buffer.append(note)
                to = to + 1  # +1 is to ensure token arrival by that time
                log.debug( 'Queueing notification. Reason: {}. Flush attempt in {}s'\
                 .format('plug or fullscreen window detected' if plug else 'notification rate limit', to) )
                self.flush(timeout=to)
                return 0

        if self._note_buffer:
            self._note_buffer.append(note)
            log.debug('Token-flush of notification buffer')
            self.flush()
            return 0
        else:
            log.debug('Token-pass, {} token(s) left'.format(
                self._note_limit.tokens))
            return self.display(note)

    _flush_timer = _flush_id = None

    def flush(self, force=False, timeout=None):
        if self._flush_timer:
            GObject.source_remove(self._flush_timer)
            self._flush_timer = None
        if timeout:
            log.debug(
                'Scheduled notification buffer flush in {}s'.format(timeout))
            self._flush_timer = GObject.timeout_add(int(timeout * 1000),
                                                    self.flush)
            return
        if not self._note_buffer:
            log.debug('Flush event with empty notification buffer')
            return

        log.debug( 'Flushing notification buffer ({} msgs, {} dropped)'\
         .format(len(self._note_buffer), self._note_buffer.dropped) )

        self._note_limit.consume()
        if not force:
            if optz.fs_check and (self.plugged or self._fullscreen_check):
                log.debug( '{} detected, delaying buffer flush by {}s'\
                 .format(( 'Fullscreen window'
                  if not self.plugged else 'Plug' ), poll_interval) )
                self.flush(timeout=poll_interval)
                return

        if self._note_buffer:
            # Decided not to use replace_id here - several feeds are okay
            self._flush_id = self.display( self._note_buffer[0]\
             if len(self._note_buffer) == 1 else core.Notification(
              'Feed' if not self._note_buffer.dropped
               else 'Feed ({} dropped)'.format(self._note_buffer.dropped),
              '\n\n'.join(it.starmap( '--- {}\n  {}'.format,
               it.imap(op.itemgetter('summary', 'body'), self._note_buffer) )),
              app_name='notification-feed', icon='FBReader' ) )
            self._note_buffer.flush()
            log.debug('Notification buffer flushed')

    def display(self, note_or_summary, *argz, **kwz):
        note = note_or_summary\
         if isinstance(note_or_summary, core.Notification)\
         else core.Notification(note_or_summary, *argz, **kwz)

        if note.replaces_id in self._note_windows:
            self.close(note.replaces_id, close_reasons.closed)
            note.id = self._note_windows[note.replaces_id]
        else:
            note.id = next(self._note_id_pool)
        nid = note.id

        self._renderer.display(note,
                               cb_hover=ft.partial(self.close, delay=True),
                               cb_leave=ft.partial(self.close, delay=False),
                               cb_dismiss=ft.partial(
                                   self.close, reason=close_reasons.dismissed))
        self._note_windows[nid] = note

        if self.timeout_cleanup and note.timeout > 0:
            note.timer_created, note.timer_left = time(), note.timeout / 1000.0
            note.timer_id = GObject.timeout_add(note.timeout, self.close, nid,
                                                close_reasons.expired)

        log.debug( 'Created notification (id: {}, timeout: {} (ms))'\
         .format(nid, self.timeout_cleanup and note.timeout) )
        return nid

    def close(self, nid=None, reason=close_reasons.undefined, delay=None):
        if nid:
            note = self._note_windows.get(nid, None)
            if note:
                if getattr(note, 'timer_id', None):
                    GObject.source_remove(note.timer_id)

                if delay is None: del self._note_windows[nid]
                elif 'timer_id' in note:  # these get sent very often
                    if delay:
                        if note.timer_id:
                            note.timer_id, note.timer_left = None,\
                             note.timer_left - (time() - note.timer_created)
                    else:
                        note.timer_created = time()
                        note.timer_id = GObject.timeout_add(
                            int(max(note.timer_left, 1) * 1000), self.close,
                            nid, close_reasons.expired)
                    return

            if delay is None:  # try it, even if there's no note object
                log.debug(
                 'Closing notification(s) (id: {}, reason: {})'\
                 .format(nid, close_reasons.by_id(reason)) )
                try:
                    self._renderer.close(nid)
                except self._renderer.NoWindowError:
                    pass  # no such window
                else:
                    self.NotificationClosed(nid, reason)

        else:  # close all of them
            for nid in self._note_windows.keys():
                self.close(nid, reason)
Exemplo n.º 7
0
class NotificationDaemon(dbus.service.Object):
	plugged, timeout_cleanup = False, True
	_activity_timer = None

	def __init__(self, *argz, **kwz):
		tick_strangle_max = op.truediv(optz.tbf_max_delay, optz.tbf_tick)
		super(NotificationDaemon, self).__init__(*argz, **kwz)
		self._note_limit = core.FC_TokenBucket(
			tick=optz.tbf_tick, burst=optz.tbf_size,
			tick_strangle=lambda x: min(x*optz.tbf_inc, tick_strangle_max),
			tick_free=lambda x: max(op.truediv(x, optz.tbf_dec), 1) )
		self._note_buffer = core.RRQ(optz.queue_len)
		self._note_windows = dict()
		self._note_id_pool = it.chain.from_iterable(
			it.imap(ft.partial(xrange, 1), it.repeat(2**30)) )
		self._renderer = NotificationDisplay(
			optz.layout_margin, optz.layout_anchor, optz.layout_direction, optz.img_w, optz.img_h)
		self._activity_event()


	def exit(self, reason=None):
		log.debug('Exiting cleanly{}'.format(', reason: {}'.format(reason) if reason else ''))
		sys.exit()

	def _activity_event(self, callback=False):
		if callback:
			if not self._note_windows:
				self.exit(reason='activity timeout ({}s)'.format(optz.activity_timeout))
			else:
				log.debug( 'Ignoring inacivity timeout event'
					' due to existing windows (retry in {}s).'.format(optz.activity_timeout) )
				self._activity_timer = None
		if self._activity_timer: GObject.source_remove(self._activity_timer)
		self._activity_timer = GObject.timeout_add_seconds(
			optz.activity_timeout, self._activity_event, True )

	_dbus_method = ft.partial(dbus.service.method, core.dbus_id)
	_dbus_signal = ft.partial(dbus.service.signal, core.dbus_id)


	@_dbus_method('', 'ssss')
	def GetServerInformation(self):
		self._activity_event()
		return 'Notifications', 'freedesktop.org', '0.1', '0.7.1'

	@_dbus_method('', 'as')
	def GetCapabilities(self):
		# action-icons, actions, body, body-hyperlinks, body-images,
		#  body-markup, icon-multi, icon-static, persistence, sound
		self._activity_event()
		return ['body', 'persistence', 'icon-static']

	@_dbus_signal('uu')
	def NotificationClosed(self, nid, reason):
		log.debug(
			'NotificationClosed signal (id: {}, reason: {})'\
			.format(nid, close_reasons.by_id(reason)) )

	@_dbus_signal('us')
	def ActionInvoked(self, nid, action_key):
		log.debug('Um... some action invoked? Params: {}'.format([nid, action_key]))


	@_dbus_method('', '')
	def Flush(self):
		log.debug('Manual flush of the notification buffer')
		self._activity_event()
		return self.flush(force=True)

	@_dbus_method('a{sb}', '')
	def Set(self, params):
		self._activity_event()
		# Urgent-passthrough controls
		if params.pop('urgent_toggle', None): params['urgent'] = not optz.urgency_check
		try: val = params.pop('urgent')
		except KeyError: pass
		else:
			optz.urgency_check = val
			if optz.status_notify:
				self.display(
					'Urgent messages passthrough {}'.format(
						'enabled' if optz.urgency_check else 'disabled' ) )
		# Plug controls
		if params.pop('plug_toggle', None): params['plug'] = not self.plugged
		try: val = params.pop('plug')
		except KeyError: pass
		else:
			if val:
				self.plugged = True
				log.debug('Notification queue plugged')
				if optz.status_notify:
					self.display( 'Notification proxy: queue is plugged',
						'Only urgent messages will be passed through'
							if optz.urgency_check else 'All messages will be stalled' )
			else:
				self.plugged = False
				log.debug('Notification queue unplugged')
				if optz.status_notify:
					self.display('Notification proxy: queue is unplugged')
				if self._note_buffer:
					log.debug('Flushing plugged queue')
					self.flush()
		# Timeout override
		if params.pop('cleanup_toggle', None): params['cleanup'] = not self.timeout_cleanup
		try: val = params.pop('cleanup')
		except KeyError: pass
		else:
			self.timeout_cleanup = val
			log.debug('Cleanup timeout: {}'.format(self.timeout_cleanup))
			if optz.status_notify:
				self.display( 'Notification proxy: cleanup timeout is {}'\
					.format('enabled' if self.timeout_cleanup else 'disabled') )
		# Notify about malformed arguments, if any
		if params and optz.status_notify:
			self.display('Notification proxy: unrecognized parameters', repr(params))

	@_dbus_method('susssasa{sv}i', 'u')
	def Notify(self, app_name, nid, icon, summary, body, actions, hints, timeout):
		self._activity_event()
		note = core.Notification.from_dbus(
			app_name, nid, icon, summary, body, actions, hints, timeout )
		if nid: self.close(nid, reason=close_reasons.closed)
		try: return self.filter(note)
		except Exception:
			log.exception('Unhandled error')
			return 0

	@_dbus_method('u', '')
	def CloseNotification(self, nid):
		log.debug('CloseNotification call (id: {})'.format(nid))
		self._activity_event()
		self.close(nid, reason=close_reasons.closed)


	_filter_ts_chk = 0
	_filter_callback = None, 0

	def _notification_check(self, summary, body):
		(cb, mtime), ts = self._filter_callback, time()
		if self._filter_ts_chk < ts - poll_interval:
			self._filter_ts_chk = ts
			try: ts = int(os.stat(optz.filter_file).st_mtime)
			except (OSError, IOError): return True
			if ts > mtime:
				mtime = ts
				try: cb = core.get_filter(optz.filter_file)
				except:
					ex, self._filter_callback = core.ext_traceback(), (None, 0)
					log.debug( 'Failed to load'
						' notification filters (from {}):\n{}'.format(optz.filter_file, ex) )
					if optz.status_notify:
						self.display('Notification proxy: failed to load notification filters', ex)
					return True
				else:
					log.debug('(Re)Loaded notification filters')
					self._filter_callback = cb, mtime
		if cb is None: return True # no filtering defined
		elif not callable(cb): return bool(cb)
		try: return cb(summary, body)
		except:
			ex = core.ext_traceback()
			log.debug('Failed to execute notification filters:\n{}'.format(ex))
			if optz.status_notify:
				self.display('Notification proxy: notification filters failed', ex)
			return True

	@property
	def _fullscreen_check(self, jitter=5):
		screen = Gdk.Screen.get_default()
		win = screen.get_active_window()
		win_state = win.get_state()
		x,y,w,h = win.get_geometry()
		return win_state & win_state.FULLSCREEN\
			or ( x <= jitter and y <= jitter
				and w >= screen.get_width() - jitter
				and h >= screen.get_height() - jitter )

	def filter(self, note):
		# TODO: also, just update timeout if content is the same as one of the displayed
		try: urgency = int(note.hints['urgency'])
		except KeyError: urgency = None

		plug = self.plugged or (optz.fs_check and self._fullscreen_check)
		urgent = optz.urgency_check and urgency == core.urgency_levels.critical

		if urgent: # special case - no buffer checks
			self._note_limit.consume()
			log.debug( 'Urgent message immediate passthru'
				', tokens left: {}'.format(self._note_limit.tokens) )
			return self.display(note)

		if not self._notification_check(note.summary, note.body):
			log.debug('Dropped notification due to negative filtering result')
			return 0

		if plug or not self._note_limit.consume():
			# Delay notification
			to = self._note_limit.get_eta() if not plug else poll_interval
			if to > 1: # no need to bother otherwise, note that it'll be an extra token ;)
				self._note_buffer.append(note)
				to = to + 1 # +1 is to ensure token arrival by that time
				log.debug( 'Queueing notification. Reason: {}. Flush attempt in {}s'\
					.format('plug or fullscreen window detected' if plug else 'notification rate limit', to) )
				self.flush(timeout=to)
				return 0

		if self._note_buffer:
			self._note_buffer.append(note)
			log.debug('Token-flush of notification buffer')
			self.flush()
			return 0
		else:
			log.debug('Token-pass, {} token(s) left'.format(self._note_limit.tokens))
			return self.display(note)


	_flush_timer = _flush_id = None

	def flush(self, force=False, timeout=None):
		if self._flush_timer:
			GObject.source_remove(self._flush_timer)
			self._flush_timer = None
		if timeout:
			log.debug('Scheduled notification buffer flush in {}s'.format(timeout))
			self._flush_timer = GObject.timeout_add(int(timeout * 1000), self.flush)
			return
		if not self._note_buffer:
			log.debug('Flush event with empty notification buffer')
			return

		log.debug( 'Flushing notification buffer ({} msgs, {} dropped)'\
			.format(len(self._note_buffer), self._note_buffer.dropped) )

		self._note_limit.consume()
		if not force:
			if optz.fs_check and (self.plugged or self._fullscreen_check):
				log.debug( '{} detected, delaying buffer flush by {}s'\
					.format(( 'Fullscreen window'
						if not self.plugged else 'Plug' ), poll_interval) )
				self.flush(timeout=poll_interval)
				return

		if self._note_buffer:
			# Decided not to use replace_id here - several feeds are okay
			self._flush_id = self.display( self._note_buffer[0]\
				if len(self._note_buffer) == 1 else core.Notification(
					'Feed' if not self._note_buffer.dropped
						else 'Feed ({} dropped)'.format(self._note_buffer.dropped),
					'\n\n'.join(it.starmap( '--- {}\n  {}'.format,
						it.imap(op.itemgetter('summary', 'body'), self._note_buffer) )),
					app_name='notification-feed', icon='FBReader' ) )
			self._note_buffer.flush()
			log.debug('Notification buffer flushed')


	def display(self, note_or_summary, *argz, **kwz):
		note = note_or_summary\
			if isinstance(note_or_summary, core.Notification)\
			else core.Notification(note_or_summary, *argz, **kwz)

		if note.replaces_id in self._note_windows:
			self.close(note.replaces_id, close_reasons.closed)
			note.id = self._note_windows[note.replaces_id]
		else: note.id = next(self._note_id_pool)
		nid = note.id

		self._renderer.display( note,
			cb_hover=ft.partial(self.close, delay=True),
			cb_leave=ft.partial(self.close, delay=False),
			cb_dismiss=ft.partial(self.close, reason=close_reasons.dismissed) )
		self._note_windows[nid] = note

		if self.timeout_cleanup and note.timeout > 0:
			note.timer_created, note.timer_left = time(), note.timeout / 1000.0
			note.timer_id = GObject.timeout_add(
				note.timeout, self.close, nid, close_reasons.expired )

		log.debug( 'Created notification (id: {}, timeout: {} (ms))'\
			.format(nid, self.timeout_cleanup and note.timeout) )
		return nid

	def close(self, nid=None, reason=close_reasons.undefined, delay=None):
		if nid:
			note = self._note_windows.get(nid, None)
			if note:
				if getattr(note, 'timer_id', None): GObject.source_remove(note.timer_id)

				if delay is None: del self._note_windows[nid]
				elif 'timer_id' in note: # these get sent very often
					if delay:
						if note.timer_id:
							note.timer_id, note.timer_left = None,\
								note.timer_left - (time() - note.timer_created)
					else:
						note.timer_created = time()
						note.timer_id = GObject.timeout_add(
							int(max(note.timer_left, 1) * 1000),
							self.close, nid, close_reasons.expired )
					return

			if delay is None: # try it, even if there's no note object
				log.debug(
					'Closing notification(s) (id: {}, reason: {})'\
					.format(nid, close_reasons.by_id(reason)) )
				try: self._renderer.close(nid)
				except self._renderer.NoWindowError: pass # no such window
				else: self.NotificationClosed(nid, reason)

		else: # close all of them
			for nid in self._note_windows.keys(): self.close(nid, reason)