[gnome-clocks/wip/laney/systemd-alarms] Activate alarms using systemd timers



commit 3ca3b5cc17534b275013a8e82bcb26c56fd367cd
Author: Iain Lane <iain orangesquash org uk>
Date:   Wed Jan 31 15:37:30 2018 +0000

    Activate alarms using systemd timers
    
    This is a reworking of the way that alarms are activated, which allows
    for alarms to be sounded when Clocks is closed. The way it works is like
    this:
    
    When the session is started up, a small binary is invoked which starts
    gnome-clocks just enough to load the list of alarms, create transient
    systemd units for them, and then exit. These units are created, updated
    and destroyed when the alarm is edited in the UI.
    
    To fire an alarm, we start the same small binary which invokes a GAction
    on gnome-clocks to ring the alarm in question.
    
    After this commit, Clocks depends on systemd in the session for its
    alarm functionality.

 data/gnome-clocks-alarm  service in  |   11 ++
 data/meson.build                     |   10 ++
 meson.build                          |    4 +
 src/alarm.vala                       |  185 +++++++++++++++++++++++----------
 src/application.vala                 |   25 +++++-
 src/gnome-clocks-activate-alarm.vala |   58 +++++++++++
 src/meson.build                      |   26 +++++
 src/systemd.vala                     |   57 +++++++++++
 src/window.vala                      |    5 +
 9 files changed, 324 insertions(+), 57 deletions(-)
---
diff --git a/data/gnome-clocks-alarm  service in b/data/gnome-clocks-alarm  service in
new file mode 100644
index 0000000..92ff613
--- /dev/null
+++ b/data/gnome-clocks-alarm  service in
@@ -0,0 +1,11 @@
+[Unit]
+Description=Sound a GNOME Clocks alarm (%i)
+PartOf=graphical-session.target
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/gnome-clocks-activate-alarm '%i'
+
+[Install]
+DefaultInstance=all
+WantedBy=graphical-session.target
diff --git a/data/meson.build b/data/meson.build
index 382f109..50a109a 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -53,3 +53,13 @@ configure_file(
   install: true,
   install_dir: join_paths(get_option('datadir'), 'glib-2.0', 'schemas'),
 )
+
+systemdconf = configuration_data()
+systemdconf.set('libexecdir', join_paths(get_option('prefix'), get_option('libexecdir')))
+configure_file(
+  input: 'gnome-clocks-alarm  service in',
+  output: 'gnome-clocks-alarm@.service',
+  configuration: systemdconf,
+  install: true,
+  install_dir: systemd_userunitdir
+)
diff --git a/meson.build b/meson.build
index 64e6e37..a4715a0 100644
--- a/meson.build
+++ b/meson.build
@@ -19,6 +19,10 @@ gweather = dependency('gweather-3.0', version: '>=3.27.2')
 gnomedesktop = dependency('gnome-desktop-3.0', version: '>=3.8')
 geocodeglib = dependency('geocode-glib-1.0', version: '>=1.0')
 libgeoclue = dependency('libgeoclue-2.0', version: '>=2.4')
+posix = meson.get_compiler('vala').find_library('posix')
+
+systemd = dependency('systemd')
+systemd_userunitdir = systemd.get_pkgconfig_variable('systemduserunitdir')
 
 cc = meson.get_compiler('c')
 math = cc.find_library('m', required: false)
