d80eceb799bbcb6d0fc9a01a19953651d5d5269e
[platform/upstream/folks.git] / folks / backend-store.vala
1 /*
2  * Copyright (C) 2008 Nokia Corporation.
3  * Copyright (C) 2008 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>.
4  * Copyright (C) 2010 Collabora Ltd.
5  *
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.
10  *
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.
15  *
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/>.
18  *
19  * Authors: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
20  *          Travis Reitter <travis.reitter@collabora.co.uk>
21  *
22  * This file was originally part of Rygel.
23  */
24
25 using Gee;
26 using GLib;
27
28 extern const string G_LOG_DOMAIN;
29
30 /**
31  * Responsible for backend loading.
32  *
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.
37  */
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);
43
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;
57   private Debug _debug;
58
59   /**
60    * This keyword in the keyfile acts as a wildcard for all backends not already
61    * listed in the same file.
62    *
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).
66    *
67    * To avoid conflicts, backends must not use this as a name.
68    *
69    * @since 0.4.0
70    */
71   public static string KEY_FILE_GROUP_ALL_OTHERS = "all-others";
72
73   /**
74    * Emitted when a backend has been added to the BackendStore.
75    *
76    * This will not be emitted until after {@link BackendStore.load_backends}
77    * has been called.
78    *
79    * {@link Backend}s referenced in this signal are also included in
80    * {@link BackendStore.enabled_backends}.
81    *
82    * @param backend the new {@link Backend}
83    */
84   public signal void backend_available (Backend backend);
85
86   /**
87    * The list of backends visible to this store which have not been explicitly
88    * disabled.
89    *
90    * This list will be empty before {@link BackendStore.load_backends} has been
91    * called.
92    *
93    * The backends in this list have been prepared and are ready to use.
94    *
95    * @since 0.5.1
96    */
97   public Map<string, Backend> enabled_backends
98     {
99       /* Return a read-only view of the map */
100       get { return this._prepared_backends_ro; }
101
102       private set {}
103     }
104
105   /**
106    * Whether {@link BackendStore.prepare} has successfully completed for this
107    * store.
108    *
109    * @since 0.3.0
110    */
111   public bool is_prepared
112     {
113       get { return this._is_prepared; }
114
115       private set {}
116     }
117
118   /**
119    * Create a new BackendStore.
120    */
121   public static BackendStore dup ()
122     {
123       if (BackendStore._instance == null)
124         {
125           /* use an intermediate variable to force a strong reference */
126           var new_instance = new BackendStore ();
127           BackendStore._instance = new_instance;
128
129           return new_instance;
130         }
131
132       return (!) BackendStore._instance;
133     }
134
135   private BackendStore ()
136     {
137       Object ();
138     }
139
140   construct
141     {
142       /* Treat this as a library init function */
143       var debug_no_colour = Environment.get_variable ("FOLKS_DEBUG_NO_COLOUR");
144       this._debug =
145           Debug.dup_with_flags (Environment.get_variable ("G_MESSAGES_DEBUG"),
146               (debug_no_colour == null || debug_no_colour == "0"));
147
148       /* register the core debug messages */
149       this._debug._register_domain (G_LOG_DOMAIN);
150
151       this._debug.print_status.connect (this._debug_print_status);
152
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;
157     }
158
159   ~BackendStore ()
160     {
161       /* Finalize all the loaded modules that have finalize functions */
162       foreach (var module in this._modules.values)
163         {
164           void* func;
165           if (module.symbol ("module_finalize", out func))
166             {
167               ModuleFinalizeFunc module_finalize = (ModuleFinalizeFunc) func;
168               module_finalize (this);
169             }
170         }
171
172       /* Disconnect from the debug handler */
173       this._debug.print_status.disconnect (this._debug_print_status);
174
175       /* manually clear the singleton instance */
176       BackendStore._instance = null;
177     }
178
179   private void _debug_print_status (Debug debug)
180     {
181       const string domain = Debug.STATUS_LOG_DOMAIN;
182       const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
183
184       debug.print_heading (domain, level, "BackendStore (%p)", this);
185       debug.print_line (domain, level, "%u Backends:",
186           this._backend_hash.size);
187
188       debug.indent ();
189
190       foreach (var backend in this._backend_hash.values)
191         {
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"
198           );
199           debug.print_line (domain, level, "%u PersonaStores:",
200               backend.persona_stores.size);
201
202           debug.indent ();
203
204           foreach (var persona_store in backend.persona_stores.values)
205             {
206               string? trust_level = null;
207
208               switch (persona_store.trust_level)
209                 {
210                   case PersonaStoreTrust.NONE:
211                     trust_level = "none";
212                     break;
213                   case PersonaStoreTrust.PARTIAL:
214                     trust_level = "partial";
215                     break;
216                   case PersonaStoreTrust.FULL:
217                     trust_level = "full";
218                     break;
219                   default:
220                     assert_not_reached ();
221                 }
222
223               var writeable_props = string.joinv (",",
224                   persona_store.always_writeable_properties);
225
226               debug.print_heading (domain, level, "PersonaStore (%p)",
227                   persona_store);
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",
232                   "Is primary store?",
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 ()
238               );
239             }
240
241           debug.unindent ();
242         }
243
244       debug.unindent ();
245
246       debug.print_line (domain, level, "");
247     }
248
249   /**
250    * Prepare the BackendStore for use.
251    *
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.
255    *
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).
259    *
260    * @since 0.3.0
261    */
262   public async void prepare ()
263     {
264       Internal.profiling_start ("preparing BackendStore");
265
266       /* (re-)load the list of disabled backends */
267       yield this._load_disabled_backend_names ();
268
269       if (this._is_prepared == false)
270         {
271           this._is_prepared = true;
272           this.notify_property ("is-prepared");
273         }
274
275       Internal.profiling_end ("preparing BackendStore");
276     }
277
278   /**
279    * Find, load, and prepare all backends which are not disabled.
280    *
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.
284    *
285    * This method is not safe to call multiple times concurrently.
286    *
287    * @throws GLib.Error currently unused
288    */
289   public async void load_backends () throws GLib.Error
290     {
291       assert (Module.supported());
292
293       Internal.profiling_start ("loading backends in BackendStore");
294
295       yield this.prepare ();
296
297       /* unload backends that have been disabled since they were loaded */
298       foreach (var backend_existing in this._backend_hash.values)
299         {
300           yield this._backend_unload_if_needed (backend_existing);
301         }
302
303       Internal.profiling_point ("unloaded backends in BackendStore");
304
305       string? _path = Environment.get_variable ("FOLKS_BACKEND_PATH");
306       string path;
307
308       if (_path == null)
309         {
310           path = BuildConf.BACKEND_DIR;
311
312           debug ("Using built-in backend dir '%s' (override with " +
313               "environment variable FOLKS_BACKEND_PATH)", path);
314         }
315       else
316         {
317           path = (!) _path;
318
319           debug ("Using environment variable FOLKS_BACKEND_PATH = " +
320               "'%s' to look for backends", path);
321         }
322
323       var modules = new HashMap<string, File> ();
324       var path_split = path.split (":");
325       foreach (unowned string subpath in path_split)
326         {
327           var file = File.new_for_path (subpath);
328
329           bool is_file;
330           bool is_dir;
331           yield BackendStore._get_file_info (file, out is_file, out is_dir);
332           if (is_file)
333             {
334               modules.set (subpath, file);
335             }
336           else if (is_dir)
337             {
338               var cur_modules = yield this._get_modules_from_dir (file);
339               if (cur_modules != null)
340                 {
341                   foreach (var entry in ((!) cur_modules).entries)
342                     {
343                       modules.set (entry.key, entry.value);
344                     }
345                 }
346             }
347           else
348             {
349               critical ("FOLKS_BACKEND_PATH component '%s' is not a regular " +
350                   "file or directory; ignoring...",
351                   subpath);
352               assert_not_reached ();
353             }
354         }
355
356       Internal.profiling_point ("found modules in BackendStore");
357
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);
363
364       Internal.profiling_point ("loaded modules in BackendStore");
365
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)
369         {
370           backends_remaining++;
371           this._backend_load_if_needed.begin (backend, (o, r) =>
372             {
373               this._backend_load_if_needed.end (r);
374               backends_remaining--;
375
376               if (backends_remaining == 0)
377                 {
378                   this.load_backends.callback ();
379                 }
380             });
381         }
382       backends_remaining--;
383       if (backends_remaining > 0)
384         {
385           yield;
386         }
387
388       Internal.profiling_end ("loading backends in BackendStore");
389     }
390
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)
394     {
395       if (this._backend_is_enabled (backend.name))
396         {
397           if (!this._prepared_backends.has_key (backend.name))
398             {
399               try
400                 {
401                   yield backend.prepare ();
402
403                   debug ("New backend '%s' prepared", backend.name);
404                   this._prepared_backends.set (backend.name, backend);
405                   this.backend_available (backend);
406                 }
407               catch (GLib.Error e)
408                 {
409                   warning ("Error preparing Backend '%s': %s",
410                       backend.name, e.message);
411                 }
412             }
413         }
414     }
415
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)
419     {
420       var unloaded = false;
421
422       if (!this._backend_is_enabled (backend.name))
423         {
424           Backend? backend_existing = this._backend_hash.get (backend.name);
425           if (backend_existing != null)
426             {
427               try
428                 {
429                   yield ((!) backend_existing).unprepare ();
430                 }
431               catch (GLib.Error e)
432                 {
433                   warning ("Error unpreparing Backend '%s': %s", backend.name,
434                       e.message);
435                 }
436
437               this._prepared_backends.unset (((!) backend_existing).name);
438
439               unloaded = true;
440             }
441         }
442
443       return unloaded;
444     }
445
446   /**
447    * Add a new {@link Backend} to the BackendStore.
448    *
449    * @param backend the {@link Backend} to add
450    */
451   public void add_backend (Backend backend)
452     {
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)
456         {
457           ((!) backend_existing).unprepare.begin ();
458           this._prepared_backends.unset (((!) backend_existing).name);
459         }
460
461       this._debug._register_domain (backend.name);
462
463       this._backend_hash.set (backend.name, backend);
464     }
465
466   private bool _backend_is_enabled (string name)
467     {
468       var all_others_enabled = true;
469
470       if (this._backends_allowed != null &&
471           !(name in (!) this._backends_allowed))
472         return false;
473
474       if (this._backends_disabled != null &&
475           name in (!) this._backends_disabled)
476         return false;
477
478       try
479         {
480           all_others_enabled = this._backends_key_file.get_boolean (
481               BackendStore.KEY_FILE_GROUP_ALL_OTHERS, "enabled");
482         }
483       catch (KeyFileError e)
484         {
485           if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
486               !(e is KeyFileError.KEY_NOT_FOUND))
487             {
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");
491             }
492           else
493             {
494               debug ("No catch-all entry in the backend key file. %s " +
495                   "unlisted backends.",
496                   all_others_enabled ? "Enabling" : "Disabling");
497             }
498
499           /* fall back to the default in case of any level of failure */
500         }
501
502       var enabled = true;
503       try
504         {
505           enabled = this._backends_key_file.get_boolean (name, "enabled");
506         }
507       catch (KeyFileError e)
508         {
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))
512             {
513               debug ("Found no entry for backend '%s'.enabled in backend " +
514                   "keyfile. %s according to '%s' setting.",
515                   name,
516                   all_others_enabled ? "Enabling" : "Disabling",
517                   BackendStore.KEY_FILE_GROUP_ALL_OTHERS);
518               enabled = all_others_enabled;
519             }
520           else if (!(e is KeyFileError.GROUP_NOT_FOUND) &&
521               !(e is KeyFileError.KEY_NOT_FOUND))
522             {
523               warning ("Couldn't check enabled state of backend '%s': %s\n" +
524                   "Disabling backend.",
525                   name, e.message);
526               enabled = false;
527             }
528         }
529
530       return enabled;
531     }
532
533   /**
534    * Get a backend from the store by name. If a backend is returned, its
535    * reference count is increased.
536    *
537    * @param name the backend name to retrieve
538    * @return the backend, or ``null`` if none could be found
539    *
540    * @since 0.3.5
541    */
542   public Backend? dup_backend_by_name (string name)
543     {
544       return this._backend_hash.get (name);
545     }
546
547   /**
548    * List the currently loaded backends.
549    *
550    * @return a list of the backends currently in the BackendStore
551    */
552   public Collection<Backend> list_backends ()
553     {
554       return this._backend_hash.values.read_only_view;
555     }
556
557   /**
558    * Enable a backend.
559    *
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.
563    *
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).
567    *
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.
572    *
573    * @param name the name of the backend to enable
574    * @since 0.3.2
575    */
576   public async void enable_backend (string name)
577     {
578       this._backends_key_file.set_boolean (name, "enabled", true);
579       yield this._save_key_file ();
580     }
581
582   /**
583    * Disable a backend.
584    *
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
587    * already loaded.
588    *
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).
592    *
593    * @param name the name of the backend to disable
594    * @since 0.3.2
595    */
596   public async void disable_backend (string name)
597     {
598       this._backends_key_file.set_boolean (name, "enabled", false);
599       yield this._save_key_file ();
600     }
601
602   /* This method is safe to call multiple times concurrently. */
603   private async HashMap<string, File>? _get_modules_from_dir (File dir)
604     {
605       debug ("Searching for modules in folder '%s' ..", dir.get_path ());
606
607       var attributes =
608           FileAttribute.STANDARD_NAME + "," +
609           FileAttribute.STANDARD_TYPE + "," +
610           FileAttribute.STANDARD_IS_SYMLINK + "," +
611           FileAttribute.STANDARD_CONTENT_TYPE;
612
613       GLib.List<FileInfo> infos;
614       try
615         {
616           FileEnumerator enumerator =
617             yield dir.enumerate_children_async (attributes,
618                 FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
619
620           infos = yield enumerator.next_files_async (int.MAX,
621               Priority.DEFAULT, null);
622         }
623       catch (Error error)
624         {
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);
629
630           return null;
631         }
632
633       var modules_final = new HashMap<string, File> ();
634
635       foreach (var info in infos)
636         {
637           var file = dir.get_child (info.get_name ());
638           var file_type = info.get_file_type ();
639           unowned string content_type = info.get_content_type ();
640           /* don't load the library multiple times for its various symlink
641            * aliases */
642           var is_symlink = info.get_is_symlink ();
643
644           string? mime = ContentType.get_mime_type (content_type);
645
646           if (file_type == FileType.DIRECTORY)
647             {
648               var modules = yield this._get_modules_from_dir (file);
649               if (modules != null)
650                 {
651                   foreach (var entry in ((!) modules).entries)
652                     {
653                       modules_final.set (entry.key, entry.value);
654                     }
655                 }
656             }
657           else if (mime == "application/x-sharedlib" && !is_symlink)
658             {
659               var path = file.get_path ();
660               if (path != null)
661                 {
662                   modules_final.set ((!) path, file);
663                 }
664             }
665           else if (mime == null)
666             {
667               warning (
668                   "The content type of '%s' could not be determined. Have you installed shared-mime-info?",
669                   file.get_path ());
670             }
671           /* We should have only .la .so and sub-directories */
672           else if (mime != "application/x-sharedlib" &&
673                    mime != "application/x-shared-library-la" &&
674                    mime != "inode/directory")
675             {
676               warning ("The content type of '%s' appears to be '%s' which looks suspicious. Have you installed shared-mime-info?", file.get_path (), mime);
677             }
678         }
679
680       debug ("Finished searching for modules in folder '%s'",
681           dir.get_path ());
682
683       return modules_final;
684     }
685
686   private void _load_module_from_file (File file)
687     {
688       var _file_path = file.get_path ();
689       if (_file_path == null)
690         {
691           return;
692         }
693       var file_path = (!) _file_path;
694
695       if (this._modules.has_key (file_path))
696         return;
697
698       var _module = Module.open (file_path, ModuleFlags.BIND_LOCAL);
699       if (_module == null)
700         {
701           warning ("Failed to load module from path '%s': %s",
702                     file_path, Module.error ());
703
704           return;
705         }
706       unowned Module module = (!) _module;
707
708       void* function;
709
710       /* this causes the module to call add_backend() for its backends (adding
711        * them to the backend hash); any backends that already existed will be
712        * removed if they've since been disabled */
713       if (!module.symbol("module_init", out function))
714         {
715           warning ("Failed to find entry point function '%s' in '%s': %s",
716                     "module_init",
717                     file_path,
718                     Module.error ());
719
720           return;
721         }
722
723       ModuleInitFunc module_init = (ModuleInitFunc) function;
724       assert (module_init != null);
725
726       this._modules.set (file_path, module);
727
728       /* We don't want our modules to ever unload */
729       module.make_resident ();
730
731       module_init (this);
732
733       debug ("Loaded module source: '%s'", module.name ());
734     }
735
736   /* This method is safe to call multiple times concurrently. */
737   private async static void _get_file_info (File file,
738       out bool is_file,
739       out bool is_dir)
740     {
741       FileInfo file_info;
742       is_file = false;
743       is_dir = false;
744
745       try
746         {
747           /* Query for the MIME type; if the file doesn't exist, we'll get an
748            * appropriate error back, so this also checks for existence. */
749           file_info = yield file.query_info_async (FileAttribute.STANDARD_TYPE,
750               FileQueryInfoFlags.NONE, Priority.DEFAULT, null);
751         }
752       catch (Error error)
753         {
754           if (error is IOError.NOT_FOUND)
755             {
756               /* Translators: the parameter is a filename. */
757               critical (_("File or directory '%s' does not exist."),
758                   file.get_path ());
759             }
760           else
761             {
762               /* Translators: the parameter is a filename. */
763               critical (_("Failed to get content type for '%s'."),
764                   file.get_path ());
765             }
766
767           return;
768         }
769
770       is_file = (file_info.get_file_type () == FileType.REGULAR);
771       is_dir = (file_info.get_file_type () == FileType.DIRECTORY);
772     }
773
774   /* This method is safe to call multiple times concurrently. */
775   private async void _load_disabled_backend_names ()
776     {
777       /* If set, this is a list of allowed backends. No others can be enabled,
778        * even if the keyfile says they ought to be.
779        * The default is equivalent to "all", which allows any backend.
780        *
781        * Regression tests and benchmarks can use this to restrict themselves
782        * to a small set of backends for which they have done the necessary
783        * setup/configuration/sandboxing. */
784       var envvar = Environment.get_variable ("FOLKS_BACKENDS_ALLOWED");
785
786       if (envvar != null)
787         {
788           /* Allow space, comma or colon separation, consistent with
789            * g_parse_debug_string(). */
790           var tokens = envvar.split_set (" ,:");
791
792           this._backends_allowed = new HashSet<string> ();
793
794           foreach (unowned string s in tokens)
795             {
796               if (s == "all")
797                 {
798                   this._backends_allowed = null;
799                   break;
800                 }
801
802               if (s != "")
803                 this._backends_allowed.add (s);
804             }
805
806           if (this._backends_allowed != null)
807             {
808               debug ("Backends limited by FOLKS_BACKENDS_ALLOWED:");
809
810               foreach (unowned string s in tokens)
811                 debug ("Backend '%s' is allowed", s);
812
813               debug ("All other backends disabled by FOLKS_BACKENDS_ALLOWED");
814             }
815         }
816
817       /* If set, this is a list of disallowed backends.
818        * They are not enabled, even if the keyfile says they ought to be. */
819       envvar = Environment.get_variable ("FOLKS_BACKENDS_DISABLED");
820
821       if (envvar != null)
822         {
823           var tokens = envvar.split_set (" ,:");
824
825           this._backends_disabled = new HashSet<string> ();
826
827           foreach (unowned string s in tokens)
828             {
829               if (s != "")
830                 {
831                   debug ("Backend '%s' disabled by FOLKS_BACKENDS_DISABLED", s);
832                   this._backends_disabled.add (s);
833                 }
834             }
835         }
836
837       File file;
838       unowned string? path = Environment.get_variable (
839           "FOLKS_BACKEND_STORE_KEY_FILE_PATH");
840       if (path == null)
841         {
842           file = File.new_for_path (Environment.get_user_data_dir ());
843           file = file.get_child ("folks");
844           file = file.get_child ("backends.ini");
845
846           debug ("Using built-in backends key file '%s' (override with " +
847               "environment variable FOLKS_BACKEND_STORE_KEY_FILE_PATH)",
848               file.get_path ());
849         }
850       else
851         {
852           file = File.new_for_path ((!) path);
853           debug ("Using environment variable " +
854               "FOLKS_BACKEND_STORE_KEY_FILE_PATH = '%s' to load the backends " +
855               "key file.", (!) path);
856         }
857
858       this._config_file = file;
859
860       /* Load the disabled backends file */
861       var key_file = new GLib.KeyFile ();
862       try
863         {
864           uint8[] contents;
865
866           yield file.load_contents_async (null, out contents, null);
867           unowned string contents_s = (string) contents;
868
869           if (contents_s.length > 0)
870             {
871               key_file.load_from_data (contents_s,
872                   contents_s.length, KeyFileFlags.KEEP_COMMENTS);
873             }
874         }
875       catch (Error e1)
876         {
877           if (!(e1 is IOError.NOT_FOUND))
878             {
879               warning ("The backends key file '%s' could not be loaded: %s",
880                   file.get_path (), e1.message);
881               return;
882             }
883         }
884       finally
885         {
886           /* Update the key file in memory, whether the new one is empty or
887            * full. */
888           this._backends_key_file = (owned) key_file;
889         }
890     }
891
892   /* This method is safe to call multiple times concurrently. */
893   private async void _save_key_file ()
894     {
895       var key_file_data = this._backends_key_file.to_data ();
896
897       debug ("Saving backend key file '%s'.", this._config_file.get_path ());
898
899       try
900         {
901           /* Note: We have to use key_file_data.size () here to get its length
902            * in _bytes_ rather than _characters_. bgo#628930.
903            * In Vala >= 0.11, string.size() has been deprecated in favour of
904            * string.length (which now returns the byte length, whereas in
905            * Vala <= 0.10, it returned the character length). FIXME: We need to
906            * take this into account until we depend explicitly on
907            * Vala >= 0.11. */
908           yield this._config_file.replace_contents_async (key_file_data.data,
909               null, false, FileCreateFlags.PRIVATE,
910               null, null);
911         }
912       catch (Error e)
913         {
914           warning ("Could not write updated backend key file '%s': %s",
915               this._config_file.get_path (), e.message);
916         }
917     }
918 }