gdesktopappinfo: keep a list of files in the dirs
authorRyan Lortie <desrt@desrt.ca>
Sat, 27 Jul 2013 20:04:56 +0000 (16:04 -0400)
committerRyan Lortie <desrt@desrt.ca>
Wed, 6 Nov 2013 15:56:25 +0000 (10:56 -0500)
In each DesktopFileDir, store a list of desktop files for that
directory.  This speeds up opening desktop files by name because we can
skip statting in directories that we know don't have the file and also
speeds up _get_all() because we can avoid enumeration.

This also improves our support for dealing with names like
'kde4/kate.desktop' (equivalent to kde4-kate.desktop) since we find out
about all of these files are the start and don't need to guess about
which '-' to change to a '/'.  It also means that we can easily deal
with more than one level of such prefixes.

We use a file monitor to watch for changes, invalidating our lists when
we notice them.

https://bugzilla.gnome.org/show_bug.cgi?id=711520

gio/gdesktopappinfo.c

index 141a89a..c67186f 100644 (file)
@@ -47,6 +47,7 @@
 #include "glibintl.h"
 #include "giomodule-priv.h"
 #include "gappinfo.h"
+#include "glocaldirectorymonitor.h"
 
 
 /**
@@ -145,10 +146,187 @@ static gchar *g_desktop_env = NULL;
 typedef struct
 {
   gchar                      *path;
+  GLocalDirectoryMonitor     *monitor;
+  GHashTable                 *app_names;
+  gboolean                    is_setup;
 } DesktopFileDir;
 
 static DesktopFileDir *desktop_file_dirs;
 static guint           n_desktop_file_dirs;
+static GMutex          desktop_file_dir_lock;
+
+/* Monitor 'changed' signal handler {{{2 */
+static void desktop_file_dir_reset (DesktopFileDir *dir);
+
+static void
+desktop_file_dir_changed (GFileMonitor      *monitor,
+                          GFile             *file,
+                          GFile             *other_file,
+                          GFileMonitorEvent  event_type,
+                          gpointer           user_data)
+{
+  DesktopFileDir *dir = user_data;
+
+  /* We are not interested in receiving notifications forever just
+   * because someone asked about one desktop file once.
+   *
+   * After we receive the first notification, reset the dir, destroying
+   * the monitor.  We will take this as a hint, next time that we are
+   * asked, that we need to check if everything is up to date.
+   */
+  g_mutex_lock (&desktop_file_dir_lock);
+
+  desktop_file_dir_reset (dir);
+
+  g_mutex_unlock (&desktop_file_dir_lock);
+}
+
+/* Internal utility functions {{{2 */
+
+/*< internal >
+ * desktop_file_dir_app_name_is_masked:
+ * @dir: a #DesktopFileDir
+ * @app_name: an application ID
+ *
+ * Checks if @app_name is masked for @dir.
+ *
+ * An application is masked if a similarly-named desktop file exists in
+ * a desktop file directory with higher precedence.  Masked desktop
+ * files should be ignored.
+ */
+static gboolean
+desktop_file_dir_app_name_is_masked (DesktopFileDir *dir,
+                                     const gchar    *app_name)
+{
+  while (dir > desktop_file_dirs)
+    {
+      dir--;
+
+      if (dir->app_names && g_hash_table_contains (dir->app_names, app_name))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+/*< internal >
+ * add_to_table_if_appropriate:
+ * @apps: a string to GDesktopAppInfo hash table
+ * @app_name: the name of the application
+ * @info: a #GDesktopAppInfo, or NULL
+ *
+ * If @info is non-%NULL and non-hidden, then add it to @apps, using
+ * @app_name as a key.
+ *
+ * If @info is non-%NULL then this function will consume the passed-in
+ * reference.
+ */
+static void
+add_to_table_if_appropriate (GHashTable      *apps,
+                             const gchar     *app_name,
+                             GDesktopAppInfo *info)
+{
+  if (!info)
+    return;
+
+  if (info->hidden)
+    {
+      g_object_unref (info);
+      return;
+    }
+
+  g_free (info->desktop_id);
+  info->desktop_id = g_strdup (app_name);
+
+  g_hash_table_insert (apps, g_strdup (info->desktop_id), info);
+}
+
+/* Support for unindexed DesktopFileDirs {{{2 */
+static void
+get_apps_from_dir (GHashTable **apps,
+                   const char  *dirname,
+                   const char  *prefix)
+{
+  const char *basename;
+  GDir *dir;
+
+  dir = g_dir_open (dirname, 0, NULL);
+
+  if (dir == NULL)
+    return;
+
+  while ((basename = g_dir_read_name (dir)) != NULL)
+    {
+      gchar *filename;
+
+      filename = g_build_filename (dirname, basename, NULL);
+
+      if (g_str_has_suffix (basename, ".desktop"))
+        {
+          gchar *app_name;
+
+          app_name = g_strconcat (prefix, basename, NULL);
+
+          if (*apps == NULL)
+            *apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+          g_hash_table_insert (*apps, app_name, g_strdup (filename));
+        }
+      else if (g_file_test (filename, G_FILE_TEST_IS_DIR))
+        {
+          gchar *subprefix;
+
+          subprefix = g_strconcat (prefix, basename, "-", NULL);
+          get_apps_from_dir (apps, filename, subprefix);
+          g_free (subprefix);
+        }
+
+      g_free (filename);
+    }
+
+  g_dir_close (dir);
+}
+
+static void
+desktop_file_dir_unindexed_init (DesktopFileDir *dir)
+{
+  get_apps_from_dir (&dir->app_names, dir->path, "");
+}
+
+static GDesktopAppInfo *
+desktop_file_dir_unindexed_get_app (DesktopFileDir *dir,
+                                    const gchar    *desktop_id)
+{
+  const gchar *filename;
+
+  filename = g_hash_table_lookup (dir->app_names, desktop_id);
+
+  if (!filename)
+    return NULL;
+
+  return g_desktop_app_info_new_from_filename (filename);
+}
+
+static void
+desktop_file_dir_unindexed_get_all (DesktopFileDir *dir,
+                                    GHashTable     *apps)
+{
+  GHashTableIter iter;
+  gpointer app_name;
+  gpointer filename;
+
+  if (dir->app_names == NULL)
+    return;
+
+  g_hash_table_iter_init (&iter, dir->app_names);
+  while (g_hash_table_iter_next (&iter, &app_name, &filename))
+    {
+      if (desktop_file_dir_app_name_is_masked (dir, app_name))
+        continue;
+
+      add_to_table_if_appropriate (apps, app_name, g_desktop_app_info_new_from_filename (filename));
+    }
+}
 
 /* DesktopFileDir "API" {{{2 */
 
@@ -171,12 +349,105 @@ desktop_file_dir_create (GArray      *array,
   g_array_append_val (array, dir);
 }
 
-/* Global setup API {{{2 */
+/*< internal >
+ * desktop_file_dir_reset:
+ * @dir: a #DesktopFileDir
+ *
+ * Cleans up @dir, releasing most resources that it was using.
+ */
+static void
+desktop_file_dir_reset (DesktopFileDir *dir)
+{
+  if (dir->monitor)
+    {
+      g_signal_handlers_disconnect_by_func (dir->monitor, desktop_file_dir_changed, dir);
+      g_object_unref (dir->monitor);
+      dir->monitor = NULL;
+    }
+
+  if (dir->app_names)
+    {
+      g_hash_table_unref (dir->app_names);
+      dir->app_names = NULL;
+    }
+
+  dir->is_setup = FALSE;
+}
 
+/*< internal >
+ * desktop_file_dir_init:
+ * @dir: a #DesktopFileDir
+ *
+ * Does initial setup for @dir
+ *
+ * You should only call this if @dir is not already setup.
+ */
 static void
-desktop_file_dirs_refresh (void)
+desktop_file_dir_init (DesktopFileDir *dir)
+{
+  g_assert (!dir->is_setup);
+
+  g_assert (!dir->monitor);
+  dir->monitor = g_local_directory_monitor_new_in_worker (dir->path, G_FILE_MONITOR_NONE, NULL);
+
+  if (dir->monitor)
+    {
+      g_signal_connect (dir->monitor, "changed", G_CALLBACK (desktop_file_dir_changed), dir);
+      g_local_directory_monitor_start (dir->monitor);
+    }
+
+  desktop_file_dir_unindexed_init (dir);
+
+  dir->is_setup = TRUE;
+}
+
+/*< internal >
+ * desktop_file_dir_get_app:
+ * @dir: a DesktopFileDir
+ * @desktop_id: the desktop ID to load
+ *
+ * Creates the #GDesktopAppInfo for the given @desktop_id if it exists
+ * within @dir, even if it is hidden.
+ *
+ * This function does not check if @desktop_id would be masked by a
+ * directory with higher precedence.  The caller must do so.
+ */
+static GDesktopAppInfo *
+desktop_file_dir_get_app (DesktopFileDir *dir,
+                          const gchar    *desktop_id)
+{
+  if (!dir->app_names)
+    return NULL;
+
+  return desktop_file_dir_unindexed_get_app (dir, desktop_id);
+}
+
+/*< internal >
+ * desktop_file_dir_get_all:
+ * @dir: a DesktopFileDir
+ * @apps: a #GHashTable<string, GDesktopAppInfo>
+ *
+ * Loads all desktop files in @dir and adds them to @apps, careful to
+ * ensure we don't add any files masked by a similarly-named file in a
+ * higher-precedence directory.
+ */
+static void
+desktop_file_dir_get_all (DesktopFileDir *dir,
+                          GHashTable     *apps)
+{
+  desktop_file_dir_unindexed_get_all (dir, apps);
+}
+
+/* Lock/unlock and global setup API {{{2 */
+
+static void
+desktop_file_dirs_lock (void)
 {
-  if (g_once_init_enter (&desktop_file_dirs))
+  gint i;
+
+  g_mutex_lock (&desktop_file_dir_lock);
+
+  if (desktop_file_dirs == NULL)
     {
       const char * const *data_dirs;
       GArray *tmp;
@@ -192,10 +463,39 @@ desktop_file_dirs_refresh (void)
       for (i = 0; data_dirs[i]; i++)
         desktop_file_dir_create (tmp, data_dirs[i]);
 
+      desktop_file_dirs = (DesktopFileDir *) tmp->data;
       n_desktop_file_dirs = tmp->len;
 
-      g_once_init_leave (&desktop_file_dirs, (DesktopFileDir *) g_array_free (tmp, FALSE));
+      g_array_free (tmp, FALSE);
     }
+
+  for (i = 0; i < n_desktop_file_dirs; i++)
+    if (!desktop_file_dirs[i].is_setup)
+      desktop_file_dir_init (&desktop_file_dirs[i]);
+}
+
+static void
+desktop_file_dirs_unlock (void)
+{
+  g_mutex_unlock (&desktop_file_dir_lock);
+}
+
+static void
+desktop_file_dirs_refresh (void)
+{
+  desktop_file_dirs_lock ();
+  desktop_file_dirs_unlock ();
+}
+
+static void
+desktop_file_dirs_invalidate_user (void)
+{
+  g_mutex_lock (&desktop_file_dir_lock);
+
+  if (n_desktop_file_dirs)
+    desktop_file_dir_reset (&desktop_file_dirs[0]);
+
+  g_mutex_unlock (&desktop_file_dir_lock);
 }
 
 /* GDesktopAppInfo implementation {{{1 */
@@ -575,47 +875,24 @@ g_desktop_app_info_new_from_filename (const char *filename)
 GDesktopAppInfo *
 g_desktop_app_info_new (const char *desktop_id)
 {
-  GDesktopAppInfo *appinfo;
-  char *basename;
-  int i;
+  GDesktopAppInfo *appinfo = NULL;
+  guint i;
 
-  desktop_file_dirs_refresh ();
+  desktop_file_dirs_lock ();
 
-  basename = g_strdup (desktop_id);
-  
   for (i = 0; i < n_desktop_file_dirs; i++)
     {
-      const gchar *path = desktop_file_dirs[i].path;
-      char *filename;
-      char *p;
-
-      filename = g_build_filename (path, desktop_id, NULL);
-      appinfo = g_desktop_app_info_new_from_filename (filename);
-      g_free (filename);
-      if (appinfo != NULL)
-       goto found;
+      appinfo = desktop_file_dir_get_app (&desktop_file_dirs[i], desktop_id);
 
-      p = basename;
-      while ((p = strchr (p, '-')) != NULL)
-       {
-         *p = '/';
-
-         filename = g_build_filename (path, basename, NULL);
-         appinfo = g_desktop_app_info_new_from_filename (filename);
-         g_free (filename);
-         if (appinfo != NULL)
-           goto found;
-         *p = '-';
-         p++;
-       }
+      if (appinfo)
+        break;
     }
 
-  g_free (basename);
-  return NULL;
+  desktop_file_dirs_unlock ();
+
+  if (appinfo == NULL)
+    return NULL;
 
- found:
-  g_free (basename);
-  
   g_free (appinfo->desktop_id);
   appinfo->desktop_id = g_strdup (desktop_id);
 
@@ -2343,6 +2620,15 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo  *info,
 
   run_update_command ("update-desktop-database", "applications");
 
+  /* We just dropped a file in the user's desktop file directory.  Save
+   * the monitor the bother of having to notice it and invalidate
+   * immediately.
+   *
+   * This means that calls directly following this will be able to see
+   * the results immediately.
+   */
+  desktop_file_dirs_invalidate_user ();
+
   return TRUE;
 }
 
@@ -2762,69 +3048,6 @@ g_app_info_get_default_for_uri_scheme (const char *uri_scheme)
   return app_info;
 }
 
-static void
-get_apps_from_dir (GHashTable *apps, 
-                   const char *dirname, 
-                   const char *prefix)
-{
-  GDir *dir;
-  const char *basename;
-  char *filename, *subprefix, *desktop_id;
-  gboolean hidden;
-  GDesktopAppInfo *appinfo;
-  
-  dir = g_dir_open (dirname, 0, NULL);
-  if (dir)
-    {
-      while ((basename = g_dir_read_name (dir)) != NULL)
-       {
-         filename = g_build_filename (dirname, basename, NULL);
-         if (g_str_has_suffix (basename, ".desktop"))
-           {
-             desktop_id = g_strconcat (prefix, basename, NULL);
-
-             /* Use _extended so we catch NULLs too (hidden) */
-             if (!g_hash_table_lookup_extended (apps, desktop_id, NULL, NULL))
-               {
-                 appinfo = g_desktop_app_info_new_from_filename (filename);
-                  hidden = FALSE;
-
-                 if (appinfo && g_desktop_app_info_get_is_hidden (appinfo))
-                   {
-                     g_object_unref (appinfo);
-                     appinfo = NULL;
-                     hidden = TRUE;
-                   }
-                                     
-                 if (appinfo || hidden)
-                   {
-                     g_hash_table_insert (apps, g_strdup (desktop_id), appinfo);
-
-                     if (appinfo)
-                       {
-                         /* Reuse instead of strdup here */
-                         appinfo->desktop_id = desktop_id;
-                         desktop_id = NULL;
-                       }
-                   }
-               }
-             g_free (desktop_id);
-           }
-         else
-           {
-             if (g_file_test (filename, G_FILE_TEST_IS_DIR))
-               {
-                 subprefix = g_strconcat (prefix, basename, "-", NULL);
-                 get_apps_from_dir (apps, filename, subprefix);
-                 g_free (subprefix);
-               }
-           }
-         g_free (filename);
-       }
-      g_dir_close (dir);
-    }
-}
-
 /* "Get all" API {{{2 */
 
 /**
@@ -2851,15 +3074,14 @@ g_app_info_get_all (void)
   int i;
   GList *infos;
 
-  desktop_file_dirs_refresh ();
-
-  apps = g_hash_table_new_full (g_str_hash, g_str_equal,
-                               g_free, NULL);
+  apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
 
+  desktop_file_dirs_lock ();
 
   for (i = 0; i < n_desktop_file_dirs; i++)
-    get_apps_from_dir (apps, desktop_file_dirs[i].path, "");
+    desktop_file_dir_get_all (&desktop_file_dirs[i], apps);
 
+  desktop_file_dirs_unlock ();
 
   infos = NULL;
   g_hash_table_iter_init (&iter, apps);
@@ -2871,7 +3093,7 @@ g_app_info_get_all (void)
 
   g_hash_table_destroy (apps);
 
-  return g_list_reverse (infos);
+  return infos;
 }
 
 /* Caching of mimeinfo.cache and defaults.list files {{{2 */