Commit 9efe38f1 authored by Allison Karlitskaya's avatar Allison Karlitskaya

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.
parent be2656f1
......@@ -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 */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment