/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ #include "config.h" #include "shell-app-cache-private.h" /** * SECTION:shell-app-cache * @title: ShellAppCache * @short_description: application information cache * * The #ShellAppCache is responsible for caching information about #GAppInfo * to ensure that the compositor thread never needs to perform disk reads to * access them. All of the work is done off-thread. When the new data has * been loaded, a #ShellAppCache::changed signal is emitted. * * Additionally, the #ShellAppCache caches information about translations for * directories. This allows translation provided in [Desktop Entry] GKeyFiles * to be available when building StLabel and other elements without performing * costly disk reads. * * Various monitors are used to keep this information up to date while the * Shell is running. */ #define DEFAULT_TIMEOUT_SECONDS 5 struct _ShellAppCache { GObject parent_instance; GAppInfoMonitor *monitor; GPtrArray *dir_monitors; GHashTable *folders; GCancellable *cancellable; GList *app_infos; guint queued_update; }; typedef struct { GList *app_infos; GHashTable *folders; } CacheState; G_DEFINE_TYPE (ShellAppCache, shell_app_cache, G_TYPE_OBJECT) enum { CHANGED, N_SIGNALS }; static guint signals [N_SIGNALS]; static void cache_state_free (CacheState *state) { g_clear_pointer (&state->folders, g_hash_table_unref); g_list_free_full (state->app_infos, g_object_unref); g_free (state); } static CacheState * cache_state_new (void) { CacheState *state; state = g_new0 (CacheState, 1); state->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); return g_steal_pointer (&state); } /** * shell_app_cache_get_default: * * Gets the default #ShellAppCache. * * Returns: (transfer none): a #ShellAppCache */ ShellAppCache * shell_app_cache_get_default (void) { static ShellAppCache *instance; if (instance == NULL) { instance = g_object_new (SHELL_TYPE_APP_CACHE, NULL); g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); } return instance; } static void load_folder (GHashTable *folders, const char *path) { g_autoptr(GDir) dir = NULL; const char *name; g_assert (folders != NULL); g_assert (path != NULL); dir = g_dir_open (path, 0, NULL); if (dir == NULL) return; while ((name = g_dir_read_name (dir))) { g_autofree gchar *filename = NULL; g_autoptr(GKeyFile) keyfile = NULL; /* First added wins */ if (g_hash_table_contains (folders, name)) continue; filename = g_build_filename (path, name, NULL); keyfile = g_key_file_new (); if (g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, NULL)) { gchar *translated; translated = g_key_file_get_locale_string (keyfile, "Desktop Entry", "Name", NULL, NULL); if (translated != NULL) g_hash_table_insert (folders, g_strdup (name), translated); } } } static void load_folders (GHashTable *folders) { const char * const *dirs; g_autofree gchar *userdir = NULL; guint i; g_assert (folders != NULL); userdir = g_build_filename (g_get_user_data_dir (), "desktop-directories", NULL); load_folder (folders, userdir); dirs = g_get_system_data_dirs (); for (i = 0; dirs[i] != NULL; i++) { g_autofree gchar *sysdir = g_build_filename (dirs[i], "desktop-directories", NULL); load_folder (folders, sysdir); } } static void shell_app_cache_worker (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { CacheState *state; g_assert (G_IS_TASK (task)); g_assert (SHELL_IS_APP_CACHE (source_object)); state = cache_state_new (); state->app_infos = g_app_info_get_all (); load_folders (state->folders); g_task_return_pointer (task, state, (GDestroyNotify) cache_state_free); } static void apply_update_cb (GObject *object, GAsyncResult *result, gpointer user_data) { ShellAppCache *cache = (ShellAppCache *)object; g_autoptr(GError) error = NULL; CacheState *state; g_assert (SHELL_IS_APP_CACHE (cache)); g_assert (G_IS_TASK (result)); g_assert (user_data == NULL); state = g_task_propagate_pointer (G_TASK (result), &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; g_list_free_full (cache->app_infos, g_object_unref); cache->app_infos = g_steal_pointer (&state->app_infos); g_clear_pointer (&cache->folders, g_hash_table_unref); cache->folders = g_steal_pointer (&state->folders); g_signal_emit (cache, signals[CHANGED], 0); cache_state_free (state); } static gboolean shell_app_cache_do_update (gpointer user_data) { ShellAppCache *cache = user_data; g_autoptr(GTask) task = NULL; cache->queued_update = 0; /* Reset the cancellable state so we don't race with * two updates coming back overlapped and applying the * information in the wrong order. */ g_cancellable_cancel (cache->cancellable); g_clear_object (&cache->cancellable); cache->cancellable = g_cancellable_new (); task = g_task_new (cache, cache->cancellable, apply_update_cb, NULL); g_task_set_source_tag (task, shell_app_cache_do_update); g_task_run_in_thread (task, shell_app_cache_worker); return G_SOURCE_REMOVE; } static void shell_app_cache_queue_update (ShellAppCache *self) { g_assert (SHELL_IS_APP_CACHE (self)); if (self->queued_update != 0) g_source_remove (self->queued_update); self->queued_update = g_timeout_add_seconds (DEFAULT_TIMEOUT_SECONDS, shell_app_cache_do_update, self); } static void monitor_desktop_directories_for_data_dir (ShellAppCache *self, const gchar *directory) { g_autofree gchar *subdir = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GFileMonitor) monitor = NULL; g_assert (SHELL_IS_APP_CACHE (self)); if (directory == NULL) return; subdir = g_build_filename (directory, "desktop-directories", NULL); file = g_file_new_for_path (subdir); monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL); if (monitor != NULL) { g_file_monitor_set_rate_limit (monitor, DEFAULT_TIMEOUT_SECONDS * 1000); g_signal_connect_object (monitor, "changed", G_CALLBACK (shell_app_cache_queue_update), self, G_CONNECT_SWAPPED); g_ptr_array_add (self->dir_monitors, g_steal_pointer (&monitor)); } } static void shell_app_cache_finalize (GObject *object) { ShellAppCache *self = (ShellAppCache *)object; g_clear_object (&self->monitor); if (self->queued_update) { g_source_remove (self->queued_update); self->queued_update = 0; } g_clear_pointer (&self->dir_monitors, g_ptr_array_unref); g_clear_pointer (&self->folders, g_hash_table_unref); g_list_free_full (self->app_infos, g_object_unref); G_OBJECT_CLASS (shell_app_cache_parent_class)->finalize (object); } static void shell_app_cache_class_init (ShellAppCacheClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = shell_app_cache_finalize; /** * ShellAppCache::changed: * * The "changed" signal is emitted when the cache has updated * information about installed applications. */ signals [CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } static void shell_app_cache_init (ShellAppCache *self) { const gchar * const *sysdirs; guint i; /* Monitor directories for translation changes */ self->dir_monitors = g_ptr_array_new_with_free_func (g_object_unref); monitor_desktop_directories_for_data_dir (self, g_get_user_data_dir ()); sysdirs = g_get_system_data_dirs (); for (i = 0; sysdirs[i] != NULL; i++) monitor_desktop_directories_for_data_dir (self, sysdirs[i]); /* Load translated directory names immediately */ self->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); load_folders (self->folders); /* Setup AppMonitor to track changes */ self->monitor = g_app_info_monitor_get (); g_signal_connect_object (self->monitor, "changed", G_CALLBACK (shell_app_cache_queue_update), self, G_CONNECT_SWAPPED); self->app_infos = g_app_info_get_all (); } /** * shell_app_cache_get_all: * @cache: (nullable): a #ShellAppCache or %NULL * * Like g_app_info_get_all() but always returns a * cached set of application info so the caller can be * sure that I/O will not happen on the current thread. * * Returns: (transfer none) (element-type GAppInfo): * a #GList of references to #GAppInfo. */ GList * shell_app_cache_get_all (ShellAppCache *cache) { g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); return cache->app_infos; } /** * shell_app_cache_get_info: * @cache: (nullable): a #ShellAppCache or %NULL * @id: the application id * * A replacement for g_desktop_app_info_new() that will lookup the * information from the cache instead of (re)loading from disk. * * Returns: (nullable) (transfer none): a #GDesktopAppInfo or %NULL */ GDesktopAppInfo * shell_app_cache_get_info (ShellAppCache *cache, const char *id) { const GList *iter; g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); for (iter = cache->app_infos; iter != NULL; iter = iter->next) { GAppInfo *info = iter->data; if (g_strcmp0 (id, g_app_info_get_id (info)) == 0) return G_DESKTOP_APP_INFO (info); } return NULL; } /** * shell_app_cache_translate_folder: * @cache: (nullable): a #ShellAppCache or %NULL * @name: the folder name * * Gets the translated folder name for @name if any exists. * * Returns: (nullable): the translated string or %NULL if there is no * translation. */ char * shell_app_cache_translate_folder (ShellAppCache *cache, const char *name) { g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); if (name == NULL) return NULL; return g_strdup (g_hash_table_lookup (cache->folders, name)); }