/* * menu-cache.c * * Copyright 2008 PCMan * Copyright 2009 Jürgen Hötzel * Copyright 2012-2017 Andriy Grytsenko (LStranger) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include #endif #include "version.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "menu-cache.h" #ifdef G_ENABLE_DEBUG #define DEBUG(...) g_debug(__VA_ARGS__) #else #define DEBUG(...) #endif #if GLIB_CHECK_VERSION(2, 32, 0) static GRecMutex _cache_lock; # define MENU_CACHE_LOCK g_rec_mutex_lock(&_cache_lock) # define MENU_CACHE_UNLOCK g_rec_mutex_unlock(&_cache_lock) /* for sync lookup */ static GMutex sync_run_mutex; static GCond sync_run_cond; #define SET_CACHE_READY(_cache_) do { \ g_mutex_lock(&sync_run_mutex); \ _cache_->ready = TRUE; \ g_cond_broadcast(&sync_run_cond); \ g_mutex_unlock(&sync_run_mutex); } while(0) #else /* before 2.32 GLib had another entity for statically allocated mutexes */ static GStaticRecMutex _cache_lock = G_STATIC_REC_MUTEX_INIT; # define MENU_CACHE_LOCK g_static_rec_mutex_lock(&_cache_lock) # define MENU_CACHE_UNLOCK g_static_rec_mutex_unlock(&_cache_lock) /* for sync lookup */ static GMutex *sync_run_mutex = NULL; static GCond *sync_run_cond = NULL; #define SET_CACHE_READY(_cache_) do { \ g_mutex_lock(sync_run_mutex); \ _cache_->ready = TRUE; \ if(sync_run_cond) g_cond_broadcast(sync_run_cond); \ g_mutex_unlock(sync_run_mutex); } while(0) #endif typedef struct { char *dir; gint n_ref; } MenuCacheFileDir; struct _MenuCacheItem { guint n_ref; MenuCacheType type; char* id; char* name; char* comment; char* icon; MenuCacheFileDir* file_dir; char* file_name; MenuCacheDir* parent; }; struct _MenuCacheDir { MenuCacheItem item; GSList* children; guint32 flags; }; struct _MenuCacheApp { MenuCacheItem item; char* generic_name; char* exec; char* working_dir; guint32 show_in_flags; guint32 flags; char* try_exec; const char **categories; char* keywords; }; struct _MenuCache { guint n_ref; MenuCacheDir* root_dir; char* menu_name; char* reg; /* includes md5 sum */ char* md5; /* link inside of reg */ char* cache_file; char** known_des; GSList* notifiers; GThread* thr; GCancellable* cancellable; guint version; guint reload_id; gboolean ready : 1; /* used for sync access */ }; static int server_fd = -1; G_LOCK_DEFINE(connect); /* for server_fd */ static GHashTable* hash = NULL; /* Don't call this API directly. Use menu_cache_lookup instead. */ static MenuCache* menu_cache_new( const char* cache_file ); static gboolean connect_server(GCancellable* cancellable); static gboolean register_menu_to_server(MenuCache* cache); static void unregister_menu_from_server( MenuCache* cache ); /* keep them for backward compatibility */ #ifdef G_DISABLE_DEPRECATED MenuCacheDir* menu_cache_get_root_dir( MenuCache* cache ); MenuCacheDir* menu_cache_item_get_parent( MenuCacheItem* item ); MenuCacheDir* menu_cache_get_dir_from_path( MenuCache* cache, const char* path ); GSList* menu_cache_dir_get_children( MenuCacheDir* dir ); #endif void menu_cache_init(int flags) { #if !GLIB_CHECK_VERSION(2, 36, 0) g_type_init(); #endif } static MenuCacheItem* read_item(GDataInputStream* f, MenuCache* cache, MenuCacheFileDir** all_used_files, int n_all_used_files); /* functions read_dir(), read_app(), and read_item() should be called for items that aren't accessible yet, therefore no lock is required */ static void read_dir(GDataInputStream* f, MenuCacheDir* dir, MenuCache* cache, MenuCacheFileDir** all_used_files, int n_all_used_files) { MenuCacheItem* item; char *line; gsize len; /* nodisplay flag */ if (cache->version >= 2) { line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if (G_UNLIKELY(line == NULL)) return; dir->flags = (guint32)atoi(line); g_free(line); } /* load child items in the dir */ while( (item = read_item( f, cache, all_used_files, n_all_used_files )) ) { /* menu_cache_ref shouldn't be called here for dir. * Otherwise, circular reference will happen. */ item->parent = dir; dir->children = g_slist_prepend( dir->children, item ); } dir->children = g_slist_reverse( dir->children ); /* set flag by children if working with old cache generator */ if (cache->version == 1) { if (dir->children == NULL) dir->flags = FLAG_IS_NODISPLAY; else if ((line = menu_cache_item_get_file_path(MENU_CACHE_ITEM(dir))) != NULL) { GKeyFile *kf = g_key_file_new(); if (g_key_file_load_from_file(kf, line, G_KEY_FILE_NONE, NULL) && g_key_file_get_boolean(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, NULL)) dir->flags = FLAG_IS_NODISPLAY; g_key_file_free(kf); g_free(line); } } } static char *_unescape_lf(char *str) { char *c, *p = str; gsize len = 0; while ((c = strchr(p, '\\')) != NULL) { if (p != &str[len]) memmove(&str[len], p, c - p); len += (c - p); if (c[1] == 'n') { str[len++] = '\n'; c++; } else if (c != &str[len]) str[len++] = *c; p = &c[1]; } if (p != &str[len]) memmove(&str[len], p, strlen(p) + 1); return str; } static void read_app(GDataInputStream* f, MenuCacheApp* app, MenuCache* cache) { char *line; gsize len; GString *str; /* generic name */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return; if(G_LIKELY(len > 0)) app->generic_name = _unescape_lf(line); else g_free(line); /* exec */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return; if(G_LIKELY(len > 0)) app->exec = _unescape_lf(line); else g_free(line); /* terminal / startup notify */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return; app->flags = (guint32)atoi(line); g_free(line); /* ShowIn flags */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return; app->show_in_flags = (guint32)atol(line); g_free(line); if (cache->version < 2) return; /* TryExec */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if (G_UNLIKELY(line == NULL)) return; if (G_LIKELY(len > 0)) app->try_exec = g_strchomp(line); else g_free(line); /* Path */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if (G_UNLIKELY(line == NULL)) return; if (G_LIKELY(len > 0)) app->working_dir = line; else g_free(line); /* Categories */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if (G_UNLIKELY(line == NULL)) return; if (G_LIKELY(len > 0)) { const char **x; /* split and intern all the strings so categories can be processed later for search doing g_quark_try_string()+g_quark_to_string() */ app->categories = x = (const char **)g_strsplit(line, ";", 0); while (*x != NULL) { char *cat = (char *)*x; *x = g_intern_string(cat); g_free(cat); x++; } } g_free(line); /* Keywords */ str = g_string_new(MENU_CACHE_ITEM(app)->name); if (G_LIKELY(app->exec != NULL)) { char *sp = strchr(app->exec, ' '); char *bn = strrchr(app->exec, G_DIR_SEPARATOR); g_string_append_c(str, ','); if (bn == NULL && sp == NULL) g_string_append(str, app->exec); else if (bn == NULL || (sp != NULL && sp < bn)) g_string_append_len(str, app->exec, sp - app->exec); else if (sp == NULL) g_string_append(str, &bn[1]); else g_string_append_len(str, &bn[1], sp - &bn[1]); } if (app->generic_name != NULL) { g_string_append_c(str, ','); g_string_append(str, app->generic_name); } line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if (G_UNLIKELY(line == NULL)) return; if (len > 0) { g_string_append_c(str, ','); g_string_append(str, line); } app->keywords = g_utf8_casefold(str->str, str->len); g_string_free(str, TRUE); g_free(line); } static MenuCacheItem* read_item(GDataInputStream* f, MenuCache* cache, MenuCacheFileDir** all_used_files, int n_all_used_files) { MenuCacheItem* item; char *line; gsize len; gint idx; /* desktop/menu id */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return NULL; if( G_LIKELY(len >= 1) ) { if( line[0] == '+' ) /* menu dir */ { item = (MenuCacheItem*)g_slice_new0( MenuCacheDir ); item->n_ref = 1; item->type = MENU_CACHE_TYPE_DIR; } else if( line[0] == '-' ) /* menu item */ { item = (MenuCacheItem*)g_slice_new0( MenuCacheApp ); item->n_ref = 1; if( G_LIKELY( len > 1 ) ) /* application item */ item->type = MENU_CACHE_TYPE_APP; else /* separator */ { item->type = MENU_CACHE_TYPE_SEP; return item; } } else return NULL; item->id = g_strndup( line + 1, len - 1 ); g_free(line); } else { g_free(line); return NULL; } /* name */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) goto _fail; if(G_LIKELY(len > 0)) item->name = _unescape_lf(line); else g_free(line); /* comment */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) goto _fail; if(G_LIKELY(len > 0)) item->comment = _unescape_lf(line); else g_free(line); /* icon */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) goto _fail; if(G_LIKELY(len > 0)) item->icon = line; else g_free(line); /* file dir/basename */ /* file name */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) goto _fail; if(G_LIKELY(len > 0)) item->file_name = line; else if( item->type == MENU_CACHE_TYPE_APP ) { /* When file name is the same as desktop_id, which is * quite common in desktop files, we use this trick to * save memory usage. */ item->file_name = item->id; g_free(line); } else g_free(line); /* desktop file dir */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) { _fail: g_free(item->id); g_free(item->name); g_free(item->comment); g_free(item->icon); if(item->file_name && item->file_name != item->id) g_free(item->file_name); if(item->type == MENU_CACHE_TYPE_DIR) g_slice_free(MenuCacheDir, MENU_CACHE_DIR(item)); else g_slice_free(MenuCacheApp, MENU_CACHE_APP(item)); return NULL; } idx = atoi( line ); g_free(line); if( G_LIKELY( idx >=0 && idx < n_all_used_files ) ) { item->file_dir = all_used_files[ idx ]; g_atomic_int_inc(&item->file_dir->n_ref); } if( item->type == MENU_CACHE_TYPE_DIR ) read_dir( f, MENU_CACHE_DIR(item), cache, all_used_files, n_all_used_files ); else if( item->type == MENU_CACHE_TYPE_APP ) read_app( f, MENU_CACHE_APP(item), cache ); return item; } static void menu_cache_file_dir_unref(MenuCacheFileDir *file_dir) { if (file_dir && g_atomic_int_dec_and_test(&file_dir->n_ref)) { g_free(file_dir->dir); g_free(file_dir); } } static gint read_all_used_files(GDataInputStream* f, MenuCache* cache, MenuCacheFileDir*** all_used_files) { char *line; gsize len; int i, n; MenuCacheFileDir** dirs; line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return -1; n = atoi( line ); g_free(line); if (G_UNLIKELY(n <= 0)) return n; dirs = g_new0( MenuCacheFileDir *, n ); for( i = 0; i < n; ++i ) { line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) { while (i-- > 0) menu_cache_file_dir_unref(dirs[i]); g_free(dirs); return -1; } dirs[i] = g_new(MenuCacheFileDir, 1); dirs[i]->n_ref = 1; dirs[i]->dir = line; /* don't include \n */ } *all_used_files = dirs; return n; } static gboolean read_all_known_des(GDataInputStream* f, MenuCache* cache) { char *line; gsize len; line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) return FALSE; cache->known_des = g_strsplit_set( line, ";\n", 0 ); g_free(line); return TRUE; } static MenuCache* menu_cache_new( const char* cache_file ) { MenuCache* cache; cache = g_slice_new0( MenuCache ); cache->cache_file = g_strdup( cache_file ); cache->n_ref = 1; return cache; } /** * menu_cache_ref * @cache: a menu cache descriptor * * Increases reference counter on @cache. * * Returns: @cache. * * Since: 0.1.0 */ MenuCache* menu_cache_ref(MenuCache* cache) { g_atomic_int_inc( &cache->n_ref ); return cache; } /** * menu_cache_unref * @cache: a menu cache descriptor * * Descreases reference counter on @cache. When reference count becomes 0 * then resources associated with @cache will be freed. * * Since: 0.1.0 */ void menu_cache_unref(MenuCache* cache) { /* DEBUG("cache_unref: %d", cache->n_ref); */ /* we need a lock here unfortunately because item in hash isn't protected by reference therefore another thread may get access to it right now */ MENU_CACHE_LOCK; if( g_atomic_int_dec_and_test(&cache->n_ref) ) { /* g_assert(cache->reload_id != 0); */ unregister_menu_from_server( cache ); /* DEBUG("unregister to server"); */ g_hash_table_remove( hash, cache->menu_name ); if( g_hash_table_size(hash) == 0 ) { /* DEBUG("destroy hash"); */ g_hash_table_destroy(hash); /* DEBUG("disconnect from server"); */ G_LOCK(connect); shutdown(server_fd, SHUT_RDWR); /* the IO thread will terminate itself */ server_fd = -1; G_UNLOCK(connect); hash = NULL; } MENU_CACHE_UNLOCK; if(G_LIKELY(cache->thr)) { g_cancellable_cancel(cache->cancellable); g_thread_join(cache->thr); } g_object_unref(cache->cancellable); if( G_LIKELY(cache->root_dir) ) { /* DEBUG("unref root dir"); */ menu_cache_item_unref( MENU_CACHE_ITEM(cache->root_dir) ); /* DEBUG("unref root dir finished"); */ } g_free( cache->cache_file ); g_free( cache->menu_name ); g_free(cache->reg); /* g_free( cache->menu_file_path ); */ g_strfreev(cache->known_des); g_slist_free(cache->notifiers); g_slice_free( MenuCache, cache ); } else MENU_CACHE_UNLOCK; } /** * menu_cache_get_root_dir * @cache: a menu cache instance * * Since: 0.1.0 * * Deprecated: 0.3.4: Use menu_cache_dup_root_dir() instead. */ MenuCacheDir* menu_cache_get_root_dir( MenuCache* cache ) { MenuCacheDir* dir = menu_cache_dup_root_dir(cache); /* NOTE: this is very ugly hack but cache->root_dir may be changed by cache reload in server-io thread, so we should keep it alive :( */ if(dir) g_timeout_add_seconds(10, (GSourceFunc)menu_cache_item_unref, dir); return dir; } /** * menu_cache_dup_root_dir * @cache: a menu cache instance * * Retrieves root directory for @cache. Returned data should be freed * with menu_cache_item_unref() after usage. * * Returns: (transfer full): root item or %NULL in case of error. * * Since: 0.3.4 */ MenuCacheDir* menu_cache_dup_root_dir( MenuCache* cache ) { MenuCacheDir* dir; MENU_CACHE_LOCK; dir = cache->root_dir; if(G_LIKELY(dir)) menu_cache_item_ref(MENU_CACHE_ITEM(dir)); MENU_CACHE_UNLOCK; return dir; } /** * menu_cache_item_ref * @item: a menu cache item * * Increases reference counter on @item. * * Returns: @item. * * Since: 0.1.0 */ MenuCacheItem* menu_cache_item_ref(MenuCacheItem* item) { g_atomic_int_inc( &item->n_ref ); /* DEBUG("item_ref %s: %d -> %d", item->id, item->n_ref-1, item->n_ref); */ return item; } static gboolean menu_cache_reload_idle(gpointer cache) { /* do reload once */ if (!g_source_is_destroyed(g_main_current_source())) menu_cache_reload(cache); return FALSE; } typedef struct _CacheReloadNotifier { MenuCacheReloadNotify func; gpointer user_data; }CacheReloadNotifier; struct _MenuCacheNotifyId { GSList l; }; /** * menu_cache_add_reload_notify * @cache: a menu cache instance * @func: callback to call when menu cache is reloaded * @user_data: user data provided for @func * * Adds a @func to list of callbacks that are called each time menu cache * is loaded. * * Returns: an ID of added callback. * * Since: 0.1.0 */ MenuCacheNotifyId menu_cache_add_reload_notify(MenuCache* cache, MenuCacheReloadNotify func, gpointer user_data) { GSList* l = g_slist_alloc(); CacheReloadNotifier* n = g_slice_new(CacheReloadNotifier); gboolean is_first; n->func = func; n->user_data = user_data; l->data = n; MENU_CACHE_LOCK; is_first = (cache->root_dir == NULL && cache->notifiers == NULL); cache->notifiers = g_slist_concat( cache->notifiers, l ); /* reload existing file first so it will be ready right away */ if(is_first && cache->reload_id == 0) cache->reload_id = g_idle_add_full(G_PRIORITY_HIGH_IDLE, menu_cache_reload_idle, menu_cache_ref(cache), (GDestroyNotify)menu_cache_unref); MENU_CACHE_UNLOCK; return (MenuCacheNotifyId)l; } /** * menu_cache_remove_reload_notify * @cache: a menu cache instance * @notify_id: an ID of callback * * Removes @notify_id from list of callbacks added for @cache by previous * call to menu_cache_add_reload_notify(). * * Since: 0.1.0 */ void menu_cache_remove_reload_notify(MenuCache* cache, MenuCacheNotifyId notify_id) { MENU_CACHE_LOCK; g_slice_free( CacheReloadNotifier, ((GSList*)notify_id)->data ); cache->notifiers = g_slist_delete_link( cache->notifiers, (GSList*)notify_id ); MENU_CACHE_UNLOCK; } static gboolean reload_notify(gpointer data) { MenuCache* cache = (MenuCache*)data; GSList* l; MENU_CACHE_LOCK; /* we have it referenced and there is no source removal so no check */ for( l = cache->notifiers; l; l = l->next ) { CacheReloadNotifier* n = (CacheReloadNotifier*)l->data; if(n->func) n->func( cache, n->user_data ); } MENU_CACHE_UNLOCK; return FALSE; } /** * menu_cache_reload * @cache: a menu cache instance * * Reloads menu cache from file generated by menu-cached. * * Returns: %TRUE if reload was successful. * * Since: 0.1.0 */ gboolean menu_cache_reload( MenuCache* cache ) { char* line; gsize len; GFile* file; GFileInputStream* istr = NULL; GDataInputStream* f; MenuCacheFileDir** all_used_files; int i, n; int ver_maj, ver_min; MENU_CACHE_LOCK; if (cache->reload_id) g_source_remove(cache->reload_id); cache->reload_id = 0; MENU_CACHE_UNLOCK; file = g_file_new_for_path(cache->cache_file); if(!file) return FALSE; istr = g_file_read(file, cache->cancellable, NULL); g_object_unref(file); if(!istr) return FALSE; f = g_data_input_stream_new(G_INPUT_STREAM(istr)); g_object_unref(istr); if( ! f ) return FALSE; /* the first line is version number */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_LIKELY(line)) { len = sscanf(line, "%d.%d", &ver_maj, &ver_min); g_free(line); if(len < 2) goto _fail; if( ver_maj != VER_MAJOR || ver_min > VER_MINOR || ver_min < VER_MINOR_SUPPORTED ) goto _fail; } else goto _fail; g_debug("menu cache: got file version 1.%d", ver_min); /* the second line is menu name */ line = g_data_input_stream_read_line(f, &len, cache->cancellable, NULL); if(G_UNLIKELY(line == NULL)) goto _fail; g_free(line); /* FIXME: this may lock other threads for some time */ MENU_CACHE_LOCK; if(cache->notifiers == NULL) { /* nobody aware of reloads, stupid clients may think root is forever */ MENU_CACHE_UNLOCK; goto _fail; } /* get all used files */ n = read_all_used_files( f, cache, &all_used_files ); if (n <= 0) { MENU_CACHE_UNLOCK; goto _fail; } /* read known DEs */ g_strfreev( cache->known_des ); if( ! read_all_known_des( f, cache ) ) { cache->known_des = NULL; MENU_CACHE_UNLOCK; for (i = 0; i < n; i++) menu_cache_file_dir_unref(all_used_files[i]); g_free(all_used_files); _fail: g_object_unref(f); return FALSE; } cache->version = ver_min; if(cache->root_dir) menu_cache_item_unref( MENU_CACHE_ITEM(cache->root_dir) ); cache->root_dir = (MenuCacheDir*)read_item( f, cache, all_used_files, n ); g_object_unref(f); g_idle_add_full(G_PRIORITY_HIGH_IDLE, reload_notify, menu_cache_ref(cache), (GDestroyNotify)menu_cache_unref); MENU_CACHE_UNLOCK; for (i = 0; i < n; i++) menu_cache_file_dir_unref(all_used_files[i]); g_free(all_used_files); return TRUE; } /** * menu_cache_item_unref * @item: a menu cache item * * Decreases reference counter on @item. When reference count becomes 0 * then resources associated with @item will be freed. * * Returns: %FALSE (since 0.5.0) * * Since: 0.1.0 */ gboolean menu_cache_item_unref(MenuCacheItem* item) { /* DEBUG("item_unref(%s): %d", item->id, item->n_ref); */ /* We need a lock here unfortunately since another thread may have access to it via some child->parent which isn't protected by reference */ MENU_CACHE_LOCK; /* lock may be recursive here */ if( g_atomic_int_dec_and_test( &item->n_ref ) ) { /* DEBUG("free item: %s", item->id); */ g_free( item->id ); g_free( item->name ); g_free( item->comment ); g_free( item->icon ); menu_cache_file_dir_unref(item->file_dir); if( item->file_name && item->file_name != item->id ) g_free( item->file_name ); if( item->parent ) { /* DEBUG("remove %s from parent %s", item->id, MENU_CACHE_ITEM(item->parent)->id); */ /* remove ourselve from the parent node. */ item->parent->children = g_slist_remove(item->parent->children, item); } if( item->type == MENU_CACHE_TYPE_DIR ) { MenuCacheDir* dir = MENU_CACHE_DIR(item); GSList* l; for(l = dir->children; l; ) { MenuCacheItem* child = MENU_CACHE_ITEM(l->data); /* remove ourselve from the children. */ child->parent = NULL; l = l->next; menu_cache_item_unref(child); } g_slist_free( dir->children ); g_slice_free( MenuCacheDir, dir ); } else { MenuCacheApp* app = MENU_CACHE_APP(item); g_free( app->exec ); g_free(app->try_exec); g_free(app->working_dir); g_free(app->categories); g_free(app->keywords); g_slice_free( MenuCacheApp, app ); } } MENU_CACHE_UNLOCK; return FALSE; } /** * menu_cache_item_get_type * @item: a menu cache item * * Checks type of @item. * * Returns: type of @item. * * Since: 0.1.0 */ MenuCacheType menu_cache_item_get_type( MenuCacheItem* item ) { return item->type; } /** * menu_cache_item_get_id * @item: a menu cache item * * Retrieves ID (short name such as 'application.desktop') of @item. * Returned data are owned by menu cache and should be not freed by caller. * * Returns: (transfer none): item ID. * * Since: 0.1.0 */ const char* menu_cache_item_get_id( MenuCacheItem* item ) { return item->id; } /** * menu_cache_item_get_name * @item: a menu cache item * * Retrieves display name of @item. Returned data are owned by menu * cache and should be not freed by caller. * * Returns: (transfer none): @item display name or %NULL. * * Since: 0.1.0 */ const char* menu_cache_item_get_name( MenuCacheItem* item ) { return item->name; } /** * menu_cache_item_get_comment * @item: a menu cache item * * Retrieves comment of @item. The comment can be used to show tooltip * on @item. Returned data are owned by menu cache and should be not * freed by caller. * * Returns: (transfer none): @item comment or %NULL. * * Since: 0.1.0 */ const char* menu_cache_item_get_comment( MenuCacheItem* item ) { return item->comment; } /** * menu_cache_item_get_icon * @item: a menu cache item * * Retrieves name of icon of @item. Returned data are owned by menu * cache and should be not freed by caller. * * Returns: (transfer none): @item icon name or %NULL. * * Since: 0.1.0 */ const char* menu_cache_item_get_icon( MenuCacheItem* item ) { return item->icon; } /** * menu_cache_item_get_file_basename * @item: a menu cache item * * Retrieves basename of @item. This API can return %NULL if @item is a * directory and have no directory desktop entry file. Returned data are * owned by menu cache and should be not freed by caller. * * Returns: (transfer none): @item file basename or %NULL. * * Since: 0.2.0 */ const char* menu_cache_item_get_file_basename( MenuCacheItem* item ) { return item->file_name; } /** * menu_cache_item_get_file_dirname * @item: a menu cache item * * Retrieves path to directory where @item desktop enrty file is located. * This API can return %NULL if @item is a directory and have no * desktop entry file. Returned data are owned by menu cache and should * be not freed by caller. * * Returns: (transfer none): @item file parent directory path or %NULL. * * Since: 0.2.0 */ const char* menu_cache_item_get_file_dirname( MenuCacheItem* item ) { return item->file_dir ? item->file_dir->dir + 1 : NULL; } /** * menu_cache_item_get_file_path * @item: a menu cache item * * Retrieves path to @item desktop enrty file. This API can return %NULL * if @item is a directory and have no desktop entry file. Returned data * should be freed with g_free() after usage. * * Returns: (transfer full): @item file path or %NULL. * * Since: 0.2.0 */ char* menu_cache_item_get_file_path( MenuCacheItem* item ) { if( ! item->file_name || ! item->file_dir ) return NULL; return g_build_filename( item->file_dir->dir + 1, item->file_name, NULL ); } /** * menu_cache_item_get_parent * @item: a menu cache item * * Since: 0.1.0 * * Deprecated: 0.3.4: Use menu_cache_item_dup_parent() instead. */ MenuCacheDir* menu_cache_item_get_parent( MenuCacheItem* item ) { MenuCacheDir* dir = menu_cache_item_dup_parent(item); /* NOTE: this is very ugly hack but parent may be changed by item freeing so we should keep it alive :( */ if(dir) g_timeout_add_seconds(10, (GSourceFunc)menu_cache_item_unref, dir); return dir; } /** * menu_cache_item_dup_parent * @item: a menu item * * Retrieves parent (directory) for @item. Returned data should be freed * with menu_cache_item_unref() after usage. * * Returns: (transfer full): parent item or %NULL in case of error. * * Since: 0.3.4 */ MenuCacheDir* menu_cache_item_dup_parent( MenuCacheItem* item ) { MenuCacheDir* dir; MENU_CACHE_LOCK; dir = item->parent; if(G_LIKELY(dir)) menu_cache_item_ref(MENU_CACHE_ITEM(dir)); MENU_CACHE_UNLOCK; return dir; } /** * menu_cache_dir_get_children * @dir: a menu cache item * * Retrieves list of items contained in @dir. Returned data are owned by * menu cache and should be not freed by caller. * This API is thread unsafe and should be never called from outside of * default main loop. * * Returns: (transfer none) (element-type MenuCacheItem): list of items. * * Since: 0.1.0 * * Deprecated: 0.4.0: Use menu_cache_dir_list_children() instead. */ GSList* menu_cache_dir_get_children( MenuCacheDir* dir ) { /* NOTE: this is very ugly hack but dir may be freed by cache reload in server-io thread, so we should keep it alive :( */ g_timeout_add_seconds(10, (GSourceFunc)menu_cache_item_unref, menu_cache_item_ref(MENU_CACHE_ITEM(dir))); return dir->children; } /** * menu_cache_dir_list_children * @dir: a menu cache item * * Retrieves list of items contained in @dir. Returned data should be * freed with g_slist_free_full(list, menu_cache_item_unref) after usage. * * Returns: (transfer full) (element-type MenuCacheItem): list of items. * * Since: 0.4.0 */ GSList* menu_cache_dir_list_children(MenuCacheDir* dir) { GSList *children, *l; if(MENU_CACHE_ITEM(dir)->type != MENU_CACHE_TYPE_DIR) return NULL; MENU_CACHE_LOCK; children = g_slist_copy(dir->children); for(l = children; l; l = l->next) menu_cache_item_ref(l->data); MENU_CACHE_UNLOCK; return children; } /** * menu_cache_find_child_by_id * @dir: a menu cache item * @id: a string to find * * Checks if @dir has a child with given @id. Returned data should be * freed with menu_cache_item_unref() when no longer needed. * * Returns: (transfer full): found item or %NULL. * * Since: 0.5.0 */ MenuCacheItem *menu_cache_find_child_by_id(MenuCacheDir *dir, const char *id) { GSList *child; MenuCacheItem *item = NULL; if (MENU_CACHE_ITEM(dir)->type != MENU_CACHE_TYPE_DIR || id == NULL) return NULL; MENU_CACHE_LOCK; for (child = dir->children; child; child = child->next) if (g_strcmp0(MENU_CACHE_ITEM(child->data)->id, id) == 0) { item = menu_cache_item_ref(child->data); break; } MENU_CACHE_UNLOCK; return item; } /** * menu_cache_find_child_by_name * @dir: a menu cache item * @name: a string to find * * Checks if @dir has a child with given @name. Returned data should be * freed with menu_cache_item_unref() when no longer needed. * * Returns: (transfer full): found item or %NULL. * * Since: 0.5.0 */ MenuCacheItem *menu_cache_find_child_by_name(MenuCacheDir *dir, const char *name) { GSList *child; MenuCacheItem *item = NULL; if (MENU_CACHE_ITEM(dir)->type != MENU_CACHE_TYPE_DIR || name == NULL) return NULL; MENU_CACHE_LOCK; for (child = dir->children; child; child = child->next) if (g_strcmp0(MENU_CACHE_ITEM(child->data)->name, name) == 0) { item = menu_cache_item_ref(child->data); break; } MENU_CACHE_UNLOCK; return item; } /** * menu_cache_dir_is_visible * @dir: a menu cache item * * Checks if @dir should be visible. * * Returns: %TRUE if @dir is visible. * * Since: 0.5.0 */ gboolean menu_cache_dir_is_visible(MenuCacheDir *dir) { return ((dir->flags & FLAG_IS_NODISPLAY) == 0); } /** * menu_cache_app_get_generic_name * @app: a menu cache item * * Retrieves generic name for @app. Returned data are owned by menu * cache and should not be freed by caller. * * Returns: (transfer none): app's generic name or %NULL. * * Since: 1.0.3 */ const char* menu_cache_app_get_generic_name( MenuCacheApp* app ) { return app->generic_name; } /** * menu_cache_app_get_exec * @app: a menu cache item * * Retrieves execution string for @app. Returned data are owned by menu * cache and should be not freed by caller. * * Returns: (transfer none): item execution string or %NULL. * * Since: 0.1.0 */ const char* menu_cache_app_get_exec( MenuCacheApp* app ) { return app->exec; } /** * menu_cache_app_get_working_dir * @app: a menu cache item * * Retrieves working directory for @app. Returned data are owned by menu * cache and should be not freed by caller. * * Returns: (transfer none): item working directory or %NULL. * * Since: 0.1.0 */ const char* menu_cache_app_get_working_dir( MenuCacheApp* app ) { return app->working_dir; } /** * menu_cache_app_get_categories * @app: a menu cache item * * Retrieves list of categories for @app. Returned data are owned by menu * cache and should be not freed by caller. * * Returns: (transfer none): list of categories or %NULL. * * Since: 1.0.0 */ const char * const * menu_cache_app_get_categories(MenuCacheApp* app) { return app->categories; } /** * menu_cache_app_get_use_terminal * @app: a menu cache item * * Checks if @app should be ran in terminal. * * Returns: %TRUE if @app requires terminal to run. * * Since: 0.1.0 */ gboolean menu_cache_app_get_use_terminal( MenuCacheApp* app ) { return ( (app->flags & FLAG_USE_TERMINAL) != 0 ); } /** * menu_cache_app_get_use_sn * @app: a menu cache item * * Checks if @app wants startup notification. * * Returns: %TRUE if @app wants startup notification. * * Since: 0.1.0 */ gboolean menu_cache_app_get_use_sn( MenuCacheApp* app ) { return ( (app->flags & FLAG_USE_SN) != 0 ); } /** * menu_cache_app_get_show_flags * @app: a menu cache item * * Retrieves list of desktop environments where @app should be visible. * * Returns: bit mask of DE. * * Since: 0.2.0 */ guint32 menu_cache_app_get_show_flags( MenuCacheApp* app ) { return app->show_in_flags; } static gboolean _can_be_exec(MenuCacheApp *app) { char *path; if (app->try_exec == NULL) return TRUE; path = g_find_program_in_path(app->try_exec); g_free(path); return (path != NULL); } /** * menu_cache_app_get_is_visible * @app: a menu cache item * @de_flags: bit mask of DE to test * * Checks if @app should be visible in any of desktop environments * @de_flags. * * Returns: %TRUE if @app is visible. * * Since: 0.2.0 */ gboolean menu_cache_app_get_is_visible( MenuCacheApp* app, guint32 de_flags ) { if(app->flags & FLAG_IS_NODISPLAY) return FALSE; return (!app->show_in_flags || (app->show_in_flags & de_flags)) && _can_be_exec(app); } /* MenuCacheApp* menu_cache_find_app_by_exec( const char* exec ) { return NULL; } */ /** * menu_cache_get_dir_from_path * @cache: a menu cache instance * @path: item path * * Since: 0.1.0 * * Deprecated: 0.3.4: Use menu_cache_item_from_path() instead. */ MenuCacheDir* menu_cache_get_dir_from_path( MenuCache* cache, const char* path ) { char** names = g_strsplit( path + 1, "/", -1 ); int i = 0; MenuCacheDir* dir = NULL; if( !names ) return NULL; if( G_UNLIKELY(!names[0]) ) { g_strfreev(names); return NULL; } /* the topmost dir of the path should be the root menu dir. */ MENU_CACHE_LOCK; dir = cache->root_dir; if (G_UNLIKELY(dir == NULL) || strcmp(names[0], MENU_CACHE_ITEM(dir)->id)) { MENU_CACHE_UNLOCK; return NULL; } for( ++i; names[i]; ++i ) { GSList* l; for( l = dir->children; l; l = l->next ) { MenuCacheItem* item = MENU_CACHE_ITEM(l->data); if( item->type == MENU_CACHE_TYPE_DIR && 0 == strcmp( item->id, names[i] ) ) dir = MENU_CACHE_DIR(item); } /* FIXME: we really should ref it on return since other thread may destroy the parent at this time and returned data become invalid. Therefore this call isn't thread-safe! */ if( ! dir ) { MENU_CACHE_UNLOCK; return NULL; } } MENU_CACHE_UNLOCK; return dir; } /** * menu_cache_item_from_path * @cache: cache to inspect * @path: item path * * Searches item @path in the @cache. The @path consists of item IDs * separated by slash ('/'). Returned data should be freed with * menu_cache_item_unref() after usage. * * Returns: (transfer full): found item or %NULL if no item found. * * Since: 0.3.4 */ MenuCacheItem* menu_cache_item_from_path( MenuCache* cache, const char* path ) { char** names = g_strsplit( path + 1, "/", -1 ); int i; MenuCacheDir* dir; MenuCacheItem* item = NULL; if( !names ) return NULL; if( G_UNLIKELY(!names[0]) ) { g_strfreev(names); return NULL; } /* the topmost dir of the path should be the root menu dir. */ MENU_CACHE_LOCK; dir = cache->root_dir; if( G_UNLIKELY(!dir) || strcmp(names[0], MENU_CACHE_ITEM(dir)->id) != 0 ) goto _end; for( i = 1; names[i]; ++i ) { GSList* l; item = NULL; if( !dir ) break; l = dir->children; dir = NULL; for( ; l; l = l->next ) { item = MENU_CACHE_ITEM(l->data); if( g_strcmp0( item->id, names[i] ) == 0 ) { if( item->type == MENU_CACHE_TYPE_DIR ) dir = MENU_CACHE_DIR(item); break; } item = NULL; } if( !item ) break; } if(item) menu_cache_item_ref(item); _end: MENU_CACHE_UNLOCK; g_strfreev(names); return item; } /** * menu_cache_dir_make_path * @dir: a menu cache item * * Retrieves path of @dir. The path consists of item IDs separated by * slash ('/'). Returned data should be freed with g_free() after usage. * * Returns: (transfer full): item path. * * Since: 0.1.0 */ char* menu_cache_dir_make_path( MenuCacheDir* dir ) { GString* path = g_string_sized_new(1024); MenuCacheItem* it; MENU_CACHE_LOCK; while( (it = MENU_CACHE_ITEM(dir)) ) /* this is not top dir */ { g_string_prepend( path, menu_cache_item_get_id(it) ); g_string_prepend_c( path, '/' ); /* FIXME: if parent is already unref'd by another thread then path being made will be broken. Is there any way to avoid that? */ dir = it->parent; } MENU_CACHE_UNLOCK; return g_string_free( path, FALSE ); } static void get_socket_name( char* buf, int len ) { char* dpy = g_strdup(g_getenv("DISPLAY")); if(dpy && *dpy) { char* p = strchr(dpy, ':'); for(++p; *p && *p != '.' && *p != '\n';) ++p; if(*p) *p = '\0'; } #if GLIB_CHECK_VERSION(2, 28, 0) g_snprintf( buf, len, "%s/menu-cached-%s", g_get_user_runtime_dir(), dpy ? dpy : ":0" ); #else g_snprintf( buf, len, "%s/.menu-cached-%s-%s", g_get_tmp_dir(), dpy ? dpy : ":0", g_get_user_name() ); #endif g_free(dpy); } #define MAX_RETRIES 25 static gboolean fork_server(const char *path) { int ret, pid, status; if (!g_file_test (MENUCACHE_LIBEXECDIR "/menu-cached", G_FILE_TEST_IS_EXECUTABLE)) { g_error("failed to find menu-cached"); } /* Start daemon */ pid = fork(); if (pid == 0) { execl(MENUCACHE_LIBEXECDIR "/menu-cached", MENUCACHE_LIBEXECDIR "/menu-cached", path, NULL); g_print("failed to exec %s %s\n", MENUCACHE_LIBEXECDIR "/menu-cached", path); } /* * do a waitpid on the intermediate process to avoid zombies. */ retry_wait: ret = waitpid(pid, &status, 0); if (ret < 0) { if (errno == EINTR) goto retry_wait; } return TRUE; } /* this thread is started by connect_server() */ static gpointer server_io_thread(gpointer data) { char buf[1024]; /* protocol has a lot shorter strings */ ssize_t sz; size_t ptr = 0; int fd = GPOINTER_TO_INT(data); GHashTableIter it; char* menu_name; MenuCache* cache; while(fd >= 0) { sz = read(fd, &buf[ptr], sizeof(buf) - ptr); if(sz <= 0) /* socket error or EOF */ { MENU_CACHE_LOCK; ptr = hash ? g_hash_table_size(hash) : 0; MENU_CACHE_UNLOCK; if (ptr == 0) /* don't need it anymore */ break; G_LOCK(connect); if(fd != server_fd) /* someone replaced us?! go out immediately! */ { G_UNLOCK(connect); break; } server_fd = -1; G_UNLOCK(connect); DEBUG("connect failed, trying reconnect"); sleep(1); if( ! connect_server(NULL) ) { g_critical("fail to re-connect to the server."); MENU_CACHE_LOCK; if(hash) { g_hash_table_iter_init(&it, hash); while(g_hash_table_iter_next(&it, (gpointer*)&menu_name, (gpointer*)&cache)) SET_CACHE_READY(cache); } MENU_CACHE_UNLOCK; break; } DEBUG("successfully reconnected server, re-register menus."); /* re-register all menu caches */ MENU_CACHE_LOCK; if(hash) { g_hash_table_iter_init(&it, hash); while(g_hash_table_iter_next(&it, (gpointer*)&menu_name, (gpointer*)&cache)) register_menu_to_server(cache); /* FIXME: need we remove it from hash if failed? */ } MENU_CACHE_UNLOCK; break; /* next thread will do it */ } while(sz > 0) { while(sz > 0) { if(buf[ptr] == '\n') break; sz--; ptr++; } if(ptr == sizeof(buf)) /* EOB reached, seems we got garbage */ { g_warning("menu cache: got garbage from server, break connect"); shutdown(fd, SHUT_RDWR); /* drop connection */ break; /* we handle it above */ } else if(sz == 0) /* incomplete line, wait for data again */ break; /* we got a line, let check what we got */ buf[ptr] = '\0'; if(memcmp(buf, "REL:", 4) == 0) /* reload */ { DEBUG("server ask us to reload cache: %s", &buf[4]); MENU_CACHE_LOCK; if(hash) { g_hash_table_iter_init(&it, hash); while(g_hash_table_iter_next(&it, (gpointer*)&menu_name, (gpointer*)&cache)) { if(memcmp(cache->md5, &buf[4], 32) == 0) { DEBUG("RELOAD!"); menu_cache_reload(cache); SET_CACHE_READY(cache); break; } } } MENU_CACHE_UNLOCK; /* DEBUG("cache reloaded"); */ } else g_warning("menu cache: unrecognized input: %s", buf); /* go to next line */ sz--; if(sz > 0) memmove(buf, &buf[ptr+1], sz); ptr = 0; } } G_LOCK(connect); if (fd == server_fd) server_fd = -1; G_UNLOCK(connect); close(fd); /* DEBUG("server io thread terminated"); */ #if GLIB_CHECK_VERSION(2, 32, 0) g_thread_unref(g_thread_self()); #endif return NULL; } static gboolean connect_server(GCancellable* cancellable) { int fd, rc; struct sockaddr_un addr; int retries = 0; G_LOCK(connect); if(server_fd != -1 || (cancellable && g_cancellable_is_cancelled(cancellable))) { G_UNLOCK(connect); return TRUE; } retry: fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) { g_print("Failed to create socket\n"); G_UNLOCK(connect); return FALSE; } fcntl (fd, F_SETFD, FD_CLOEXEC); memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; get_socket_name( addr.sun_path, sizeof( addr.sun_path ) ); if( connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { rc = errno; close(fd); if(cancellable && g_cancellable_is_cancelled(cancellable)) { G_UNLOCK(connect); return TRUE; } if((rc == ECONNREFUSED || rc == ENOENT) && retries == 0) { DEBUG("no running server found, starting it"); fork_server(addr.sun_path); ++retries; goto retry; } if(retries < MAX_RETRIES) { usleep(50000); ++retries; goto retry; } g_print("Unable to connect\n"); G_UNLOCK(connect); return FALSE; } server_fd = fd; G_UNLOCK(connect); #if GLIB_CHECK_VERSION(2, 32, 0) g_thread_new("menu-cache-io", server_io_thread, GINT_TO_POINTER(fd)); #else g_thread_create(server_io_thread, GINT_TO_POINTER(fd), FALSE, NULL); #endif return TRUE; } #define CACHE_VERSION __num2str(VER_MAJOR) "." __num2str(VER_MINOR) #define __num2str(s) __def2str(s) #define __def2str(s) #s static inline char *_validate_env(const char *env) { char *res, *c; if (env) res = g_strdup(env); else res = g_strdup(""); for (c = res; *c; c++) if (*c == '\n' || *c == '\t') *c = ' '; return res; } static MenuCache* menu_cache_create(const char* menu_name) { MenuCache* cache; const gchar * const * langs = g_get_language_names(); const char* xdg_cfg_env = g_getenv("XDG_CONFIG_DIRS"); const char* xdg_prefix_env = g_getenv("XDG_MENU_PREFIX"); const char* xdg_data_env = g_getenv("XDG_DATA_DIRS"); const char* xdg_cfg_home_env = g_getenv("XDG_CONFIG_HOME"); const char* xdg_data_home_env = g_getenv("XDG_DATA_HOME"); const char* xdg_cache_home_env = g_getenv("XDG_CACHE_HOME"); char *xdg_cfg, *xdg_prefix, *xdg_data, *xdg_cfg_home, *xdg_data_home, *xdg_cache_home; char* buf; const char* md5; char* file_name; int len = 0; GChecksum *sum; char *langs_list; xdg_cfg = _validate_env(xdg_cfg_env); xdg_prefix = _validate_env(xdg_prefix_env); xdg_data = _validate_env(xdg_data_env); xdg_cfg_home = _validate_env(xdg_cfg_home_env); xdg_data_home = _validate_env(xdg_data_home_env); xdg_cache_home = _validate_env(xdg_cache_home_env); /* reconstruct languages list in form as it should be in $LANGUAGES */ langs_list = g_strjoinv(":", (char **)langs); for (buf = langs_list; *buf; buf++) /* reusing buf var as char pointer */ if (*buf == '\n' || *buf == '\t') *buf = ' '; buf = g_strdup_printf( "REG:%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t" CACHE_VERSION "\t00000000000000000000000000000000\n", menu_name, langs_list, xdg_cache_home, xdg_cfg, xdg_prefix, xdg_data, xdg_cfg_home, xdg_data_home ); /* calculate the md5 sum of menu name + lang + all environment variables */ sum = g_checksum_new(G_CHECKSUM_MD5); len = strlen(buf); g_checksum_update(sum, (guchar*)buf + 4, len - 38); md5 = g_checksum_get_string(sum); file_name = g_build_filename( g_get_user_cache_dir(), "menus", md5, NULL ); DEBUG("cache file_name = %s", file_name); cache = menu_cache_new( file_name ); cache->reg = buf; cache->md5 = buf + len - 33; memcpy( cache->md5, md5, 32 ); cache->menu_name = g_strdup(menu_name); g_free( file_name ); g_free(langs_list); g_free(xdg_cfg); g_free(xdg_prefix); g_free(xdg_data); g_free(xdg_cfg_home); g_free(xdg_data_home); g_free(xdg_cache_home); g_checksum_free(sum); /* md5 is also freed here */ MENU_CACHE_LOCK; g_hash_table_insert( hash, g_strdup(menu_name), cache ); MENU_CACHE_UNLOCK; return cache; } static gboolean register_menu_to_server(MenuCache* cache) { ssize_t len = strlen(cache->reg); /* FIXME: do unblocking I/O */ if(write(server_fd, cache->reg, len) < len) { DEBUG("register_menu_to_server: sending failed"); return FALSE; /* socket write failed */ } return TRUE; } static void unregister_menu_from_server( MenuCache* cache ) { char buf[38]; g_snprintf( buf, 38, "UNR:%s\n", cache->md5 ); /* FIXME: do unblocking I/O */ if(write( server_fd, buf, 37 ) <= 0) { DEBUG("unregister_menu_from_server: sending failed"); } } static gpointer menu_cache_loader_thread(gpointer data) { MenuCache* cache = (MenuCache*)data; /* try to connect server now */ if(!connect_server(cache->cancellable)) { g_print("unable to connect to menu-cached.\n"); SET_CACHE_READY(cache); return NULL; } /* and request update from server */ if ((cache->cancellable && g_cancellable_is_cancelled(cache->cancellable)) || !register_menu_to_server(cache)) SET_CACHE_READY(cache); return NULL; } /** * menu_cache_lookup * @menu_name: a menu name * * Searches for connection to menu-cached for @menu_name. If there is no * such connection exist then creates new one. Caller can be notified * when cache is (re)loaded by adding callback. Caller should check if * the cache is already loaded trying to retrieve its root. * * See also: menu_cache_add_reload_notify(), menu_cache_item_dup_parent(). * * Returns: (transfer full): menu cache descriptor. * * Since: 0.1.0 */ MenuCache* menu_cache_lookup( const char* menu_name ) { MenuCache* cache; /* lookup in a hash table for already loaded menus */ MENU_CACHE_LOCK; #if !GLIB_CHECK_VERSION(2, 32, 0) /* FIXME: destroy them on application exit? */ if(!sync_run_mutex) sync_run_mutex = g_mutex_new(); if(!sync_run_cond) sync_run_cond = g_cond_new(); #endif if( G_UNLIKELY( ! hash ) ) hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL ); else { cache = (MenuCache*)g_hash_table_lookup(hash, menu_name); if( cache ) { menu_cache_ref(cache); MENU_CACHE_UNLOCK; return cache; } } MENU_CACHE_UNLOCK; cache = menu_cache_create(menu_name); cache->cancellable = g_cancellable_new(); #if GLIB_CHECK_VERSION(2, 32, 0) cache->thr = g_thread_new(menu_name, menu_cache_loader_thread, cache); #else cache->thr = g_thread_create(menu_cache_loader_thread, cache, TRUE, NULL); #endif return cache; } /** * menu_cache_lookup_sync * @menu_name: a menu name * * Searches for data from menu-cached for @menu_name. If no connection * exists yet then creates new one and retrieves all data. * * Returns: (transfer full): menu cache descriptor. * * Since: 0.3.1 */ MenuCache* menu_cache_lookup_sync( const char* menu_name ) { MenuCache* mc = menu_cache_lookup(menu_name); MenuCacheDir* root_dir = menu_cache_dup_root_dir(mc); /* ensure that the menu cache is loaded */ if(root_dir) menu_cache_item_unref(MENU_CACHE_ITEM(root_dir)); else /* if it's not yet loaded */ { MenuCacheNotifyId notify_id; /* add stub */ notify_id = menu_cache_add_reload_notify(mc, NULL, NULL); #if GLIB_CHECK_VERSION(2, 32, 0) g_mutex_lock(&sync_run_mutex); while(!mc->ready) g_cond_wait(&sync_run_cond, &sync_run_mutex); g_mutex_unlock(&sync_run_mutex); #else g_mutex_lock(sync_run_mutex); g_debug("menu_cache_lookup_sync: enter wait %p", mc); while(!mc->ready) g_cond_wait(sync_run_cond, sync_run_mutex); g_debug("menu_cache_lookup_sync: leave wait"); g_mutex_unlock(sync_run_mutex); #endif menu_cache_remove_reload_notify(mc, notify_id); } return mc; } static GSList* list_app_in_dir(MenuCacheDir* dir, GSList* list) { GSList* l; for( l = dir->children; l; l = l->next ) { MenuCacheItem* item = MENU_CACHE_ITEM(l->data); switch( menu_cache_item_get_type(item) ) { case MENU_CACHE_TYPE_DIR: list = list_app_in_dir( MENU_CACHE_DIR(item), list ); break; case MENU_CACHE_TYPE_APP: list = g_slist_prepend(list, menu_cache_item_ref(item)); break; case MENU_CACHE_TYPE_NONE: case MENU_CACHE_TYPE_SEP: break; } } return list; } /** * menu_cache_list_all_apps * @cache: a menu cache descriptor * * Retrieves full list of applications in menu cache. Returned list * should be freed with g_slist_free_full(list, menu_cache_item_unref) * after usage. * * Returns: (transfer full) (element-type MenuCacheItem): list of items. * * Since: 0.1.2 */ GSList* menu_cache_list_all_apps(MenuCache* cache) { GSList* list; MENU_CACHE_LOCK; if (G_UNLIKELY(!cache->root_dir)) /* empty cache */ list = NULL; else list = list_app_in_dir(cache->root_dir, NULL); MENU_CACHE_UNLOCK; return list; } /** * menu_cache_get_desktop_env_flag * @cache: a menu cache descriptor * @desktop_env: desktop environment name * * Makes bit mask of desktop environment from its name. The @desktop_env * may be simple string or colon separated list of compatible session * names according to XDG_CURRENT_DESKTOP freedesktop.org specification. * * Returns: DE bit mask. * * Since: 0.2.0 */ guint32 menu_cache_get_desktop_env_flag( MenuCache* cache, const char* desktop_env ) { char** de; char **envs; guint32 flags = 0; int j; if (desktop_env == NULL || desktop_env[0] == '\0') return flags; envs = g_strsplit(desktop_env, ":", -1); MENU_CACHE_LOCK; de = cache->known_des; for (j = 0; envs[j]; j++) { if( de ) { int i; for( i = 0; de[i]; ++i ) if (strcmp(envs[j], de[i]) == 0) break; if (de[i]) { flags |= 1 << (i + N_KNOWN_DESKTOPS); continue; } } if (strcmp(envs[j], "GNOME") == 0) flags |= SHOW_IN_GNOME; else if (strcmp(envs[j], "KDE") == 0) flags |= SHOW_IN_KDE; else if (strcmp(envs[j], "XFCE") == 0) flags |= SHOW_IN_XFCE; else if (strcmp(envs[j], "LXDE") == 0) flags |= SHOW_IN_LXDE; else if (strcmp(envs[j], "ROX") == 0) flags |= SHOW_IN_ROX; } MENU_CACHE_UNLOCK; g_strfreev(envs); return flags; } static MenuCacheItem *_scan_by_id(MenuCacheItem *item, const char *id) { GSList *l; if (item) switch (menu_cache_item_get_type(item)) { case MENU_CACHE_TYPE_DIR: for (l = MENU_CACHE_DIR(item)->children; l; l = l->next) { item = _scan_by_id(MENU_CACHE_ITEM(l->data), id); if (item) return item; } break; case MENU_CACHE_TYPE_APP: if (g_strcmp0(menu_cache_item_get_id(item), id) == 0) return item; break; default: ; } return NULL; } /** * menu_cache_find_item_by_id * @cache: a menu cache descriptor * @id: item ID (name such as 'application.desktop') * * Searches if @id already exists within @cache and returns found item. * Returned data should be freed with menu_cache_item_unref() after usage. * * Returns: (transfer full): found item or %NULL. * * Since: 0.5.0 */ MenuCacheItem *menu_cache_find_item_by_id(MenuCache *cache, const char *id) { MenuCacheItem *item = NULL; MENU_CACHE_LOCK; if (cache && id) item = _scan_by_id(MENU_CACHE_ITEM(cache->root_dir), id); if (item) menu_cache_item_ref(item); MENU_CACHE_UNLOCK; return item; } static GSList* list_app_in_dir_for_cat(MenuCacheDir *dir, GSList *list, const char *id) { const char **cat; GSList *l; for (l = dir->children; l; l = l->next) { MenuCacheItem *item = MENU_CACHE_ITEM(l->data); switch (item->type) { case MENU_CACHE_TYPE_DIR: list = list_app_in_dir_for_cat(MENU_CACHE_DIR(item), list, id); break; case MENU_CACHE_TYPE_APP: cat = MENU_CACHE_APP(item)->categories; if (cat) while (*cat) if (*cat++ == id) { list = g_slist_prepend(list, menu_cache_item_ref(item)); break; } break; case MENU_CACHE_TYPE_NONE: case MENU_CACHE_TYPE_SEP: break; } } return list; } /** * menu_cache_list_all_for_category * @cache: a menu cache descriptor * @category: category to list items * * Retrieves list of applications in menu cache which have @category in * their list of categories. The search is case-sensitive. Returned list * should be freed with g_slist_free_full(list, menu_cache_item_unref) * after usage. * * Returns: (transfer full) (element-type MenuCacheItem): list of items. * * Since: 1.0.0 */ GSList *menu_cache_list_all_for_category(MenuCache* cache, const char *category) { GQuark q; GSList *list; g_return_val_if_fail(cache != NULL && category != NULL, NULL); q = g_quark_try_string(category); if (q == 0) return NULL; MENU_CACHE_LOCK; if (G_UNLIKELY(cache->root_dir == NULL)) list = NULL; else list = list_app_in_dir_for_cat(cache->root_dir, NULL, g_quark_to_string(q)); MENU_CACHE_UNLOCK; return list; } static GSList* list_app_in_dir_for_kw(MenuCacheDir *dir, GSList *list, const char *kw) { GSList *l; for (l = dir->children; l; l = l->next) { MenuCacheItem *item = MENU_CACHE_ITEM(l->data); switch (item->type) { case MENU_CACHE_TYPE_DIR: list = list_app_in_dir_for_kw(MENU_CACHE_DIR(item), list, kw); break; case MENU_CACHE_TYPE_APP: if (strstr(MENU_CACHE_APP(item)->keywords, kw) != NULL) list = g_slist_prepend(list, menu_cache_item_ref(item)); break; case MENU_CACHE_TYPE_NONE: case MENU_CACHE_TYPE_SEP: break; } } return list; } /** * menu_cache_list_all_for_keyword * @cache: a menu cache descriptor * @keyword: a keyword to search * * Retrieves list of applications in menu cache which have a @keyword * as either a word or part of word in exec command, name, generic name * or defined keywords. The search is case-insensitive. Returned list * should be freed with g_slist_free_full(list, menu_cache_item_unref) * after usage. * * Returns: (transfer full) (element-type MenuCacheItem): list of items. * * Since: 1.0.0 */ GSList *menu_cache_list_all_for_keyword(MenuCache* cache, const char *keyword) { char *casefolded = g_utf8_casefold(keyword, -1); GSList *list; g_return_val_if_fail(cache != NULL && keyword != NULL, NULL); MENU_CACHE_LOCK; if (G_UNLIKELY(cache->root_dir == NULL)) list = NULL; else list = list_app_in_dir_for_kw(cache->root_dir, NULL, casefolded); MENU_CACHE_UNLOCK; g_free(casefolded); return list; }