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