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)
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 __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)
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()
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)
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)
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)