[glib/wip/gdesktopappinfo: 1/6] gdesktopappinfo: keep a list of files in the dirs



commit 9efe38f1c7aad6672a0e2c2c58db39ca529f43a9
Author: Ryan Lortie <desrt desrt ca>
Date:   Sat Jul 27 16:04:56 2013 -0400

    gdesktopappinfo: keep a list of files in the dirs
    
    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.
    
    We use a file monitor to watch for changes, invalidating our lists when
    we notice them.

 gio/gdesktopappinfo.c |  595 ++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 489 insertions(+), 106 deletions(-)
---
diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c
index 141a89a..6a4050d 100644
--- a/gio/gdesktopappinfo.c
+++ b/gio/gdesktopappinfo.c
@@ -47,6 +47,7 @@
 #include "glibintl.h"
 #include "giomodule-priv.h"
 #include "gappinfo.h"
+#include "glocaldirectorymonitor.h"
 
 
 /**
@@ -145,10 +146,348 @@ 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->filename), info);
+}
+
+enum
+{
+  DESKTOP_KEY_nil,
+
+  /* NB: must keep this list sorted */
+  DESKTOP_KEY_Actions,
+  DESKTOP_KEY_Categories,
+  DESKTOP_KEY_Comment,
+  DESKTOP_KEY_DBusActivatable,
+  DESKTOP_KEY_Exec,
+  DESKTOP_KEY_GenericName,
+  DESKTOP_KEY_Hidden,
+  DESKTOP_KEY_Icon,
+  DESKTOP_KEY_Keywords,
+  DESKTOP_KEY_MimeType,
+  DESKTOP_KEY_Name,
+  DESKTOP_KEY_NoDisplay,
+  DESKTOP_KEY_NotShowIn,
+  DESKTOP_KEY_OnlyShowIn,
+  DESKTOP_KEY_Path,
+  DESKTOP_KEY_StartupNotify,
+  DESKTOP_KEY_StartupWMClass,
+  DESKTOP_KEY_Terminal,
+  DESKTOP_KEY_TryExec,
+  DESKTOP_KEY_Type,
+  DESKTOP_KEY_Version,
+  DESKTOP_KEY_X_GNOME_FullName,
+
+  N_DESKTOP_KEYS
+};
+
+const gchar *desktop_key_names[N_DESKTOP_KEYS];
+
+const gchar desktop_key_match_category[N_DESKTOP_KEYS] = {
+  /* NB: no harm in repeating numbers in case of a tie */
+  [DESKTOP_KEY_Name]             = 1,
+  [DESKTOP_KEY_GenericName]      = 2,
+  [DESKTOP_KEY_Keywords]         = 3,
+  [DESKTOP_KEY_X_GNOME_FullName] = 4,
+  [DESKTOP_KEY_Comment]          = 5
+};
+
+#define DESKTOP_KEY_NUM_MATCH_CATEGORIES  5
+
+static void
+desktop_key_init (void)
+{
+  if G_LIKELY (desktop_key_names[1])
+    return;
+
+  desktop_key_names[DESKTOP_KEY_Actions]                = g_intern_static_string ("Actions");
+  desktop_key_names[DESKTOP_KEY_Categories]             = g_intern_static_string ("Categories");
+  desktop_key_names[DESKTOP_KEY_Comment]                = g_intern_static_string ("Comment");
+  desktop_key_names[DESKTOP_KEY_DBusActivatable]        = g_intern_static_string ("DBusActivatable");
+  desktop_key_names[DESKTOP_KEY_Exec]                   = g_intern_static_string ("Exec");
+  desktop_key_names[DESKTOP_KEY_GenericName]            = g_intern_static_string ("GenericName");
+  desktop_key_names[DESKTOP_KEY_Hidden]                 = g_intern_static_string ("Hidden");
+  desktop_key_names[DESKTOP_KEY_Icon]                   = g_intern_static_string ("Icon");
+  desktop_key_names[DESKTOP_KEY_Keywords]               = g_intern_static_string ("Keywords");
+  desktop_key_names[DESKTOP_KEY_MimeType]               = g_intern_static_string ("MimeType");
+  desktop_key_names[DESKTOP_KEY_Name]                   = g_intern_static_string ("Name");
+  desktop_key_names[DESKTOP_KEY_NoDisplay]              = g_intern_static_string ("NoDisplay");
+  desktop_key_names[DESKTOP_KEY_NotShowIn]              = g_intern_static_string ("NotShowIn");
+  desktop_key_names[DESKTOP_KEY_OnlyShowIn]             = g_intern_static_string ("OnlyShowIn");
+  desktop_key_names[DESKTOP_KEY_Path]                   = g_intern_static_string ("Path");
+  desktop_key_names[DESKTOP_KEY_StartupNotify]          = g_intern_static_string ("StartupNotify");
+  desktop_key_names[DESKTOP_KEY_StartupWMClass]         = g_intern_static_string ("StartupWMClass");
+  desktop_key_names[DESKTOP_KEY_Terminal]               = g_intern_static_string ("Terminal");
+  desktop_key_names[DESKTOP_KEY_TryExec]                = g_intern_static_string ("TryExec");
+  desktop_key_names[DESKTOP_KEY_Type]                   = g_intern_static_string ("Type");
+  desktop_key_names[DESKTOP_KEY_Version]                = g_intern_static_string ("Version");
+  desktop_key_names[DESKTOP_KEY_X_GNOME_FullName]       = g_intern_static_string ("X-GNOME-FullName");
+}
+
+static void
+insert_into_list (GSList      **categories,
+                  const gchar  *item,
+                  gint          into_bucket,
+                  gint          max_hits)
+{
+  gint n_hits = 0;
+  gint i;
+
+  for (i = 0; i <= into_bucket; i++)
+    {
+      GSList *node;
+
+      /* If we have enough hits from previous categories, stop.
+       *
+       * Note.  This check will not be applied after counting up the
+       * hits in categories[into_bucket] and that's intentional: we
+       * don't want to toss out our new item if it has the same score as
+       * items we're already returning.
+       *
+       * That might mean that we get more than max_hits items, but
+       * that's the desired behaviour.
+       */
+      if (max_hits >= 0 && n_hits >= max_hits)
+        return;
+
+      /* We want to do the dup-checking in all cases, though */
+      for (node = categories[i]; node; node = node->next)
+        {
+          /* Pointer comparison is good enough because we're using the
+           * name from the desktop index (where it is unique) and
+           * because we'll never see the same desktop file name across
+           * multiple indexes due to our use of masking.
+           */
+          if (node->data == item)
+            return;
+
+          n_hits++;
+        }
+    }
+
+  /* No duplicate found at this level or any before, and not too many
+   * items in earlier buckets, so add our item.
+   */
+  categories[into_bucket] = g_slist_prepend (categories[into_bucket], (gchar *) item);
+  n_hits++;
+
+  /* We may need to remove ourselves from a later category if we already
+   * had a weaker hit for this item.
+   *
+   * We may also need to blow away an entire category if n_hits is big
+   * enough.
+   */
+  for (i = into_bucket + 1; i < DESKTOP_KEY_NUM_MATCH_CATEGORIES; i++)
+    {
+      if (!categories[i])
+        continue;
+
+      if (max_hits >= 0 && n_hits >= max_hits)
+        {
+          g_slist_free (categories[i]);
+          categories[i] = NULL;
+        }
+      else
+        {
+          GSList **ptr;
+
+          for (ptr = &categories[i]; *ptr; ptr = &(*ptr)->next)
+            if ((*ptr)->data == item)
+              {
+                /* Found us! */
+                *ptr = g_slist_delete_link (*ptr, *ptr);
+                n_hits--;
+
+                /* Since we effectively didn't add any items this time
+                 * around, we know we will not need to blow away later
+                 * categories.  We also know that we will not appear in
+                 * a later category, since we appeared here.
+                 *
+                 * So, we're done.
+                 */
+                return;
+              }
+        }
+    }
+}
+
+/* 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 +510,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)
 {
-  if (g_once_init_enter (&desktop_file_dirs))
+  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)
+{
+  gint i;
+
+  g_mutex_lock (&desktop_file_dir_lock);
+
+  if (desktop_file_dirs == NULL)
     {
       const char * const *data_dirs;
       GArray *tmp;
@@ -192,10 +624,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 +1036,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;
-
-      p = basename;
-      while ((p = strchr (p, '-')) != NULL)
-       {
-         *p = '/';
+      appinfo = desktop_file_dir_get_app (&desktop_file_dirs[i], desktop_id);
 
-         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 +2781,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 +3209,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 +3235,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 +3254,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 */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]