Facility for acking/unacking log entries
[gregoa/zavai.git] / src / clock.vala
1 /*
2  * clock - clock resource for zavai
3  *
4  * Copyright (C) 2009--2010  Enrico Zini <enrico@enricozini.org>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20
21 using GLib;
22
23 namespace zavai {
24 namespace clock {
25
26 public enum SourceType
27 {
28     SYSTEM,
29     GPS
30 }
31
32 /*
33 TODO: schedule alarms via at
34
35 Uses the 'z' queue.
36
37 atq -q z  can be used to list the jobs (for example, at startup, or after a job has run)
38 at -c id  can be used to query a job, parsing its contents (which can have
39           comments or variables being set)
40 zavai --notify ...  can be used to notify the job (and start zavai if it's not running)
41
42 Alarm needs to be able to serialize itself to an at invocation and to
43 deserialize itself from the output of at -c
44
45 Alarm needs to deserialize also a job with no special markers whatsoever: a
46 generic at job.
47
48
49
50 refresh_alarms()
51 {
52     oldtime = next_alarm ? next_alarm.time : 0
53     next_alarm = the first alarm from atq
54     if (oldtime != next_alarm.time)
55     {
56         remove existing triggers
57           (triggers can be skipped if we don't need to support non-zavai alarms)
58         schedule a trigger calling refresh_alarms() at next_alarm.time + 30 seconds
59           (triggers can be skipped if we don't need to support non-zavai alarms)
60     }
61 }
62
63 at clock constructor: refresh_alarms()
64 inotifywait -e close /usr/bin/at -> refresh_alarms()  (shows when someone has just used at)
65   (can be skipped if we don't need to support non-zavai alarms)
66 at alarm triggered through zavai: refresh_alarms()
67
68
69 */
70
71
72 public class Alarm : Object
73 {
74     public at.Event ev;
75     public string label;
76
77     // Schedule with at
78     public static void schedule(string timespec, string label) throws Error
79     {
80         string argv[5];
81         argv[0] = "/usr/bin/at";
82         argv[1] = "-q";
83         argv[2] = "z";
84         argv[3] = timespec;
85         argv[4] = null;
86
87         Pid pid;
88         int stdinfd;
89
90         if (!Process.spawn_async_with_pipes("/", argv, null, SpawnFlags.STDERR_TO_DEV_NULL, null, out pid, out stdinfd, null, null))
91             return;
92
93         {
94             FileStream fs = FileStream.fdopen(stdinfd, "w");
95             string display = GLib.Environment.get_variable("DISPLAY");
96             if (display != null)
97                 fs.printf("DISPLAY=\"%s\"; export DISPLAY\n", display);
98             fs.printf("# Zavai variables start here\n");
99             fs.printf("ZAVAI_LABEL=\"%s\"\n", label.escape(""));
100             fs.printf("# Zavai commands starts here\n");
101             fs.printf("%s notify \"$ZAVAI_LABEL\"", zavai.config.argv0);
102         }
103         
104         Process.close_pid(pid);
105     }
106
107     // Get the label of the job with the given at ID
108     public static string? getLabel(int atID)
109     {
110         string label = null;
111         at.jobContents(atID, fd => {
112             FileStream fs = FileStream.fdopen(fd, "r");
113             while (true)
114             {
115                 string? line = fs.read_line();
116                 if (line == null) break;
117                 if (line.has_prefix("ZAVAI_LABEL=\""))
118                 {
119                     size_t size = line.size();
120                     if (size < 15) continue;
121                     label = line.substring(13, (long)(size - 14));
122                     label = label.compress();
123                     break;
124                 }
125             }
126             return true;
127         });
128         return label;
129     }
130 }
131
132 [DBus (name = "org.enricozini.zavai.Alarm")]
133 public class ZavaiClock : Object {
134     public void Notify (string label) {
135         clock.notify_alarm(label);
136     }
137 }
138
139 public class AlarmTriggerInfo
140 {
141     public zavai.log.Log log;
142     public string label;
143     public bool acked;
144     public bool canceled;
145
146     public AlarmTriggerInfo(string label)
147     {
148         this.label = label;
149         acked = false;
150         canceled = false;
151     }
152 }
153
154 public class AlarmTriggerQueue : zavai.Service
155 {
156     protected List<AlarmTriggerInfo> queue;
157
158     public signal void triggered(AlarmTriggerInfo info);
159     public signal void acked(AlarmTriggerInfo info);
160     public signal void canceled(AlarmTriggerInfo info);
161
162     public AlarmTriggerQueue()
163     {
164         queue = new List<AlarmTriggerInfo>();
165     }
166
167     public void enqueue_trigger(AlarmTriggerInfo info)
168     {
169         // Reuse IDs from the associated logger object
170         info.log = zavai.log.log.start("alarm", "Alarm " + info.label);
171         queue.append(info);
172         if (queue.data == info)
173             triggered(queue.data);
174     }
175
176     protected void done_with_first()
177     {
178         var first = queue.data;
179         queue.remove_link(queue);
180         if (queue != null)
181             triggered(queue.data);
182     }
183
184     public void ack(AlarmTriggerInfo info)
185     {
186         if (queue == null || info != queue.data) return;
187         if (!info.acked && !info.canceled)
188         {
189             info.acked = true;
190             acked(info);
191             info.log.add("alarm acknowledged");
192             zavai.log.log.end(info.log);
193         }
194         done_with_first();
195     }
196
197     public void cancel(AlarmTriggerInfo info)
198     {
199         if (queue == null || info != queue.data) return;
200         if (!info.acked && !info.canceled)
201         {
202             info.canceled = true;
203             canceled(info);
204             info.log.add("alarm canceled");
205             zavai.log.log.end(info.log);
206         }
207         done_with_first();
208     }
209 }
210
211 public class Clock: zavai.Service
212 {
213     protected time_t last_gps_time;
214     protected time_t last_gps_time_system_time;
215     protected time_t last_system_time;
216     protected uint system_time_timeout;
217     protected time_t last_minute;
218     protected time_t chosen_time;
219     protected SourceType chosen_type;
220     protected ZavaiClock dbusClock;
221
222     protected dynamic DBus.Object otimed_alarm;
223     protected dynamic DBus.Object rtc;
224     protected SList<Alarm> alarms;
225
226     // Ticks once a minute
227     public signal void minute_changed(long time, SourceType source);
228     public signal void schedule_changed(Alarm? next);
229
230     public Clock()
231     {
232         Object(name: "clock");
233         alarms = null;
234         dbusClock = new ZavaiClock();
235         last_minute = 0;
236         last_gps_time = 0;
237         last_gps_time_system_time = 0;
238         last_system_time = time_t();
239         chosen_time = last_system_time;
240
241         // FSO alarm system
242         otimed_alarm = zavai.registry.sbus.get_object(
243                 "org.freesmartphone.otimed",
244                 "/org/freesmartphone/Time/Alarm",
245                 "org.freesmartphone.Time.Alarm");
246
247         rtc = zavai.registry.sbus.get_object(
248                 "org.freesmartphone.odeviced",
249                 "/org/freesmartphone/Device/RTC/0",
250                 "org.freesmartphone.Device.RealtimeClock");
251
252         zavai.registry.sbus.register_object("/org/enricozini/Zavai/Clock", dbusClock);
253     }
254
255     public void notify_alarm(string label)
256     {
257         stderr.printf("Notifying %s\n", label);
258         AlarmTriggerInfo info = new AlarmTriggerInfo(label);
259         alarm_trigger_queue.enqueue_trigger(info);
260         schedule_changed(next_alarm());
261     }
262
263     public Alarm? next_alarm()
264     {
265         at.Event ev;
266         ev = at.earliestID("z");
267         if (ev.deadline == 0)
268             return null;
269         string label = Alarm.getLabel(ev.id);
270         Alarm res = new Alarm();
271         res.ev = ev;
272         res.label = label;
273         return res;
274     }
275
276     public void schedule(string timespec, string label) throws Error
277     {
278         Alarm.schedule(timespec, label);
279         schedule_changed(next_alarm());
280     }
281
282     private void on_gps_time(uint t)
283     {
284         if (t == 0)
285         {
286             last_gps_time_system_time = 0;
287             update_time();
288         } else {
289             last_gps_time = (time_t)t;
290             last_gps_time_system_time = time_t();
291             update_time();
292         }
293     }
294
295     private bool on_system_time()
296     {
297         last_system_time = time_t();
298         update_time();
299         return true;
300     }
301
302     private void update_time()
303     {
304         if (last_gps_time_system_time + 10 > last_system_time)
305         {
306             chosen_time = last_gps_time;
307             chosen_type = SourceType.GPS;
308         }
309         else
310         {
311             chosen_time = last_system_time;
312             chosen_type = SourceType.SYSTEM;
313         }
314         if (chosen_time / 60 != last_minute)
315         {
316             last_minute = chosen_time / 60;
317             minute_changed(chosen_time, chosen_type);
318         }
319     }
320
321     /// Request GPS resource
322     public override void start()
323     {
324         if (started) return;
325
326         system_time_timeout = Timeout.add(5000, on_system_time);
327         zavai.gps.gps.time_changed += on_gps_time;
328         last_system_time = time_t();
329         update_time();
330
331         base.start();
332     }
333
334     public override void stop()
335     {
336         if (!started) return;
337
338         Source.remove(system_time_timeout);
339         zavai.gps.gps.time_changed -= on_gps_time;
340
341         base.stop();
342     }
343 }
344
345 public Clock clock = null;
346 public AlarmTriggerQueue alarm_trigger_queue = null;
347
348 public void init()
349 {
350     clock = new Clock();
351     alarm_trigger_queue = new AlarmTriggerQueue();
352 }
353
354 }
355 }