1/* zeitgeist-daemon.vala
2 *
3 * Copyright © 2011 Seif Lotfy <seif@lotfy.com>
4 * Copyright © 2011 Michal Hruby <michal.mhr@gmail.com>
5 * Copyright © 2011 Collabora Ltd.
6 *             By Siegfried-Angel Gevatter Pujals <siegfried@gevatter.com>
7 *             By Seif Lotfy <seif@lotfy.com>
8 * Copyright © 2012 Canonical Ltd.
9 *             By Siegfried-A. Gevatter <siegfried.gevatter@collabora.co.uk>
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 2.1 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU Lesser General Public License
22 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 *
24 */
25
26namespace Zeitgeist
27{
28
29    [DBus (name = "org.freedesktop.DBus")]
30    public interface RemoteDBus : Object
31    {
32        public abstract bool name_has_owner (string name) throws GLib.Error;
33    }
34
35    public class Daemon : Object, RemoteLog
36    {
37        private static bool show_version_info = false;
38        private static bool show_options = false;
39        private static bool no_datahub = false;
40        private static bool perform_vacuum = false;
41        private static bool replace_mode = false;
42        private static bool quit_daemon = false;
43        private static string log_level = "";
44        private static string? log_file = null;
45
46        // load the builtin extensions first
47        RegisterExtensionFunc[] builtins = {};
48
49        const OptionEntry[] options =
50        {
51            {
52                "version", 'v', 0, OptionArg.NONE, out show_version_info,
53                "Print program's version number and exit", null
54            },
55            {
56                "no-datahub", 0, 0, OptionArg.NONE, out no_datahub,
57                "Do not start zeitgeist-datahub automatically", null
58            },
59            {
60                "vacuum", 0, 0, OptionArg.NONE, out perform_vacuum,
61                "Perform VACUUM on database and exit", null
62            },
63            {
64                "no-passive-loggers", 0, OptionFlags.HIDDEN, OptionArg.NONE,
65                out no_datahub, null, null
66            },
67            {
68                "replace", 'r', 0, OptionArg.NONE, out replace_mode,
69                "If another Zeitgeist instance is already running, replace it",
70                null
71            },
72            {
73                "quit", 'q', 0, OptionArg.NONE, out quit_daemon,
74                "Quit running Zeitgeist daemon instance", null
75            },
76            {
77                "log-level", 0, 0, OptionArg.STRING, out log_level,
78                "How much information should be printed; possible values: " +
79                "DEBUG, INFO, WARNING, ERROR, CRITICAL", "LEVEL"
80            },
81            {
82                "log-file", 0, 0, OptionArg.STRING, out log_file,
83                "File to which the log output will be appended", null
84            },
85            {
86                "shell-completion", 0, OptionFlags.HIDDEN, OptionArg.NONE,
87                out show_options, null, null
88            },
89            {
90                null
91            }
92        };
93
94        private static Daemon? instance;
95        private static MainLoop mainloop;
96        private static bool name_acquired = false;
97
98        private Engine engine;
99        private MonitorManager notifications;
100
101        private uint log_register_id;
102        private unowned DBusConnection connection;
103
104        public string[] extensions
105        {
106            owned get
107            {
108                string[] ext = engine.get_extension_names ();
109                return ext;
110            }
111        }
112
113        public VersionStruct version
114        {
115            owned get
116            {
117                var s = VersionStruct ();
118                string[] ver = Config.VERSION.split (".");
119                if (ver.length >= 1)
120                {
121                    s.major = int.parse (ver[0]);
122                    s.minor = (ver.length >= 2) ? int.parse (ver[1]) : 0;
123                    s.micro = (ver.length >= 3) ? int.parse (ver[2]) : 0;
124                } else {
125                    warning ("Unable to parse version info `%s`!", Config.VERSION);
126                    s.major = 1;
127                    s.minor = 0;
128                    s.micro = 0;
129                }
130
131                return s;
132            }
133        }
134
135        public string datapath
136        {
137            owned get
138            {
139                return Utils.get_database_file_path ();
140            }
141        }
142
143        public Daemon () throws EngineError
144        {
145#if BUILTIN_EXTENSIONS
146            builtins = {
147                data_source_registry_extension_init,
148                blacklist_init,
149                histogram_init,
150                storage_monitor_init,
151                fts_init,
152                benchmark_init
153            };
154#endif
155            engine = new Engine.with_builtins (builtins);
156            notifications = MonitorManager.get_default ();
157        }
158
159        public async Variant get_events (uint32[] event_ids, Cancellable? cancellable,
160            BusName? sender=null) throws Error
161        {
162            var timer = new Timer ();
163            GenericArray<Event> events = engine.get_events (event_ids);
164            debug ("%s executed in %f seconds: got %i events",
165                GLib.Log.METHOD, timer.elapsed (), events.length);
166            return Events.to_variant_with_limit (events);
167        }
168
169        public async string[] find_related_uris (Variant time_range,
170                Variant event_templates,
171                Variant result_event_templates,
172                uint storage_state, uint num_events, uint result_type,
173                Cancellable? cancellable, BusName? sender=null) throws Error
174        {
175            return engine.find_related_uris (
176                new TimeRange.from_variant (time_range),
177                Events.from_variant (event_templates),
178                Events.from_variant (result_event_templates),
179                storage_state, num_events, result_type);
180        }
181
182        public async uint32[] find_event_ids (Variant time_range,
183                Variant event_templates,
184                uint storage_state, uint num_events, uint result_type,
185                Cancellable? cancellable=null,
186                BusName? sender=null) throws Error
187        {
188            var timer = new Timer ();
189            var ids = engine.find_event_ids (
190                new TimeRange.from_variant (time_range),
191                Events.from_variant(event_templates),
192                storage_state, num_events, result_type, sender);
193            debug ("%s executed in %f seconds: found %i event ids",
194                GLib.Log.METHOD, timer.elapsed (), ids.length);
195            return ids;
196        }
197
198        public async Variant find_events (Variant time_range,
199                Variant event_templates,
200                uint storage_state, uint num_events, uint result_type,
201                Cancellable? cancellable=null,
202                BusName? sender=null) throws Error
203        {
204            var timer = new Timer ();
205            var events = engine.find_events (
206                new TimeRange.from_variant (time_range),
207                Events.from_variant (event_templates),
208                storage_state, num_events, result_type, sender);
209            debug ("%s executed in %f seconds: found %i events",
210                GLib.Log.METHOD, timer.elapsed (), events.length);
211            return Events.to_variant_with_limit (events);
212        }
213
214        public async uint32[] insert_events (
215                Variant vevents,
216                Cancellable? cancellable=null,
217                BusName? sender=null) throws Error
218        {
219            var events = Events.from_variant (vevents);
220            uint32[] event_ids = engine.insert_events (events, sender);
221            var min_timestamp = int64.MAX;
222            var max_timestamp = int64.MIN;
223            for (int i = 0; i < events.length; i++)
224            {
225                if (events[i] == null) continue;
226                min_timestamp = int64.min (min_timestamp, events[i].timestamp);
227                max_timestamp = int64.max (max_timestamp, events[i].timestamp);
228            }
229
230            if (min_timestamp < int64.MAX)
231            {
232                notifications.notify_insert (
233                    new TimeRange (min_timestamp, max_timestamp), events);
234            }
235            /* else { there's not even one valid event } */
236
237            return event_ids;
238        }
239
240        public async Variant delete_events (uint32[] event_ids,
241            Cancellable? cancellable=null, BusName? sender=null) throws Error
242        {
243            TimeRange? time_range = engine.delete_events (event_ids, sender);
244            if (time_range != null)
245            {
246                notifications.notify_delete (time_range, event_ids);
247            }
248            else
249            {
250                // All the given event_ids are invalod or the events
251                // have already been deleted before!
252                time_range = new TimeRange (-1, -1);
253            }
254            return time_range.to_variant ();
255        }
256
257        public async void quit (Cancellable? cancellable=null) throws Error
258        {
259            do_quit ();
260        }
261
262        private void do_quit ()
263        {
264            engine.close ();
265            mainloop.quit ();
266        }
267
268        public async void install_monitor (ObjectPath monitor_path,
269                Variant time_range,
270                Variant event_templates,
271                BusName? owner=null) throws Error
272        {
273            assert (owner != null);
274            notifications.install_monitor (owner, monitor_path,
275                new TimeRange.from_variant (time_range),
276                Events.from_variant (event_templates));
277        }
278
279        public async void remove_monitor (ObjectPath monitor_path, BusName? owner=null)
280            throws Error
281        {
282            assert (owner != null);
283            notifications.remove_monitor (owner, monitor_path);
284        }
285
286        public void register_dbus_object (DBusConnection conn) throws IOError
287        {
288            connection = conn;
289            log_register_id = conn.register_object<RemoteLog> (
290                Utils.ENGINE_DBUS_PATH, this);
291        }
292
293        public void unregister_dbus_object ()
294        {
295            if (log_register_id != 0)
296            {
297                connection.unregister_object (log_register_id);
298                log_register_id = 0;
299            }
300        }
301
302        private static bool quit_running_instance (DBusConnection conn)
303        {
304            try
305            {
306                var running_instance = conn.get_proxy_sync<RemoteLog> (
307                    Utils.ENGINE_DBUS_NAME, Utils.ENGINE_DBUS_PATH);
308
309                running_instance.quit.begin ();
310                return true;
311            }
312            catch (Error err)
313            {
314                warning ("%s", err.message);
315            }
316
317            return false;
318        }
319
320        private static void name_acquired_callback (DBusConnection conn)
321        {
322            name_acquired = true;
323
324            // only run datahub when we acquire bus name
325            if (!no_datahub)
326            {
327                try
328                {
329                    Process.spawn_command_line_async ("zeitgeist-datahub");
330                }
331                catch (SpawnError err)
332                {
333                    warning ("%s", err.message);
334                }
335            }
336        }
337
338        private static void name_lost_callback (DBusConnection? conn)
339        {
340            if (conn == null)
341            {
342                // something happened to our bus connection
343                mainloop.quit ();
344            }
345            else if (instance != null && !name_acquired)
346            {
347                // we acquired bus connection, but couldn't own the name
348                if (!replace_mode)
349                {
350                }
351
352                debug ("Waiting 10 seconds to acquire name...");
353                // we already called Quit, let's wait a while
354                // for the running instance to quit, bail out
355                // if it doesn't
356                Timeout.add (10000, () =>
357                {
358                    if (!name_acquired)
359                    {
360                        warning ("Timeout reached, unable to acquire name!");
361                        mainloop.quit ();
362                    }
363                    return false;
364                });
365            }
366            else if (instance != null && name_acquired)
367            {
368                // we owned the name and we lost it... what to do?
369                mainloop.quit ();
370            }
371        }
372
373        static void run ()
374            throws Error
375        {
376            DBusConnection connection;
377            bool name_owned;
378            try
379            {
380                connection = Bus.get_sync (BusType.SESSION);
381                var proxy = connection.get_proxy_sync<RemoteDBus> (
382                    "org.freedesktop.DBus", "/org/freedesktop/DBus",
383                    DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
384                name_owned = proxy.name_has_owner (Utils.ENGINE_DBUS_NAME);
385            }
386            catch (IOError err)
387            {
388                throw err;
389            }
390            if (name_owned)
391            {
392                if (replace_mode || quit_daemon)
393                {
394                    quit_running_instance (connection);
395                }
396                else
397                {
398                    warning ("An existing instance was found. Please use " +
399                        "--replace to stop it and start a new instance.");
400                    throw new EngineError.EXISTING_INSTANCE (
401                        "Zeitgeist is running already.");
402                }
403            }
404
405            /* don't do anything else if we were called with --quit param */
406            if (quit_daemon) return;
407
408            /* setup Engine instance and register objects on dbus */
409            try
410            {
411                instance = new Daemon ();
412                instance.register_dbus_object (connection);
413            }
414            catch (Error err)
415            {
416                if (err is EngineError.DATABASE_CANTOPEN)
417                {
418                    warning ("Could not access the database file.\n" +
419                        "Please check the permissions of file %s.",
420                        Utils.get_database_file_path ());
421                }
422                else if (err is EngineError.DATABASE_BUSY)
423                {
424                    warning ("It looks like another Zeitgeist instance " +
425                        "is already running (the database is locked). " +
426                        "If you want to start a new instance, use --replace.");
427                }
428                throw err;
429            }
430
431            uint owner_id = Bus.own_name_on_connection (connection,
432                Utils.ENGINE_DBUS_NAME,
433                BusNameOwnerFlags.NONE,
434                name_acquired_callback,
435                name_lost_callback);
436
437            mainloop = new MainLoop ();
438            mainloop.run ();
439
440            if (instance != null)
441            {
442                Bus.unown_name (owner_id);
443                instance.unregister_dbus_object ();
444                instance = null;
445
446                // make sure we send quit reply
447                try
448                {
449                    connection.flush_sync ();
450                }
451                catch (Error e)
452                {
453                    warning ("%s", e.message);
454                }
455            }
456        }
457
458        static void safe_exit ()
459        {
460            instance.do_quit ();
461        }
462
463        static int vacuum ()
464        {
465            Sqlite.Database database;
466
467            if (Utils.using_in_memory_database ())
468                warning ("Using in-memory database, no VACUUM needed");
469
470            unowned string db_path = Utils.get_database_file_path ();
471            debug ("Opening database file at %s", db_path);
472
473            int rc = Sqlite.Database.open_v2 (db_path, out database, Sqlite.OPEN_READWRITE);
474            if (rc != Sqlite.OK)
475            {
476                warning ("Failed to open database \"%s\" (%s)", db_path, database.errmsg ());
477                return rc;
478            }
479
480            stdout.printf ("Performing VACUUM operation... ");
481            stdout.flush ();
482            rc = database.exec ("VACUUM");
483            if (rc != Sqlite.OK)
484            {
485                stdout.printf ("FAIL\n");
486                warning (database.errmsg ());
487                return rc;
488            }
489
490            stdout.printf ("OK\n");
491
492            return 0;
493        }
494
495        static int main (string[] args)
496        {
497            Posix.signal (Posix.SIGHUP, safe_exit);
498            Posix.signal (Posix.SIGINT, safe_exit);
499            Posix.signal (Posix.SIGTERM, safe_exit);
500
501            Intl.setlocale (LocaleCategory.ALL, "");
502
503            var opt_context = new OptionContext (" - Zeitgeist daemon");
504            opt_context.add_main_entries (options, null);
505
506            try
507            {
508                opt_context.parse (ref args);
509
510                if (show_version_info)
511                {
512                    stdout.printf (Config.VERSION + "\n");
513                    return 0;
514                }
515                if (show_options)
516                {
517                    foreach (unowned OptionEntry? entry in options)
518                    {
519                        if (entry.long_name != null)
520                            stdout.printf ("--%s ", entry.long_name);
521                        if (entry.short_name != 0)
522                            stdout.printf ("-%c ", entry.short_name);
523                    }
524                    stdout.printf ("--help\n");
525
526                    return 0;
527                }
528                if (perform_vacuum)
529                {
530                    return vacuum ();
531                }
532
533                Logging.setup_logging (log_level, log_file);
534
535                run ();
536            }
537            catch (Error err)
538            {
539                if (err is EngineError.EXISTING_INSTANCE)
540                    return 10;
541                if (err is EngineError.DATABASE_CANTOPEN)
542                    return 21;
543                if (err is EngineError.DATABASE_BUSY)
544                    return 22;
545
546                warning ("%s", err.message);
547                return 1;
548            }
549
550            return 0;
551        }
552
553    }
554
555#if BUILTIN_EXTENSIONS
556    private extern static Type data_source_registry_extension_init (TypeModule mod);
557    private extern static Type blacklist_init (TypeModule mod);
558    private extern static Type histogram_init (TypeModule mod);
559    private extern static Type storage_monitor_init (TypeModule mod);
560    private extern static Type fts_init (TypeModule mod);
561    private extern static Type benchmark_init (TypeModule mod);
562#endif
563}
564
565// vim:expandtab:ts=4:sw=4
566