diff --git a/src/alarm.vala b/src/alarm.vala
index 001ebc0..316b2b5 100644
--- a/src/alarm.vala
+++ b/src/alarm.vala
@@ -16,17 +16,19 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+using Clocks.Systemd;
+
 namespace Clocks {
 namespace Alarm {
 
-private struct AlarmTime {
+public struct AlarmTime {
     public int hour;
     public int minute;
 }
 
-private class Item : Object, ContentItem {
+public class Item : Object, ContentItem {
     const int SNOOZE_MINUTES = 9;
-    const int RING_MINUTES = 3;
+    const int RING_SECONDS = 3 * 60;
 
     // FIXME: should we add a "MISSED" state where the alarm stopped
     // ringing but we keep showing the ringing panel?
@@ -100,15 +102,109 @@ private class Item : Object, ContentItem {
 
     private string _name;
     private bool _active;
-    private GLib.DateTime alarm_time;
+    public GLib.DateTime alarm_time { get; private set; }
     private GLib.DateTime snooze_time;
-    private GLib.DateTime ring_end_time;
     private Utils.Bell bell;
     private GLib.Notification notification;
+    private Systemd.SystemdManager systemd;
+    private Systemd.SystemdUnit? transient_unit;
 
     public Item (string? id = null) {
         var guid = id != null ? id : GLib.DBus.generate_guid ();
         Object (id: guid);
+        try {
+            systemd = Bus.get_proxy_sync (BusType.SESSION,
+                                          "org.freedesktop.systemd1",
+                                          "/org/freedesktop/systemd1");
+        } catch (IOError e) {
+            GLib.critical ("Failed to get session bus - alarm will not work.");
+            return;
+        }
+
+        try {
+            set_up_transient_unit ();
+        } catch (IOError e) {
+            /* Expected if the alarm wasn't already created by the time we were
+             * launched */
+        }
+
+        this.notify["alarm-time"].connect(start_systemd_timer);
+        this.notify["active"].connect(start_systemd_timer);
+    }
+
+    public string systemd_time {
+        owned get {
+            string ds = "";
+            if (days != null && !days.empty) {
+                string[] d = {};
+                for (int i = 0; i < 7; i++) {
+                    if (days.get ((Utils.Weekdays.Day) i)) {
+                        d += Utils.Weekdays.abbreviation ((Utils.Weekdays.Day) i);
+                    }
+                }
+                ds = "%s ".printf(string.joinv(",", d));
+            }
+            return "%s*-*-* %d:%d:00".printf(ds, time.hour, time.minute);
+        }
+    }
+
+    private string transient_unit_name {
+        owned get {
+            return "gnome-clocks-alarm@%s.timer".printf(id);
+        }
+    }
+
+    private void set_up_transient_unit () throws IOError {
+        var transient_unit_path = systemd.get_unit (transient_unit_name);
+        transient_unit = Bus.get_proxy_sync (BusType.SESSION,
+                                             "org.freedesktop.systemd1",
+                                             transient_unit_path);
+    }
+
+    private void start_systemd_timer () {
+        string trigger_time;
+
+        if (state == State.SNOOZING) {
+            trigger_time = snooze_time.format("%Y-%m-%d %H:%M:%S %Z");
+        } else {
+            trigger_time = systemd_time;
+        }
+
+        Systemd.Property[] properties = {
+            Systemd.Property() { name = "OnCalendar", value = trigger_time },
+            Systemd.Property() { name = "AccuracyUSec", value = 1000000ull /* 1s, must be uint64 (ull) */ }
+        };
+        Systemd.Aux[] aux = new Aux[0];
+
+        if (transient_unit != null) {
+            /* Stop the old one */
+            try {
+                transient_unit.stop ("replace");
+                transient_unit = null;
+            } catch (IOError e) {
+                GLib.critical ("Failed to stop transient unit '%s': %s",
+                               transient_unit_name,
+                               e.message);
+            }
+        }
+
+        if (!active) {
+            /* Not active, so don't create a unit */
+            return;
+        }
+
+        try {
+            systemd.start_transient_unit (transient_unit_name,
+                                          "replace",
+                                          properties,
+                                          aux);
+
+            set_up_transient_unit ();
+        } catch (IOError e) {
+            GLib.critical ("Failed to start transient unit '%s': %s",
+                           transient_unit_name,
+                           e.message);
+        }
     }
 
     private void setup_bell () {
@@ -121,7 +217,6 @@ private class Item : Object, ContentItem {
 
     public void reset () {
         update_alarm_time ();
-        update_snooze_time (alarm_time);
         state = State.READY;
     }
 
@@ -153,31 +248,34 @@ private class Item : Object, ContentItem {
         alarm_time = dt;
     }
 
-    private void update_snooze_time (GLib.DateTime start_time) {
-        snooze_time = start_time.add_minutes (SNOOZE_MINUTES);
-    }
-
     public virtual signal void ring () {
         var app = GLib.Application.get_default () as Clocks.Application;
         app.send_notification ("alarm-clock-elapsed", notification);
         bell.ring ();
+        GLib.Timeout.add_seconds(RING_SECONDS, (() => {
+                                 stop ();
+                                 return GLib.Source.REMOVE;
+        }));
     }
 
-    private void start_ringing (GLib.DateTime now) {
-        update_snooze_time (now);
-        ring_end_time = now.add_minutes (RING_MINUTES);
+    public void start_ringing () {
         state = State.RINGING;
         ring ();
     }
 
     public void snooze () {
+        var wallclock = Utils.WallClock.get_default ();
+        var now = wallclock.date_time;
         bell.stop ();
+
+        snooze_time = now.add_minutes (SNOOZE_MINUTES);
         state = State.SNOOZING;
+
+        start_systemd_timer ();
     }
 
     public void stop () {
         bell.stop ();
-        update_snooze_time (alarm_time);
         state = State.READY;
     }
 
@@ -196,35 +294,6 @@ private class Item : Object, ContentItem {
         return false;
     }
 
-    // Update the state and ringing time. Ring or stop
-    // depending on the current time.
-    // Returns true if the state changed, false otherwise.
-    public bool tick () {
-        if (!active) {
-            return false;
-        }
-
-        State last_state = state;
-
-        var wallclock = Utils.WallClock.get_default ();
-        var now = wallclock.date_time;
-
-        if (state == State.RINGING && now.compare (ring_end_time) > 0) {
-            stop ();
-        }
-
-        if (state == State.SNOOZING && now.compare (snooze_time) > 0) {
-            start_ringing (now);
-        }
-
-        if (state == State.READY && now.compare (alarm_time) > 0) {
-            start_ringing (now);
-            update_alarm_time (); // reschedule for the next repeat
-        }
-
-        return state != last_state;
-    }
-
     public void serialize (GLib.VariantBuilder builder) {
         builder.open (new GLib.VariantType ("a{sv}"));
         builder.add ("{sv}", "name", new GLib.Variant.string (name));
@@ -261,11 +330,15 @@ private class Item : Object, ContentItem {
         }
         if (name != null && hour >= 0 && minute >= 0) {
             Item alarm = new Item (id);
+            /* Don't trigger alarm_time changes until we're done - we update
+             * the systemd unit when we change. */
+            alarm.freeze_notify();
             alarm.name = name;
             alarm.active = active;
             alarm.time = { hour, minute };
             alarm.days = days;
             alarm.reset ();
+            alarm.thaw_notify();
             return alarm;
         } else {
             warning ("Invalid alarm %s", name != null ? name : "name missing");
@@ -493,6 +566,9 @@ private class SetupDialog : Gtk.Dialog {
 
     private void avoid_duplicate_alarm () {
         var alarm = new Item ();
+        /* This isn't a real alarm, don't trigger property change notifications
+         * for it. */
+        alarm.freeze_notify ();
         apply_to_alarm (alarm);
 
         var duplicate = alarm.check_duplicate_alarm (other_alarms);
@@ -642,21 +718,18 @@ public class Face : Gtk.Stack, Clocks.Clock {
         });
 
         reset_view ();
+    }
 
-        // Start ticking...
-        Utils.WallClock.get_default ().tick.connect (() => {
-            alarms.foreach ((i) => {
-                var a = (Item)i;
-                if (a.tick ()) {
-                    if (a.state == Item.State.RINGING) {
-                        show_ringing_panel (a);
-                        ring ();
-                    } else if (ringing_panel.alarm == a) {
-                        ringing_panel.update ();
-                    }
-                }
-            });
+    public void activate_alarm (string alarm_id) {
+        var a = (Item)alarms.find ((a) => {
+            return ((Item)a).id == alarm_id;
         });
+
+        if (a != null) {
+            a.start_ringing ();
+            show_ringing_panel (a);
+            ring ();
+        }
     }
 
     public signal void ring ();
diff --git a/src/application.vala b/src/application.vala
index 32c3e74..df50d90 100644
--- a/src/application.vala
+++ b/src/application.vala
@@ -28,7 +28,9 @@ public class Application : Gtk.Application {
         { "stop-alarm", null, "s" },
         { "snooze-alarm", null, "s" },
         { "quit", on_quit_activate },
-        { "add-location", on_add_location_activate, "v" }
+        { "add-location", on_add_location_activate, "v" },
+        { "activate-alarm", on_activate_alarm_activate, "s" },
+        { "activate-all-alarms", on_activate_all_alarms_activate }
     };
 
     private SearchProvider search_provider;
@@ -140,6 +142,27 @@ public class Application : Gtk.Application {
         }
     }
 
+    public void on_activate_all_alarms_activate (GLib.SimpleAction action, GLib.Variant? parameter) {
+        /* This is run as part of session startup. Just create the alarm
+         * objects, which is enough to get them to set themselves up as systemd
+         * timer units. */
+        var settings = new GLib.Settings ("org.gnome.clocks");
+        var alarms = new ContentStore ();
+        alarms.deserialize (settings.get_value ("alarms"), Alarm.Item.deserialize);
+    }
+
+    public void on_activate_alarm_activate (GLib.SimpleAction action, GLib.Variant? parameter) {
+        if (parameter == null) {
+            return;
+        }
+
+        string alarm_id = parameter.get_string ();
+
+        ensure_window ();
+        window.activate_alarm (alarm_id);
+        window.present ();
+    }
+
     public new void send_notification (string notification_id, GLib.Notification notification) {
         base.send_notification (notification_id, notification);
 
diff --git a/src/gnome-clocks-activate-alarm.vala b/src/gnome-clocks-activate-alarm.vala
new file mode 100644
index 0000000..2fe41b8
--- /dev/null
+++ b/src/gnome-clocks-activate-alarm.vala
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018  Canonical Ltd
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+/* Trivial program to activate the named alarm or set up all alarms as part of
+ * initial login. To be executed by our systemd unit. */
+
+[DBus (name="org.freedesktop.Application")]
+interface Application : Object {
+    public abstract void activate_action (string action_name,
+                                          Variant[] params,
+                                          GLib.HashTable<string, Variant> platform_data)
+        throws IOError;
+}
+
+int main (string[] args) {
+    Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.GNOMELOCALEDIR);
+    Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
+    Intl.textdomain (Config.GETTEXT_PACKAGE);
+
+    if (args.length != 2) {
+        stderr.printf ("Usage: %s [alarm id|all]\n", args[0]);
+        return Posix.EXIT_FAILURE;
+    }
+
+    try {
+        Application proxy = Bus.get_proxy_sync (GLib.BusType.SESSION,
+                                                "org.gnome.clocks",
+                                                "/org/gnome/clocks");
+
+        /* no platform_data */
+        GLib.HashTable<string,Variant> empty = new GLib.HashTable<string, Variant> (null, null);
+        if (args[1] == "all") {
+            proxy.activate_action ("activate-all-alarms", {}, empty);
+        } else {
+            proxy.activate_action ("activate-alarm", {args[1]}, empty);
+        }
+    } catch (IOError e) {
+        GLib.critical ("Failed to activate alarm: %s", e.message);
+        return Posix.EXIT_FAILURE;
+    }
+
+    return Posix.EXIT_SUCCESS;
+}
diff --git a/src/meson.build b/src/meson.build
index 42e59e5..ae381fb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -10,6 +10,7 @@ clocks_vala_sources = [
   'main.vala',
   'search-provider.vala',
   'stopwatch.vala',
+  'systemd.vala',
   'timer.vala',
   'utils.vala',
   'widgets.vala',
@@ -60,3 +61,28 @@ executable('gnome-clocks', clocks_sources,
   dependencies: clocks_dependencies,
   install: true
 )
+
+activate_alarm_sources = [
+    vapi_sources,
+    'gnome-clocks-activate-alarm.vala'
+]
+
+activate_alarm_c_args = [
+  '-include', 'config.h'
+]
+
+activate_alarm_dependencies = [
+  glib,
+  gio,
+  posix
+]
+
+executable('gnome-clocks-activate-alarm',
+           activate_alarm_sources,
+           include_directories: config_h_dir,
+           vala_args: clocks_vala_args,
+           c_args: activate_alarm_c_args,
+           dependencies: activate_alarm_dependencies,
+           install_dir: get_option('libexecdir'),
+           install: true
+)
diff --git a/src/systemd.vala b/src/systemd.vala
new file mode 100644
index 0000000..45191c9
--- /dev/null
+++ b/src/systemd.vala
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018  Canonical Ltd
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace Systemd {
+
+struct Property {
+    public string name;
+    public GLib.Variant value;
+}
+
+struct Aux {
+    public string name;
+    public Property[] value;
+}
+
+[DBus (name="org.freedesktop.systemd1.Manager")]
+interface SystemdManager : Object {
+    /* StartTransientUnit(in  s name,
+                          in  s mode,
+                          in  a(sv) properties,
+                          in  a(sa(sv)) aux,
+                          out o job); */
+    public abstract GLib.ObjectPath start_transient_unit (string name,
+                                                          string mode,
+                                                          Property[] properties,
+                                                          Aux[] aux) throws IOError;
+
+    /* GetUnit(in  s name,
+               out o unit); */
+    public abstract GLib.ObjectPath get_unit (string name) throws IOError;
+}
+
+[DBus (name="org.freedesktop.systemd1.Unit")]
+interface SystemdUnit : Object {
+    /* Stop(in  s mode,
+            out o job); */
+    public abstract GLib.ObjectPath stop (string mode) throws IOError;
+}
+
+} // namespace Clocks
+} // namespace Utils
diff --git a/src/window.vala b/src/window.vala
index 0cf2f7c..e2a39f0 100644
--- a/src/window.vala
+++ b/src/window.vala
@@ -174,6 +174,11 @@ public class Window : Gtk.ApplicationWindow {
         ((World.Face) panels[PanelId.WORLD]).add_location (location);
     }
 
+    public void activate_alarm (string alarm_id) {
+        var alarm_panel = (Alarm.Face)panels[PanelId.ALARM];
+        alarm_panel.activate_alarm (alarm_id);
+    }
+
     public override bool key_press_event (Gdk.EventKey event) {
         uint keyval;
         bool handled = false;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]