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 /* if null, all backends are allowed */
47 private HashSet<string>? _backends_allowed;
48 /* if null, no backends are disabled */
49 private HashSet<string>? _backends_disabled;
50 private HashMap<string, Backend> _prepared_backends;
51 private Map<string, Backend> _prepared_backends_ro;
52 private File _config_file;
53 private GLib.KeyFile _backends_key_file;
54 private HashMap<string,unowned Module> _modules;
55 private static weak BackendStore? _instance = null;
56 private bool _is_prepared = false;
60 * This keyword in the keyfile acts as a wildcard for all backends not already
61 * listed in the same file.
63 * This is particularly useful for tests which want to ensure they're only
64 * operating with backends they were designed for (and thus may not be able to
65 * enumerate entries for).
67 * To avoid conflicts, backends must not use this as a name.
71 public static string KEY_FILE_GROUP_ALL_OTHERS = "all-others";
74 * Emitted when a backend has been added to the BackendStore.
76 * This will not be emitted until after {@link BackendStore.load_backends}
79 * {@link Backend}s referenced in this signal are also included in
80 * {@link BackendStore.enabled_backends}.
82 * @param backend the new {@link Backend}
84 public signal void backend_available (Backend backend);
87 * The list of backends visible to this store which have not been explicitly
90 * This list will be empty before {@link BackendStore.load_backends} has been
93 * The backends in this list have been prepared and are ready to use.
97 public Map<string, Backend> enabled_backends
99 /* Return a read-only view of the map */
100 get { return this._prepared_backends_ro; }
106 * Whether {@link BackendStore.prepare} has successfully completed for this
111 public bool is_prepared
113 get { return this._is_prepared; }
119 * Create a new BackendStore.
121 public static BackendStore dup ()
123 if (BackendStore._instance == null)
125 /* use an intermediate variable to force a strong reference */
126 var new_instance = new BackendStore ();
127 BackendStore._instance = new_instance;
132 return (!) BackendStore._instance;
135 private BackendStore ()
142 /* Treat this as a library init function */
143 var debug_no_colour = Environment.get_variable ("FOLKS_DEBUG_NO_COLOUR");
145 Debug.dup_with_flags (Environment.get_variable ("G_MESSAGES_DEBUG"),
146 (debug_no_colour == null || debug_no_colour == "0"));
148 /* register the core debug messages */
149 this._debug._register_domain (G_LOG_DOMAIN);
151 this._debug.print_status.connect (this._debug_print_status);
153 this._modules = new HashMap<string,unowned Module> ();
154 this._backend_hash = new HashMap<string,Backend> ();
155 this._prepared_backends = new HashMap<string,Backend> ();
156 this._prepared_backends_ro = this._prepared_backends.read_only_view;
161 /* Finalize all the loaded modules that have finalize functions */
162 foreach (var module in this._modules.values)
165 if (module.symbol ("module_finalize", out func))
167 ModuleFinalizeFunc module_finalize = (ModuleFinalizeFunc) func;
168 module_finalize (this);
172 /* Disconnect from the debug handler */
173 this._debug.print_status.disconnect (this._debug_print_status);
175 /* manually clear the singleton instance */
176 BackendStore._instance = null;
179 private void _debug_print_status (Debug debug)
181 const string domain = Debug.STATUS_LOG_DOMAIN;
182 const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
184 debug.print_heading (domain, level, "BackendStore (%p)", this);
185 debug.print_line (domain, level, "%u Backends:",
186 this._backend_hash.size);
190 foreach (var backend in this._backend_hash.values)
192 debug.print_heading (domain, level, "Backend (%p)", backend);
193 debug.print_key_value_pairs (domain, level,
194 "Ref. count", this.ref_count.to_string (),
195 "Name", backend.name,
196 "Prepared?", backend.is_prepared ? "yes" : "no",
197 "Quiescent?", backend.is_quiescent ? "yes" : "no"
199 debug.print_line (domain, level, "%u PersonaStores:",
200 backend.persona_stores.size);
204 foreach (var persona_store in backend.persona_stores.values)
206 string? trust_level = null;
208 switch (persona_store.trust_level)
210 case PersonaStoreTrust.NONE:
211 trust_level = "none";
213 case PersonaStoreTrust.PARTIAL:
214 trust_level = "partial";
216 case PersonaStoreTrust.FULL:
217 trust_level = "full";
220 assert_not_reached ();
223 var writeable_props = string.joinv (",",
224 persona_store.always_writeable_properties);
226 debug.print_heading (domain, level, "PersonaStore (%p)",
228 debug.print_key_value_pairs (domain, level,
229 "Ref. count", this.ref_count.to_string (),
230 "ID", persona_store.id,
231 "Prepared?", persona_store.is_prepared ? "yes" : "no",
233 persona_store.is_primary_store ? "yes" : "no",
234 "Always writeable properties", writeable_props,
235 "Quiescent?", persona_store.is_quiescent ? "yes" : "no",
236 "Trust level", trust_level,
237 "Persona count", persona_store.personas.size.to_string ()
246 debug.print_line (domain, level, "");
250 * Prepare the BackendStore for use.
252 * This must only ever be called before {@link BackendStore.load_backends} is
253 * called for the first time. If it isn't called explicitly,
254 * {@link BackendStore.load_backends} will call it.
256 * This method is safe to call multiple times concurrently (e.g. an
257 * asynchronous call may begin between a subsequent asynchronous call
258 * beginning and finishing).
262 public async void prepare ()
264 Internal.profiling_start ("preparing BackendStore");
266 /* (re-)load the list of disabled backends */
267 yield this._load_disabled_backend_names ();
269 if (this._is_prepared == false)
271 this._is_prepared = true;
272 this.notify_property ("is-prepared");
275 Internal.profiling_end ("preparing BackendStore");
279 * Find, load, and prepare all backends which are not disabled.
281 * Backends will be searched for in the path given by the
282 * ``FOLKS_BACKEND_PATH`` environment variable, if it's set. If it's not set,
283 * backends will be searched for in a path set at compilation time.
285 * This method is not safe to call multiple times concurrently.
287 * @throws GLib.Error currently unused
289 public async void load_backends () throws GLib.Error
291 assert (Module.supported());
293 Internal.profiling_start ("loading backends in BackendStore");
295 yield this.prepare ();
297 /* unload backends that have been disabled since they were loaded */
298 foreach (var backend_existing in this._backend_hash.values)
300 yield this._backend_unload_if_needed (backend_existing);
303 Internal.profiling_point ("unloaded backends in BackendStore");
305 string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
310 path = BuildConf.BACKEND_DIR;
312 debug ("Using built-in backend dir '%s' (override with " +
313 "environment variable FOLKS_BACKEND_PATH)", path);
319 debug ("Using environment variable FOLKS_BACKEND_PATH = " +
320 "'%s' to look for backends", path);
323 var modules = new HashMap<string, File> ();
324 var path_split = path.split (":");
325 foreach (unowned string subpath in path_split)
327 var file = File.new_for_path (subpath);
331 yield BackendStore._get_file_info (file, out is_file, out is_dir);
334 modules.set (subpath, file);
338 var cur_modules = yield this._get_modules_from_dir (file);
339 if (cur_modules != null)
341 foreach (var entry in ((!) cur_modules).entries)
343 modules.set (entry.key, entry.value);
349 critical ("FOLKS_BACKEND_PATH component '%s' is not a regular " +
350 "file or directory; ignoring...",
352 assert_not_reached ();
356 Internal.profiling_point ("found modules in BackendStore");
358 /* this will load any new modules found in the backends dir and will
359 * prepare and unprepare backends such that they match the state in the
360 * backend store key file */
361 foreach (var module in modules.values)
362 this._load_module_from_file (module);
364 Internal.profiling_point ("loaded modules in BackendStore");
366 /* this is populated indirectly from _load_module_from_file(), above */
367 var backends_remaining = 1;
368 foreach (var backend in this._backend_hash.values)
370 backends_remaining++;
371 this._backend_load_if_needed.begin (backend, (o, r) =>
373 this._backend_load_if_needed.end (r);
374 backends_remaining--;
376 if (backends_remaining == 0)
378 this.load_backends.callback ();
382 backends_remaining--;
383 if (backends_remaining > 0)
388 Internal.profiling_end ("loading backends in BackendStore");
391 /* This method is not safe to call multiple times concurrently, since there's
392 * a race in updating this._prepared_backends. */
393 private async void _backend_load_if_needed (Backend backend)
395 if (this._backend_is_enabled (backend.name))
397 if (!this._prepared_backends.has_key (backend.name))
401 yield backend.prepare ();
403 debug ("New backend '%s' prepared", backend.name);
404 this._prepared_backends.set (backend.name, backend);
405 this.backend_available (backend);
409 warning ("Error preparing Backend '%s': %s",
410 backend.name, e.message);
416 /* This method is not safe to call multiple times concurrently, since there's
417 * a race in updating this._prepared_backends. */
418 private async bool _backend_unload_if_needed (Backend backend)
420 var unloaded = false;
422 if (!this._backend_is_enabled (backend.name))
424 Backend? backend_existing = this._backend_hash.get (backend.name);
425 if (backend_existing != null)
429 yield ((!) backend_existing).unprepare ();
433 warning ("Error unpreparing Backend '%s': %s", backend.name,
437 this._prepared_backends.unset (((!) backend_existing).name);
447 * Add a new {@link Backend} to the BackendStore.
449 * @param backend the {@link Backend} to add
451 public void add_backend (Backend backend)
453 /* Purge any other backend with the same name; re-add if enabled */
454 Backend? backend_existing = this._backend_hash.get (backend.name);
455 if (backend_existing != null && backend_existing != backend)
457 ((!) backend_existing).unprepare.begin ();
458 this._prepared_backends.unset (((!) backend_existing).name);
461 this._debug._register_domain (backend.name);
463 this._backend_hash.set (backend.name, backend);
466 private bool _backend_is_enabled (string name)
468 var all_others_enabled = true;
470 if (this._backends_allowed != null &&
471 !(name in (!) this._backends_allowed))
474 if (this._backends_disabled != null &&
475 name in (!) this._backends_disabled)
480 all_others_enabled = this._backends_key_file.get_boolean (
481 BackendStore.KEY_FILE_GROUP_ALL_OTHERS, "enabled");
483 catch (KeyFileError e)
485 if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
486 !(e is KeyFileError.KEY_NOT_FOUND))
488 warning ("Couldn't determine whether to enable or disable " +
489 "backends not listed in backend key file. Defaulting to %s.",
490 all_others_enabled ? "enabled" : "disabled");
494 debug ("No catch-all entry in the backend key file. %s " +
495 "unlisted backends.",
496 all_others_enabled ? "Enabling" : "Disabling");
499 /* fall back to the default in case of any level of failure */
505 enabled = this._backends_key_file.get_boolean (name, "enabled");
507 catch (KeyFileError e)
509 /* if there's no entry for this backend, use the default set above */
510 if ((e is KeyFileError.GROUP_NOT_FOUND) ||
511 (e is KeyFileError.KEY_NOT_FOUND))
513 debug ("Found no entry for backend '%s'.enabled in backend " +
514 "keyfile. %s according to '%s' setting.",
516 all_others_enabled ? "Enabling" : "Disabling",
517 BackendStore.KEY_FILE_GROUP_ALL_OTHERS);
518 enabled = all_others_enabled;
520 else if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
521 !(e is KeyFileError.KEY_NOT_FOUND))
523 warning ("Couldn't check enabled state of backend '%s': %s\n" +
524 "Disabling backend.",
534 * Get a backend from the store by name. If a backend is returned, its
535 * reference count is increased.
537 * @param name the backend name to retrieve
538 * @return the backend, or ``null`` if none could be found
542 public Backend? dup_backend_by_name (string name)
544 return this._backend_hash.get (name);
548 * List the currently loaded backends.
550 * @return a list of the backends currently in the BackendStore
552 public Collection<Backend> list_backends ()
554 return this._backend_hash.values.read_only_view;
560 * Mark a backend as enabled, such that the BackendStore will always attempt
561 * to load it when {@link BackendStore.load_backends} is called. This will
562 * not load the backend if it's not currently loaded.
564 * This method is safe to call multiple times concurrently (e.g. an
565 * asynchronous call may begin after a previous asynchronous call for the same
566 * backend name has begun and before it has finished).
568 * If the backend is disallowed by the FOLKS_BACKENDS_ALLOWED
569 * and/or FOLKS_BACKENDS_DISABLED environment variables, this method
570 * will store the fact that it should be enabled in future, but will
571 * not enable it during this application run.
573 * @param name the name of the backend to enable
576 public async void enable_backend (string name)
578 this._backends_key_file.set_boolean (name, "enabled", true);
579 yield this._save_key_file ();
585 * Mark a backend as disabled, such that it won't be loaded even when the
586 * client application is restarted. This will not remove the backend if it's
589 * This method is safe to call multiple times concurrently (e.g. an
590 * asynchronous call may begin after a previous asynchronous call for the same
591 * backend name has begun and before it has finished).
593 * @param name the name of the backend to disable
596 public async void disable_backend (string name)
598 this._backends_key_file.set_boolean (name, "enabled", false);
599 yield this._save_key_file ();
602 /* This method is safe to call multiple times concurrently. */
603 private async HashMap<string, File>? _get_modules_from_dir (File dir)
605 debug ("Searching for modules in folder '%s' ..", dir.get_path ());
608 FileAttribute.STANDARD_NAME + "," +
609 FileAttribute.STANDARD_TYPE + "," +
610 FileAttribute.STANDARD_IS_SYMLINK + "," +
611 FileAttribute.STANDARD_CONTENT_TYPE;
613 GLib.List<FileInfo> infos;
616 FileEnumerator enumerator =
617 yield dir.enumerate_children_async (attributes,
618 FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
620 infos = yield enumerator.next_files_async (int.MAX,
621 Priority.DEFAULT, null);
625 /* Translators: the first parameter is a folder path and the second
626 * is an error message. */
627 critical (_("Error listing contents of folder '%s': %s"),
628 dir.get_path (), error.message);
633 var modules_final = new HashMap<string, File> ();
635 string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
636 foreach (var info in infos)
638 var file = dir.get_child (info.get_name ());
639 var file_type = info.get_file_type ();
640 unowned string content_type = info.get_content_type ();
641 /* don't load the library multiple times for its various symlink
643 var is_symlink = info.get_is_symlink ();
645 string? mime = ContentType.get_mime_type (content_type);
647 if (file_type == FileType.DIRECTORY)
649 var modules = yield this._get_modules_from_dir (file);
652 foreach (var entry in ((!) modules).entries)
654 modules_final.set (entry.key, entry.value);
658 else if (mime == "application/x-sharedlib" && !is_symlink)
660 var path = file.get_path ();
663 modules_final.set ((!) path, file);
666 else if (mime == null)
669 "The content type of '%s' could not be determined. Have you installed shared-mime-info?",
673 * We should have only .la .so and sub-directories, except if FOLKS_BACKEND_PATH is set.
674 * Then we will run into all kinds of files.
676 else if (_path == null &&
677 mime != "application/x-sharedlib" &&
678 mime != "application/x-shared-library-la" &&
679 mime != "inode/directory")
681 warning ("The content type of '%s' appears to be '%s' which looks suspicious. Have you installed shared-mime-info?", file.get_path (), mime);
685 debug ("Finished searching for modules in folder '%s'",
688 return modules_final;
691 private void _load_module_from_file (File file)
693 var _file_path = file.get_path ();
694 if (_file_path == null)
698 var file_path = (!) _file_path;
700 if (this._modules.has_key (file_path))
703 var _module = Module.open (file_path, ModuleFlags.BIND_LOCAL);
706 warning ("Failed to load module from path '%s': %s",
707 file_path, Module.error ());
711 unowned Module module = (!) _module;
715 /* this causes the module to call add_backend() for its backends (adding
716 * them to the backend hash); any backends that already existed will be
717 * removed if they've since been disabled */
718 if (!module.symbol("module_init", out function))
720 warning ("Failed to find entry point function '%s' in '%s': %s",
728 ModuleInitFunc module_init = (ModuleInitFunc) function;
729 assert (module_init != null);
731 this._modules.set (file_path, module);
733 /* We don't want our modules to ever unload */
734 module.make_resident ();
738 debug ("Loaded module source: '%s'", module.name ());
741 /* This method is safe to call multiple times concurrently. */
742 private async static void _get_file_info (File file,
752 /* Query for the MIME type; if the file doesn't exist, we'll get an
753 * appropriate error back, so this also checks for existence. */
754 file_info = yield file.query_info_async (FileAttribute.STANDARD_TYPE,
755 FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
759 if (error is IOError.NOT_FOUND)
761 /* Translators: the parameter is a filename. */
762 critical (_("File or directory '%s' does not exist."),
767 /* Translators: the parameter is a filename. */
768 critical (_("Failed to get content type for '%s'."),
775 is_file = (file_info.get_file_type () == FileType.REGULAR);
776 is_dir = (file_info.get_file_type () == FileType.DIRECTORY);
779 /* This method is safe to call multiple times concurrently. */
780 private async void _load_disabled_backend_names ()
782 /* If set, this is a list of allowed backends. No others can be enabled,
783 * even if the keyfile says they ought to be.
784 * The default is equivalent to "all", which allows any backend.
786 * Regression tests and benchmarks can use this to restrict themselves
787 * to a small set of backends for which they have done the necessary
788 * setup/configuration/sandboxing. */
789 var envvar = Environment.get_variable ("FOLKS_BACKENDS_ALLOWED");
793 /* Allow space, comma or colon separation, consistent with
794 * g_parse_debug_string(). */
795 var tokens = envvar.split_set (" ,:");
797 this._backends_allowed = new HashSet<string> ();
799 foreach (unowned string s in tokens)
803 this._backends_allowed = null;
808 this._backends_allowed.add (s);
811 if (this._backends_allowed != null)
813 debug ("Backends limited by FOLKS_BACKENDS_ALLOWED:");
815 foreach (unowned string s in tokens)
816 debug ("Backend '%s' is allowed", s);
818 debug ("All other backends disabled by FOLKS_BACKENDS_ALLOWED");
822 /* If set, this is a list of disallowed backends.
823 * They are not enabled, even if the keyfile says they ought to be. */
824 envvar = Environment.get_variable ("FOLKS_BACKENDS_DISABLED");
828 var tokens = envvar.split_set (" ,:");
830 this._backends_disabled = new HashSet<string> ();
832 foreach (unowned string s in tokens)
836 debug ("Backend '%s' disabled by FOLKS_BACKENDS_DISABLED", s);
837 this._backends_disabled.add (s);
843 unowned string? path = Environment.get_variable (
844 "FOLKS_BACKEND_STORE_KEY_FILE_PATH");
847 file = File.new_for_path (Environment.get_user_data_dir ());
848 file = file.get_child ("folks");
849 file = file.get_child ("backends.ini");
851 debug ("Using built-in backends key file '%s' (override with " +
852 "environment variable FOLKS_BACKEND_STORE_KEY_FILE_PATH)",
857 file = File.new_for_path ((!) path);
858 debug ("Using environment variable " +
859 "FOLKS_BACKEND_STORE_KEY_FILE_PATH = '%s' to load the backends " +
860 "key file.", (!) path);
863 this._config_file = file;
865 /* Load the disabled backends file */
866 var key_file = new GLib.KeyFile ();
871 yield file.load_contents_async (null, out contents, null);
872 unowned string contents_s = (string) contents;
874 if (contents_s.length > 0)
876 key_file.load_from_data (contents_s,
877 contents_s.length, KeyFileFlags.KEEP_COMMENTS);
882 if (!(e1 is IOError.NOT_FOUND))
884 warning ("The backends key file '%s' could not be loaded: %s",
885 file.get_path (), e1.message);
891 /* Update the key file in memory, whether the new one is empty or
893 this._backends_key_file = (owned) key_file;
897 /* This method is safe to call multiple times concurrently. */
898 private async void _save_key_file ()
900 var key_file_data = this._backends_key_file.to_data ();
902 debug ("Saving backend key file '%s'.", this._config_file.get_path ());
906 /* Note: We have to use key_file_data.size () here to get its length
907 * in _bytes_ rather than _characters_. bgo#628930.
908 * In Vala >= 0.11, string.size() has been deprecated in favour of
909 * string.length (which now returns the byte length, whereas in
910 * Vala <= 0.10, it returned the character length). FIXME: We need to
911 * take this into account until we depend explicitly on
913 yield this._config_file.replace_contents_async (key_file_data.data,
914 null, false, FileCreateFlags.PRIVATE,
919 warning ("Could not write updated backend key file '%s': %s",
920 this._config_file.get_path (), e.message);