2 * Copyright (C) 2008 Nokia Corporation.
3 * Copyright (C) 2008 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>.
4 * Copyright (C) 2010 Collabora Ltd.
6 * This library is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 2.1 of the License, or
9 * (at your option) any later version.
11 * This library 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 Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this library. If not, see <http://www.gnu.org/licenses/>.
19 * Authors: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
20 * Travis Reitter <travis.reitter@collabora.co.uk>
22 * This file was originally part of Rygel.
28 extern const string G_LOG_DOMAIN;
31 * Responsible for backend loading.
33 * The BackendStore manages the set of available Folks backends. The
34 * {@link BackendStore.load_backends} function loads all compatible and enabled
35 * backends and the {@link BackendStore.backend_available} signal notifies when
36 * these backends are ready.
38 public class Folks.BackendStore : Object {
39 [CCode (has_target = false)]
40 private delegate void ModuleInitFunc (BackendStore store);
41 [CCode (has_target = false)]
42 private delegate void ModuleFinalizeFunc (BackendStore store);
44 /* this contains all backends, regardless of enabled or prepared state */
45 private HashMap<string,Backend> _backend_hash;
46 private HashMap<string, Backend> _prepared_backends;
47 private Map<string, Backend> _prepared_backends_ro;
48 private File _config_file;
49 private GLib.KeyFile _backends_key_file;
50 private HashMap<string,unowned Module> _modules;
51 private static weak BackendStore? _instance = null;
52 private bool _is_prepared = false;
56 * This keyword in the keyfile acts as a wildcard for all backends not already
57 * listed in the same file.
59 * This is particularly useful for tests which want to ensure they're only
60 * operating with backends they were designed for (and thus may not be able to
61 * enumerate entries for).
63 * To avoid conflicts, backends must not use this as a name.
67 public static string KEY_FILE_GROUP_ALL_OTHERS = "all-others";
70 * Emitted when a backend has been added to the BackendStore.
72 * This will not be emitted until after {@link BackendStore.load_backends}
75 * {@link Backend}s referenced in this signal are also included in
76 * {@link BackendStore.enabled_backends}.
78 * @param backend the new {@link Backend}
80 public signal void backend_available (Backend backend);
83 * The list of backends visible to this store which have not been explicitly
86 * This list will be empty before {@link BackendStore.load_backends} has been
89 * The backends in this list have been prepared and are ready to use.
93 public Map<string, Backend> enabled_backends
95 /* Return a read-only view of the map */
96 get { return this._prepared_backends_ro; }
102 * Whether {@link BackendStore.prepare} has successfully completed for this
107 public bool is_prepared
109 get { return this._is_prepared; }
115 * Create a new BackendStore.
117 public static BackendStore dup ()
119 if (BackendStore._instance == null)
121 /* use an intermediate variable to force a strong reference */
122 var new_instance = new BackendStore ();
123 BackendStore._instance = new_instance;
128 return (!) BackendStore._instance;
131 private BackendStore ()
138 /* Treat this as a library init function */
139 var debug_no_colour = Environment.get_variable ("FOLKS_DEBUG_NO_COLOUR");
141 Debug.dup_with_flags (Environment.get_variable ("FOLKS_DEBUG"),
142 (debug_no_colour == null || debug_no_colour == "0"));
144 /* register the core debug messages */
145 this._debug._register_domain (G_LOG_DOMAIN);
147 this._debug.print_status.connect (this._debug_print_status);
149 this._modules = new HashMap<string,unowned Module> (str_hash, str_equal);
150 this._backend_hash = new HashMap<string,Backend> (str_hash, str_equal);
151 this._prepared_backends = new HashMap<string,Backend> (str_hash,
153 this._prepared_backends_ro = this._prepared_backends.read_only_view;
158 /* Finalize all the loaded modules that have finalize functions */
159 foreach (var module in this._modules.values)
162 if (module.symbol ("module_finalize", out func))
164 ModuleFinalizeFunc module_finalize = (ModuleFinalizeFunc) func;
165 module_finalize (this);
169 /* Disconnect from the debug handler */
170 this._debug.print_status.disconnect (this._debug_print_status);
172 /* manually clear the singleton instance */
173 BackendStore._instance = null;
176 private void _debug_print_status (Debug debug)
178 const string domain = Debug.STATUS_LOG_DOMAIN;
179 const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
181 debug.print_heading (domain, level, "BackendStore (%p)", this);
182 debug.print_line (domain, level, "%u Backends:",
183 this._backend_hash.size);
187 foreach (var backend in this._backend_hash.values)
189 debug.print_heading (domain, level, "Backend (%p)", backend);
190 debug.print_key_value_pairs (domain, level,
191 "Ref. count", this.ref_count.to_string (),
192 "Name", backend.name,
193 "Prepared?", backend.is_prepared ? "yes" : "no",
194 "Quiescent?", backend.is_quiescent ? "yes" : "no"
196 debug.print_line (domain, level, "%u PersonaStores:",
197 backend.persona_stores.size);
201 foreach (var persona_store in backend.persona_stores.values)
203 string? trust_level = null;
205 switch (persona_store.trust_level)
207 case PersonaStoreTrust.NONE:
208 trust_level = "none";
210 case PersonaStoreTrust.PARTIAL:
211 trust_level = "partial";
213 case PersonaStoreTrust.FULL:
214 trust_level = "full";
217 assert_not_reached ();
220 var writeable_props = string.joinv (",",
221 persona_store.always_writeable_properties);
223 debug.print_heading (domain, level, "PersonaStore (%p)",
225 debug.print_key_value_pairs (domain, level,
226 "Ref. count", this.ref_count.to_string (),
227 "ID", persona_store.id,
228 "Prepared?", persona_store.is_prepared ? "yes" : "no",
230 persona_store.is_primary_store ? "yes" : "no",
231 "Always writeable properties", writeable_props,
232 "Quiescent?", persona_store.is_quiescent ? "yes" : "no",
233 "Trust level", trust_level,
234 "Persona count", persona_store.personas.size.to_string ()
243 debug.print_line (domain, level, "");
247 * Prepare the BackendStore for use.
249 * This must only ever be called before {@link BackendStore.load_backends} is
250 * called for the first time. If it isn't called explicitly,
251 * {@link BackendStore.load_backends} will call it.
255 public async void prepare ()
257 /* (re-)load the list of disabled backends */
258 yield this._load_disabled_backend_names ();
260 if (this._is_prepared == true)
262 this._is_prepared = true;
264 this.notify_property ("is-prepared");
268 * Find, load, and prepare all backends which are not disabled.
270 * Backends will be searched for in the path given by the `FOLKS_BACKEND_PATH`
271 * environment variable, if it's set. If it's not set, backends will be
272 * searched for in a path set at compilation time.
274 public async void load_backends () throws GLib.Error
276 assert (Module.supported());
278 yield this.prepare ();
280 /* unload backends that have been disabled since they were loaded */
281 foreach (var backend_existing in this._backend_hash.values)
283 yield this._backend_unload_if_needed (backend_existing);
286 string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
291 path = BuildConf.BACKEND_DIR;
293 debug ("Using built-in backend dir '%s' (override with " +
294 "environment variable FOLKS_BACKEND_PATH)", path);
300 debug ("Using environment variable FOLKS_BACKEND_PATH = " +
301 "'%s' to look for backends", path);
304 var modules = new HashMap<string, File> ();
305 var path_split = path.split (":");
306 foreach (unowned string subpath in path_split)
308 var file = File.new_for_path (subpath);
312 yield this._get_file_info (file, out is_file, out is_dir);
315 modules.set (subpath, file);
319 var cur_modules = yield this._get_modules_from_dir (file);
320 if (cur_modules != null)
322 foreach (var entry in ((!) cur_modules).entries)
324 modules.set (entry.key, entry.value);
330 critical ("FOLKS_BACKEND_PATH component '%s' is not a regular " +
331 "file or directory; ignoring...",
333 assert_not_reached ();
337 /* this will load any new modules found in the backends dir and will
338 * prepare and unprepare backends such that they match the state in the
339 * backend store key file */
340 foreach (var module in modules.values)
341 this._load_module_from_file (module);
343 /* this is populated indirectly from _load_module_from_file(), above */
344 foreach (var backend in this._backend_hash.values)
345 yield this._backend_load_if_needed (backend);
348 private async void _backend_load_if_needed (Backend backend)
350 if (this._backend_is_enabled (backend.name))
352 if (!this._prepared_backends.has_key (backend.name))
356 yield backend.prepare ();
358 debug ("New backend '%s' prepared", backend.name);
359 this._prepared_backends.set (backend.name, backend);
360 this.backend_available (backend);
364 /* Translators: the first parameter is a backend name, and the
365 * second is an error message. */
366 warning (_("Error preparing Backend '%s': %s"),
367 backend.name, e.message);
373 private async bool _backend_unload_if_needed (Backend backend)
375 var unloaded = false;
377 if (!this._backend_is_enabled (backend.name))
379 Backend? backend_existing = this._backend_hash.get (backend.name);
380 if (backend_existing != null)
384 yield ((!) backend_existing).unprepare ();
388 warning ("Error unpreparing Backend '%s': %s", backend.name,
392 this._prepared_backends.unset (((!) backend_existing).name);
402 * Add a new {@link Backend} to the BackendStore.
404 * @param backend the {@link Backend} to add
406 public void add_backend (Backend backend)
408 /* Purge any other backend with the same name; re-add if enabled */
409 Backend? backend_existing = this._backend_hash.get (backend.name);
410 if (backend_existing != null && backend_existing != backend)
412 ((!) backend_existing).unprepare ();
413 this._prepared_backends.unset (((!) backend_existing).name);
416 this._debug._register_domain (backend.name);
418 this._backend_hash.set (backend.name, backend);
421 private bool _backend_is_enabled (string name)
423 var all_others_enabled = true;
426 all_others_enabled = this._backends_key_file.get_boolean (
427 this.KEY_FILE_GROUP_ALL_OTHERS, "enabled");
429 catch (KeyFileError e)
431 if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
432 !(e is KeyFileError.KEY_NOT_FOUND))
434 warning ("Couldn't determine whether to enable or disable " +
435 "backends not listed in backend key file. Defaulting to %s.",
436 all_others_enabled ? "enabled" : "disabled");
440 debug ("No catch-all entry in the backend key file. %s " +
441 "unlisted backends.",
442 all_others_enabled ? "Enabling" : "Disabling");
445 /* fall back to the default in case of any level of failure */
451 enabled = this._backends_key_file.get_boolean (name, "enabled");
453 catch (KeyFileError e)
455 /* if there's no entry for this backend, use the default set above */
456 if ((e is KeyFileError.GROUP_NOT_FOUND) ||
457 (e is KeyFileError.KEY_NOT_FOUND))
459 debug ("Found no entry for backend '%s'.enabled in backend " +
460 "keyfile. %s according to '%s' setting.",
462 all_others_enabled ? "Enabling" : "Disabling",
463 this.KEY_FILE_GROUP_ALL_OTHERS);
464 enabled = all_others_enabled;
466 else if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
467 !(e is KeyFileError.KEY_NOT_FOUND))
469 warning ("Couldn't check enabled state of backend '%s': %s\n" +
470 "Disabling backend.",
480 * Get a backend from the store by name. If a backend is returned, its
481 * reference count is increased.
483 * @param name the backend name to retrieve
484 * @return the backend, or `null` if none could be found
488 public Backend? dup_backend_by_name (string name)
490 return this._backend_hash.get (name);
494 * List the currently loaded backends.
496 * @return a list of the backends currently in the BackendStore
498 public Collection<Backend> list_backends ()
500 return this._backend_hash.values.read_only_view;
506 * Mark a backend as enabled, such that the BackendStore will always attempt
507 * to load it when {@link BackendStore.load_backends} is called. This will
508 * not load the backend if it's not currently loaded.
510 * @param name the name of the backend to enable
513 public async void enable_backend (string name)
515 this._backends_key_file.set_boolean (name, "enabled", true);
516 yield this._save_key_file ();
522 * Mark a backend as disabled, such that it won't be loaded even when the
523 * client application is restarted. This will not remove the backend if it's
526 * @param name the name of the backend to disable
529 public async void disable_backend (string name)
531 this._backends_key_file.set_boolean (name, "enabled", false);
532 yield this._save_key_file ();
535 private async HashMap<string, File>? _get_modules_from_dir (File dir)
537 debug ("Searching for modules in folder '%s' ..", dir.get_path ());
540 FileAttribute.STANDARD_NAME + "," +
541 FileAttribute.STANDARD_TYPE + "," +
542 FileAttribute.STANDARD_IS_SYMLINK + "," +
543 FileAttribute.STANDARD_CONTENT_TYPE;
545 GLib.List<FileInfo> infos;
548 FileEnumerator enumerator =
549 yield dir.enumerate_children_async (attributes,
550 FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
552 infos = yield enumerator.next_files_async (int.MAX,
553 Priority.DEFAULT, null);
557 /* Translators: the first parameter is a folder path and the second
558 * is an error message. */
559 critical (_("Error listing contents of folder '%s': %s"),
560 dir.get_path (), error.message);
565 var modules_final = new HashMap<string, File> (str_hash, str_equal);
567 foreach (var info in infos)
569 var file = dir.get_child (info.get_name ());
570 var file_type = info.get_file_type ();
571 unowned string content_type = info.get_content_type ();
572 /* don't load the library multiple times for its various symlink
574 var is_symlink = info.get_is_symlink ();
576 string? mime = ContentType.get_mime_type (content_type);
578 if (file_type == FileType.DIRECTORY)
580 var modules = yield this._get_modules_from_dir (file);
583 foreach (var entry in ((!) modules).entries)
585 modules_final.set (entry.key, entry.value);
589 else if (mime == "application/x-sharedlib" && !is_symlink)
591 var path = file.get_path ();
594 modules_final.set ((!) path, file);
597 else if (mime == null)
600 /* Translators: the parameter is a filename. */
601 _("The content type of '%s' could not be determined. Have you installed shared-mime-info?"),
606 debug ("Finished searching for modules in folder '%s'",
609 return modules_final;
612 private void _load_module_from_file (File file)
614 var _file_path = file.get_path ();
615 if (_file_path == null)
619 var file_path = (!) _file_path;
621 if (this._modules.has_key (file_path))
624 var _module = Module.open (file_path, ModuleFlags.BIND_LOCAL);
627 /* Translators: the first parameter is a filename and the second is an
629 warning (_("Failed to load module from path '%s': %s"),
630 file_path, Module.error ());
634 unowned Module module = (!) _module;
638 /* this causes the module to call add_backend() for its backends (adding
639 * them to the backend hash); any backends that already existed will be
640 * removed if they've since been disabled */
641 if (!module.symbol("module_init", out function))
643 /* Translators: the first parameter is a function name, the second is
644 * a filename and the third is an error message. */
645 warning (_("Failed to find entry point function '%s' in '%s': %s"),
653 ModuleInitFunc module_init = (ModuleInitFunc) function;
654 assert (module_init != null);
656 this._modules.set (file_path, module);
658 /* We don't want our modules to ever unload */
659 module.make_resident ();
663 debug ("Loaded module source: '%s'", module.name ());
666 private async static void _get_file_info (File file,
676 /* Query for the MIME type; if the file doesn't exist, we'll get an
677 * appropriate error back, so this also checks for existence. */
678 file_info = yield file.query_info_async (FileAttribute.STANDARD_TYPE,
679 FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
683 if (error is IOError.NOT_FOUND)
685 /* Translators: the parameter is a filename. */
686 critical (_("File or directory '%s' does not exist."),
691 /* Translators: the parameter is a filename. */
692 critical (_("Failed to get content type for '%s'."),
699 is_file = (file_info.get_file_type () == FileType.REGULAR);
700 is_dir = (file_info.get_file_type () == FileType.DIRECTORY);
703 private async void _load_disabled_backend_names ()
706 unowned string? path = Environment.get_variable (
707 "FOLKS_BACKEND_STORE_KEY_FILE_PATH");
710 file = File.new_for_path (Environment.get_user_data_dir ());
711 file = file.get_child ("folks");
712 file = file.get_child ("backends.ini");
714 debug ("Using built-in backends key file '%s' (override with " +
715 "environment variable FOLKS_BACKEND_STORE_KEY_FILE_PATH)",
720 file = File.new_for_path ((!) path);
721 debug ("Using environment variable " +
722 "FOLKS_BACKEND_STORE_KEY_FILE_PATH = '%s' to load the backends " +
723 "key file.", (!) path);
726 this._config_file = file;
728 /* Load the disabled backends file */
729 this._backends_key_file = new GLib.KeyFile ();
732 uint8 *contents = null;
734 yield file.load_contents_async (null, out contents, null);
735 var contents_s = (string) contents;
737 if (contents_s.length > 0)
739 this._backends_key_file.load_from_data (contents_s,
740 contents_s.length, KeyFileFlags.KEEP_COMMENTS);
745 if (!(e1 is IOError.NOT_FOUND))
747 warning ("The backends key file '%s' could not be loaded: %s",
748 file.get_path (), e1.message);
754 private async void _save_key_file ()
756 var key_file_data = this._backends_key_file.to_data ();
758 debug ("Saving backend key file '%s'.", this._config_file.get_path ());
762 /* Note: We have to use key_file_data.size () here to get its length
763 * in _bytes_ rather than _characters_. bgo#628930.
764 * In Vala >= 0.11, string.size() has been deprecated in favour of
765 * string.length (which now returns the byte length, whereas in
766 * Vala <= 0.10, it returned the character length). FIXME: We need to
767 * take this into account until we depend explicitly on
769 yield this._config_file.replace_contents_async (key_file_data.data,
770 null, false, FileCreateFlags.PRIVATE,
775 warning ("Could not write updated backend key file '%s': %s",
776 this._config_file.get_path (), e.message);