diff options
Diffstat (limited to 'libeggsmclient')
-rw-r--r-- | libeggsmclient/Makefile.am | 18 | ||||
-rw-r--r-- | libeggsmclient/README | 10 | ||||
-rw-r--r-- | libeggsmclient/eggdesktopfile.c | 1477 | ||||
-rw-r--r-- | libeggsmclient/eggdesktopfile.h | 159 | ||||
-rw-r--r-- | libeggsmclient/eggsmclient-private.h | 53 | ||||
-rw-r--r-- | libeggsmclient/eggsmclient-xsmp.c | 1370 | ||||
-rw-r--r-- | libeggsmclient/eggsmclient.c | 589 | ||||
-rw-r--r-- | libeggsmclient/eggsmclient.h | 117 |
8 files changed, 3793 insertions, 0 deletions
diff --git a/libeggsmclient/Makefile.am b/libeggsmclient/Makefile.am new file mode 100644 index 00000000..62cde707 --- /dev/null +++ b/libeggsmclient/Makefile.am @@ -0,0 +1,18 @@ +INCLUDES = -DGTK_DISABLE_DEPRECATED \ + -DGDK_DISABLE_DEPRECATED \ + -DG_DISABLE_DEPRECATED + +noinst_LTLIBRARIES = libeggsmclient.la + +libeggsmclient_la_LIBADD = $(LIBEGGSMCLIENT_LIBS) \ + -lSM -lICE +libeggsmclient_la_CFLAGS = $(LIBEGGSMCLIENT_CFLAGS) \ + -DEGG_SM_CLIENT_BACKEND_XSMP \ + -DG_LOG_DOMAIN=\""EggSMClient"\" \ + $(WARN_CFLAGS) +libeggsmclient_la_SOURCES = eggdesktopfile.h \ + eggdesktopfile.c \ + eggsmclient.h \ + eggsmclient.c \ + eggsmclient-private.h \ + eggsmclient-xsmp.c diff --git a/libeggsmclient/README b/libeggsmclient/README new file mode 100644 index 00000000..00b60a26 --- /dev/null +++ b/libeggsmclient/README @@ -0,0 +1,10 @@ +MATE-Client in the libmateui library will be removed from the MATE +software stack [1]. At this time, a replacement for the session +management client has not been added to GTK+. The code in this +directory is a copy of the session management source code in the +libegg library. Hopefully, this will be a temporary solution. + +The bugzilla report at bugs.mate.org is #79285, "session management +support". + +[1] http://live.mate.org/LibmateMustDie diff --git a/libeggsmclient/eggdesktopfile.c b/libeggsmclient/eggdesktopfile.c new file mode 100644 index 00000000..6cd33c3b --- /dev/null +++ b/libeggsmclient/eggdesktopfile.c @@ -0,0 +1,1477 @@ +/* eggdesktopfile.c - Freedesktop.Org Desktop Files + * Copyright (C) 2007 Novell, Inc. + * + * Based on mate-desktop-item.c + * Copyright (C) 1999, 2000 Red Hat Inc. + * Copyright (C) 2001 George Lebl + * + * 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 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; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - + * Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "eggdesktopfile.h" + +#include <string.h> +#include <unistd.h> + +#include <glib/gi18n.h> +#include <gdk/gdkx.h> +#include <gtk/gtk.h> + +struct EggDesktopFile { + GKeyFile *key_file; + char *source; + + char *name, *icon; + EggDesktopFileType type; + char document_code; +}; + +/** + * egg_desktop_file_new: + * @desktop_file_path: path to a Freedesktop-style Desktop file + * @error: error pointer + * + * Creates a new #EggDesktopFile for @desktop_file. + * + * Return value: the new #EggDesktopFile, or %NULL on error. + **/ +EggDesktopFile * +egg_desktop_file_new (const char *desktop_file_path, GError **error) +{ + GKeyFile *key_file; + + key_file = g_key_file_new (); + if (!g_key_file_load_from_file (key_file, desktop_file_path, 0, error)) + { + g_key_file_free (key_file); + return NULL; + } + + return egg_desktop_file_new_from_key_file (key_file, desktop_file_path, + error); +} + +/** + * egg_desktop_file_new_from_data_dirs: + * @desktop_file_path: relative path to a Freedesktop-style Desktop file + * @error: error pointer + * + * Looks for @desktop_file_path in the paths returned from + * g_get_user_data_dir() and g_get_system_data_dirs(), and creates + * a new #EggDesktopFile from it. + * + * Return value: the new #EggDesktopFile, or %NULL on error. + **/ +EggDesktopFile * +egg_desktop_file_new_from_data_dirs (const char *desktop_file_path, + GError **error) +{ + EggDesktopFile *desktop_file; + GKeyFile *key_file; + char *full_path; + + key_file = g_key_file_new (); + if (!g_key_file_load_from_data_dirs (key_file, desktop_file_path, + &full_path, 0, error)) + { + g_key_file_free (key_file); + return NULL; + } + + desktop_file = egg_desktop_file_new_from_key_file (key_file, + full_path, + error); + g_free (full_path); + return desktop_file; +} + +/** + * egg_desktop_file_new_from_dirs: + * @desktop_file_path: relative path to a Freedesktop-style Desktop file + * @search_dirs: NULL-terminated array of directories to search + * @error: error pointer + * + * Looks for @desktop_file_path in the paths returned from + * g_get_user_data_dir() and g_get_system_data_dirs(), and creates + * a new #EggDesktopFile from it. + * + * Return value: the new #EggDesktopFile, or %NULL on error. + **/ +EggDesktopFile * +egg_desktop_file_new_from_dirs (const char *desktop_file_path, + const char **search_dirs, + GError **error) +{ + EggDesktopFile *desktop_file; + GKeyFile *key_file; + char *full_path; + + key_file = g_key_file_new (); + if (!g_key_file_load_from_dirs (key_file, desktop_file_path, search_dirs, + &full_path, 0, error)) + { + g_key_file_free (key_file); + return NULL; + } + + desktop_file = egg_desktop_file_new_from_key_file (key_file, + full_path, + error); + g_free (full_path); + return desktop_file; +} + +/** + * egg_desktop_file_new_from_key_file: + * @key_file: a #GKeyFile representing a desktop file + * @source: the path or URI that @key_file was loaded from, or %NULL + * @error: error pointer + * + * Creates a new #EggDesktopFile for @key_file. Assumes ownership of + * @key_file (on success or failure); you should consider @key_file to + * be freed after calling this function. + * + * Return value: the new #EggDesktopFile, or %NULL on error. + **/ +EggDesktopFile * +egg_desktop_file_new_from_key_file (GKeyFile *key_file, + const char *source, + GError **error) +{ + EggDesktopFile *desktop_file; + char *version, *type; + + if (!g_key_file_has_group (key_file, EGG_DESKTOP_FILE_GROUP)) + { + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_INVALID, + _("File is not a valid .desktop file")); + g_key_file_free (key_file); + return NULL; + } + + version = g_key_file_get_value (key_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_VERSION, + NULL); + if (version) + { + double version_num; + char *end; + + version_num = g_ascii_strtod (version, &end); + if (*end) + { + g_warning ("Invalid Version string '%s' in %s", + version, source ? source : "(unknown)"); + } + else if (version_num > 1.0) + { + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_INVALID, + _("Unrecognized desktop file Version '%s'"), version); + g_free (version); + g_key_file_free (key_file); + return NULL; + } + g_free (version); + } + + desktop_file = g_new0 (EggDesktopFile, 1); + desktop_file->key_file = key_file; + + if (g_path_is_absolute (source)) + desktop_file->source = g_filename_to_uri (source, NULL, NULL); + else + desktop_file->source = g_strdup (source); + + desktop_file->name = g_key_file_get_string (key_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_NAME, error); + if (!desktop_file->name) + { + egg_desktop_file_free (desktop_file); + return NULL; + } + + type = g_key_file_get_string (key_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_TYPE, error); + if (!type) + { + egg_desktop_file_free (desktop_file); + return NULL; + } + + if (!strcmp (type, "Application")) + { + char *exec, *p; + + desktop_file->type = EGG_DESKTOP_FILE_TYPE_APPLICATION; + + exec = g_key_file_get_string (key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_EXEC, + error); + if (!exec) + { + egg_desktop_file_free (desktop_file); + g_free (type); + return NULL; + } + + /* See if it takes paths or URIs or neither */ + for (p = exec; *p; p++) + { + if (*p == '%') + { + if (p[1] == '\0' || strchr ("FfUu", p[1])) + { + desktop_file->document_code = p[1]; + break; + } + p++; + } + } + + g_free (exec); + } + else if (!strcmp (type, "Link")) + { + char *url; + + desktop_file->type = EGG_DESKTOP_FILE_TYPE_LINK; + + url = g_key_file_get_string (key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_URL, + error); + if (!url) + { + egg_desktop_file_free (desktop_file); + g_free (type); + return NULL; + } + g_free (url); + } + else if (!strcmp (type, "Directory")) + desktop_file->type = EGG_DESKTOP_FILE_TYPE_DIRECTORY; + else + desktop_file->type = EGG_DESKTOP_FILE_TYPE_UNRECOGNIZED; + + g_free (type); + + /* Check the Icon key */ + desktop_file->icon = g_key_file_get_string (key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_ICON, + NULL); + if (desktop_file->icon && !g_path_is_absolute (desktop_file->icon)) + { + char *ext; + + /* Lots of .desktop files still get this wrong */ + ext = strrchr (desktop_file->icon, '.'); + if (ext && (!strcmp (ext, ".png") || + !strcmp (ext, ".xpm") || + !strcmp (ext, ".svg"))) + { + g_warning ("Desktop file '%s' has malformed Icon key '%s'" + "(should not include extension)", + source ? source : "(unknown)", + desktop_file->icon); + *ext = '\0'; + } + } + + return desktop_file; +} + +/** + * egg_desktop_file_free: + * @desktop_file: an #EggDesktopFile + * + * Frees @desktop_file. + **/ +void +egg_desktop_file_free (EggDesktopFile *desktop_file) +{ + g_key_file_free (desktop_file->key_file); + g_free (desktop_file->source); + g_free (desktop_file->name); + g_free (desktop_file->icon); + g_free (desktop_file); +} + +/** + * egg_desktop_file_get_source: + * @desktop_file: an #EggDesktopFile + * + * Gets the URI that @desktop_file was loaded from. + * + * Return value: @desktop_file's source URI + **/ +const char * +egg_desktop_file_get_source (EggDesktopFile *desktop_file) +{ + return desktop_file->source; +} + +/** + * egg_desktop_file_get_desktop_file_type: + * @desktop_file: an #EggDesktopFile + * + * Gets the desktop file type of @desktop_file. + * + * Return value: @desktop_file's type + **/ +EggDesktopFileType +egg_desktop_file_get_desktop_file_type (EggDesktopFile *desktop_file) +{ + return desktop_file->type; +} + +/** + * egg_desktop_file_get_name: + * @desktop_file: an #EggDesktopFile + * + * Gets the (localized) value of @desktop_file's "Name" key. + * + * Return value: the application/link name + **/ +const char * +egg_desktop_file_get_name (EggDesktopFile *desktop_file) +{ + return desktop_file->name; +} + +/** + * egg_desktop_file_get_icon: + * @desktop_file: an #EggDesktopFile + * + * Gets the value of @desktop_file's "Icon" key. + * + * If the icon string is a full path (that is, if g_path_is_absolute() + * returns %TRUE when called on it), it points to a file containing an + * unthemed icon. If the icon string is not a full path, it is the + * name of a themed icon, which can be looked up with %GtkIconTheme, + * or passed directly to a theme-aware widget like %GtkImage or + * %GtkCellRendererPixbuf. + * + * Return value: the icon path or name + **/ +const char * +egg_desktop_file_get_icon (EggDesktopFile *desktop_file) +{ + return desktop_file->icon; +} + +gboolean +egg_desktop_file_has_key (EggDesktopFile *desktop_file, + const char *key, + GError **error) +{ + return g_key_file_has_key (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, + error); +} + +char * +egg_desktop_file_get_string (EggDesktopFile *desktop_file, + const char *key, + GError **error) +{ + return g_key_file_get_string (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, + error); +} + +char * +egg_desktop_file_get_locale_string (EggDesktopFile *desktop_file, + const char *key, + const char *locale, + GError **error) +{ + return g_key_file_get_locale_string (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, locale, + error); +} + +gboolean +egg_desktop_file_get_boolean (EggDesktopFile *desktop_file, + const char *key, + GError **error) +{ + return g_key_file_get_boolean (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, + error); +} + +double +egg_desktop_file_get_numeric (EggDesktopFile *desktop_file, + const char *key, + GError **error) +{ + return g_key_file_get_double (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, + error); +} + +char ** +egg_desktop_file_get_string_list (EggDesktopFile *desktop_file, + const char *key, + gsize *length, + GError **error) +{ + return g_key_file_get_string_list (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, length, + error); +} + +char ** +egg_desktop_file_get_locale_string_list (EggDesktopFile *desktop_file, + const char *key, + const char *locale, + gsize *length, + GError **error) +{ + return g_key_file_get_locale_string_list (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, + locale, length, + error); +} + +/** + * egg_desktop_file_can_launch: + * @desktop_file: an #EggDesktopFile + * @desktop_environment: the name of the running desktop environment, + * or %NULL + * + * Tests if @desktop_file can/should be launched in the current + * environment. If @desktop_environment is non-%NULL, @desktop_file's + * "OnlyShowIn" and "NotShowIn" keys are checked to make sure that + * this desktop_file is appropriate for the named environment. + * + * Furthermore, if @desktop_file has type + * %EGG_DESKTOP_FILE_TYPE_APPLICATION, its "TryExec" key (if any) is + * also checked, to make sure the binary it points to exists. + * + * egg_desktop_file_can_launch() does NOT check the value of the + * "Hidden" key. + * + * Return value: %TRUE if @desktop_file can be launched + **/ +gboolean +egg_desktop_file_can_launch (EggDesktopFile *desktop_file, + const char *desktop_environment) +{ + char *try_exec, *found_program; + char **only_show_in, **not_show_in; + gboolean found; + int i; + + if (desktop_file->type != EGG_DESKTOP_FILE_TYPE_APPLICATION && + desktop_file->type != EGG_DESKTOP_FILE_TYPE_LINK) + return FALSE; + + if (desktop_environment) + { + only_show_in = g_key_file_get_string_list (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_ONLY_SHOW_IN, + NULL, NULL); + if (only_show_in) + { + for (i = 0, found = FALSE; only_show_in[i] && !found; i++) + { + if (!strcmp (only_show_in[i], desktop_environment)) + found = TRUE; + } + + g_strfreev (only_show_in); + + if (!found) + return FALSE; + } + + not_show_in = g_key_file_get_string_list (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_NOT_SHOW_IN, + NULL, NULL); + if (not_show_in) + { + for (i = 0, found = FALSE; not_show_in[i] && !found; i++) + { + if (!strcmp (not_show_in[i], desktop_environment)) + found = TRUE; + } + + g_strfreev (not_show_in); + + if (found) + return FALSE; + } + } + + if (desktop_file->type == EGG_DESKTOP_FILE_TYPE_APPLICATION) + { + try_exec = g_key_file_get_string (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_TRY_EXEC, + NULL); + if (try_exec) + { + found_program = g_find_program_in_path (try_exec); + g_free (try_exec); + + if (!found_program) + return FALSE; + g_free (found_program); + } + } + + return TRUE; +} + +/** + * egg_desktop_file_accepts_documents: + * @desktop_file: an #EggDesktopFile + * + * Tests if @desktop_file represents an application that can accept + * documents on the command line. + * + * Return value: %TRUE or %FALSE + **/ +gboolean +egg_desktop_file_accepts_documents (EggDesktopFile *desktop_file) +{ + return desktop_file->document_code != 0; +} + +/** + * egg_desktop_file_accepts_multiple: + * @desktop_file: an #EggDesktopFile + * + * Tests if @desktop_file can accept multiple documents at once. + * + * If this returns %FALSE, you can still pass multiple documents to + * egg_desktop_file_launch(), but that will result in multiple copies + * of the application being launched. See egg_desktop_file_launch() + * for more details. + * + * Return value: %TRUE or %FALSE + **/ +gboolean +egg_desktop_file_accepts_multiple (EggDesktopFile *desktop_file) +{ + return (desktop_file->document_code == 'F' || + desktop_file->document_code == 'U'); +} + +/** + * egg_desktop_file_accepts_uris: + * @desktop_file: an #EggDesktopFile + * + * Tests if @desktop_file can accept (non-"file:") URIs as documents to + * open. + * + * Return value: %TRUE or %FALSE + **/ +gboolean +egg_desktop_file_accepts_uris (EggDesktopFile *desktop_file) +{ + return (desktop_file->document_code == 'U' || + desktop_file->document_code == 'u'); +} + +static void +append_quoted_word (GString *str, + const char *s, + gboolean in_single_quotes, + gboolean in_double_quotes) +{ + const char *p; + + if (!in_single_quotes && !in_double_quotes) + g_string_append_c (str, '\''); + else if (!in_single_quotes && in_double_quotes) + g_string_append (str, "\"'"); + + if (!strchr (s, '\'')) + g_string_append (str, s); + else + { + for (p = s; *p != '\0'; p++) + { + if (*p == '\'') + g_string_append (str, "'\\''"); + else + g_string_append_c (str, *p); + } + } + + if (!in_single_quotes && !in_double_quotes) + g_string_append_c (str, '\''); + else if (!in_single_quotes && in_double_quotes) + g_string_append (str, "'\""); +} + +static void +do_percent_subst (EggDesktopFile *desktop_file, + char code, + GString *str, + GSList **documents, + gboolean in_single_quotes, + gboolean in_double_quotes) +{ + GSList *d; + char *doc; + + switch (code) + { + case '%': + g_string_append_c (str, '%'); + break; + + case 'F': + case 'U': + for (d = *documents; d; d = d->next) + { + doc = d->data; + g_string_append (str, " "); + append_quoted_word (str, doc, in_single_quotes, in_double_quotes); + } + *documents = NULL; + break; + + case 'f': + case 'u': + if (*documents) + { + doc = (*documents)->data; + g_string_append (str, " "); + append_quoted_word (str, doc, in_single_quotes, in_double_quotes); + *documents = (*documents)->next; + } + break; + + case 'i': + if (desktop_file->icon) + { + g_string_append (str, "--icon "); + append_quoted_word (str, desktop_file->icon, + in_single_quotes, in_double_quotes); + } + break; + + case 'c': + if (desktop_file->name) + { + append_quoted_word (str, desktop_file->name, + in_single_quotes, in_double_quotes); + } + break; + + case 'k': + if (desktop_file->source) + { + append_quoted_word (str, desktop_file->source, + in_single_quotes, in_double_quotes); + } + break; + + case 'D': + case 'N': + case 'd': + case 'n': + case 'v': + case 'm': + /* Deprecated; skip */ + break; + + default: + g_warning ("Unrecognized %%-code '%%%c' in Exec", code); + break; + } +} + +static char * +parse_exec (EggDesktopFile *desktop_file, + GSList **documents, + GError **error) +{ + char *exec, *p, *command; + gboolean escape, single_quot, double_quot; + GString *gs; + + exec = g_key_file_get_string (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_EXEC, + error); + if (!exec) + return NULL; + + /* Build the command */ + gs = g_string_new (NULL); + escape = single_quot = double_quot = FALSE; + + for (p = exec; *p != '\0'; p++) + { + if (escape) + { + escape = FALSE; + g_string_append_c (gs, *p); + } + else if (*p == '\\') + { + if (!single_quot) + escape = TRUE; + g_string_append_c (gs, *p); + } + else if (*p == '\'') + { + g_string_append_c (gs, *p); + if (!single_quot && !double_quot) + single_quot = TRUE; + else if (single_quot) + single_quot = FALSE; + } + else if (*p == '"') + { + g_string_append_c (gs, *p); + if (!single_quot && !double_quot) + double_quot = TRUE; + else if (double_quot) + double_quot = FALSE; + } + else if (*p == '%' && p[1]) + { + do_percent_subst (desktop_file, p[1], gs, documents, + single_quot, double_quot); + p++; + } + else + g_string_append_c (gs, *p); + } + + g_free (exec); + command = g_string_free (gs, FALSE); + + /* Prepend "xdg-terminal " if needed (FIXME: use gvfs) */ + if (g_key_file_has_key (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_TERMINAL, + NULL)) + { + GError *terminal_error = NULL; + gboolean use_terminal = + g_key_file_get_boolean (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_TERMINAL, + &terminal_error); + if (terminal_error) + { + g_free (command); + g_propagate_error (error, terminal_error); + return NULL; + } + + if (use_terminal) + { + gs = g_string_new ("xdg-terminal "); + append_quoted_word (gs, command, FALSE, FALSE); + g_free (command); + command = g_string_free (gs, FALSE); + } + } + + return command; +} + +static GSList * +translate_document_list (EggDesktopFile *desktop_file, GSList *documents) +{ + gboolean accepts_uris = egg_desktop_file_accepts_uris (desktop_file); + GSList *ret, *d; + + for (d = documents, ret = NULL; d; d = d->next) + { + const char *document = d->data; + gboolean is_uri = !g_path_is_absolute (document); + char *translated; + + if (accepts_uris) + { + if (is_uri) + translated = g_strdup (document); + else + translated = g_filename_to_uri (document, NULL, NULL); + } + else + { + if (is_uri) + translated = g_filename_from_uri (document, NULL, NULL); + else + translated = g_strdup (document); + } + + if (translated) + ret = g_slist_prepend (ret, translated); + } + + return g_slist_reverse (ret); +} + +static void +free_document_list (GSList *documents) +{ + GSList *d; + + for (d = documents; d; d = d->next) + g_free (d->data); + g_slist_free (documents); +} + +/** + * egg_desktop_file_parse_exec: + * @desktop_file: a #EggDesktopFile + * @documents: a list of document paths or URIs + * @error: error pointer + * + * Parses @desktop_file's Exec key, inserting @documents into it, and + * returns the result. + * + * If @documents contains non-file: URIs and @desktop_file does not + * accept URIs, those URIs will be ignored. Likewise, if @documents + * contains more elements than @desktop_file accepts, the extra + * documents will be ignored. + * + * Return value: the parsed Exec string + **/ +char * +egg_desktop_file_parse_exec (EggDesktopFile *desktop_file, + GSList *documents, + GError **error) +{ + GSList *translated, *docs; + char *command; + + docs = translated = translate_document_list (desktop_file, documents); + command = parse_exec (desktop_file, &docs, error); + free_document_list (translated); + + return command; +} + +static gboolean +parse_link (EggDesktopFile *desktop_file, + EggDesktopFile **app_desktop_file, + GSList **documents, + GError **error) +{ + char *url; + GKeyFile *key_file; + + url = g_key_file_get_string (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_URL, + error); + if (!url) + return FALSE; + *documents = g_slist_prepend (NULL, url); + + /* FIXME: use gvfs */ + key_file = g_key_file_new (); + g_key_file_set_string (key_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_NAME, + "xdg-open"); + g_key_file_set_string (key_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_TYPE, + "Application"); + g_key_file_set_string (key_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_EXEC, + "xdg-open %u"); + *app_desktop_file = egg_desktop_file_new_from_key_file (key_file, NULL, NULL); + return TRUE; +} + +#if GTK_CHECK_VERSION (2, 12, 0) +static char * +start_startup_notification (GdkDisplay *display, + EggDesktopFile *desktop_file, + const char *argv0, + int screen, + int workspace, + guint32 launch_time) +{ + static int sequence = 0; + char *startup_id; + char *description, *wmclass; + char *screen_str, *workspace_str; + + if (g_key_file_has_key (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_STARTUP_NOTIFY, + NULL)) + { + if (!g_key_file_get_boolean (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_STARTUP_NOTIFY, + NULL)) + return NULL; + wmclass = NULL; + } + else + { + wmclass = g_key_file_get_string (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_STARTUP_WM_CLASS, + NULL); + if (!wmclass) + return NULL; + } + + if (launch_time == (guint32)-1) + launch_time = gdk_x11_display_get_user_time (display); + startup_id = g_strdup_printf ("%s-%lu-%s-%s-%d_TIME%lu", + g_get_prgname (), + (unsigned long)getpid (), + g_get_host_name (), + argv0, + sequence++, + (unsigned long)launch_time); + + description = g_strdup_printf (_("Starting %s"), desktop_file->name); + screen_str = g_strdup_printf ("%d", screen); + workspace_str = workspace == -1 ? NULL : g_strdup_printf ("%d", workspace); + + gdk_x11_display_broadcast_startup_message (display, "new", + "ID", startup_id, + "NAME", desktop_file->name, + "SCREEN", screen_str, + "BIN", argv0, + "ICON", desktop_file->icon, + "DESKTOP", workspace_str, + "DESCRIPTION", description, + "WMCLASS", wmclass, + NULL); + + g_free (description); + g_free (wmclass); + g_free (screen_str); + g_free (workspace_str); + + return startup_id; +} + +static void +end_startup_notification (GdkDisplay *display, + const char *startup_id) +{ + gdk_x11_display_broadcast_startup_message (display, "remove", + "ID", startup_id, + NULL); +} + +#define EGG_DESKTOP_FILE_SN_TIMEOUT_LENGTH (30 /* seconds */) + +typedef struct { + GdkDisplay *display; + char *startup_id; +} StartupNotificationData; + +static gboolean +startup_notification_timeout (gpointer data) +{ + StartupNotificationData *sn_data = data; + + end_startup_notification (sn_data->display, sn_data->startup_id); + g_object_unref (sn_data->display); + g_free (sn_data->startup_id); + g_free (sn_data); + + return FALSE; +} + +static void +set_startup_notification_timeout (GdkDisplay *display, + const char *startup_id) +{ + StartupNotificationData *sn_data; + + sn_data = g_new (StartupNotificationData, 1); + sn_data->display = g_object_ref (display); + sn_data->startup_id = g_strdup (startup_id); + + g_timeout_add_seconds (EGG_DESKTOP_FILE_SN_TIMEOUT_LENGTH, + startup_notification_timeout, sn_data); +} +#endif /* GTK 2.12 */ + +static GPtrArray * +array_putenv (GPtrArray *env, char *variable) +{ + guint i, keylen; + + if (!env) + { + char **envp; + + env = g_ptr_array_new (); + + envp = g_listenv (); + for (i = 0; envp[i]; i++) + { + const char *value; + + value = g_getenv (envp[i]); + g_ptr_array_add (env, g_strdup_printf ("%s=%s", envp[i], + value ? value : "")); + } + g_strfreev (envp); + } + + keylen = strcspn (variable, "="); + + /* Remove old value of key */ + for (i = 0; i < env->len; i++) + { + char *envvar = env->pdata[i]; + + if (!strncmp (envvar, variable, keylen) && envvar[keylen] == '=') + { + g_free (envvar); + g_ptr_array_remove_index_fast (env, i); + break; + } + } + + /* Add new value */ + g_ptr_array_add (env, g_strdup (variable)); + + return env; +} + +static gboolean +egg_desktop_file_launchv (EggDesktopFile *desktop_file, + GSList *documents, va_list args, + GError **error) +{ + EggDesktopFileLaunchOption option; + GSList *translated_documents = NULL, *docs = NULL; + char *command, **argv; + int argc, i, screen_num; + gboolean success, current_success; + GdkDisplay *display; + char *startup_id; + + GPtrArray *env = NULL; + char **variables = NULL; + GdkScreen *screen = NULL; + int workspace = -1; + const char *directory = NULL; + guint32 launch_time = (guint32)-1; + GSpawnFlags flags = G_SPAWN_SEARCH_PATH; + GSpawnChildSetupFunc setup_func = NULL; + gpointer setup_data = NULL; + + GPid *ret_pid = NULL; + int *ret_stdin = NULL, *ret_stdout = NULL, *ret_stderr = NULL; + char **ret_startup_id = NULL; + + if (documents && desktop_file->document_code == 0) + { + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE, + _("Application does not accept documents on command line")); + return FALSE; + } + + /* Read the options: technically it's incorrect for the caller to + * NULL-terminate the list of options (rather than 0-terminating + * it), but NULL-terminating lets us use G_GNUC_NULL_TERMINATED, + * it's more consistent with other glib/gtk methods, and it will + * work as long as sizeof (int) <= sizeof (NULL), and NULL is + * represented as 0. (Which is true everywhere we care about.) + */ + while ((option = va_arg (args, EggDesktopFileLaunchOption))) + { + switch (option) + { + case EGG_DESKTOP_FILE_LAUNCH_CLEARENV: + if (env) + g_ptr_array_free (env, TRUE); + env = g_ptr_array_new (); + break; + case EGG_DESKTOP_FILE_LAUNCH_PUTENV: + variables = va_arg (args, char **); + for (i = 0; variables[i]; i++) + env = array_putenv (env, variables[i]); + break; + + case EGG_DESKTOP_FILE_LAUNCH_SCREEN: + screen = va_arg (args, GdkScreen *); + break; + case EGG_DESKTOP_FILE_LAUNCH_WORKSPACE: + workspace = va_arg (args, int); + break; + + case EGG_DESKTOP_FILE_LAUNCH_DIRECTORY: + directory = va_arg (args, const char *); + break; + case EGG_DESKTOP_FILE_LAUNCH_TIME: + launch_time = va_arg (args, guint32); + break; + case EGG_DESKTOP_FILE_LAUNCH_FLAGS: + flags |= va_arg (args, GSpawnFlags); + /* Make sure they didn't set any flags that don't make sense. */ + flags &= ~G_SPAWN_FILE_AND_ARGV_ZERO; + break; + case EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC: + setup_func = va_arg (args, GSpawnChildSetupFunc); + setup_data = va_arg (args, gpointer); + break; + + case EGG_DESKTOP_FILE_LAUNCH_RETURN_PID: + ret_pid = va_arg (args, GPid *); + break; + case EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE: + ret_stdin = va_arg (args, int *); + break; + case EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE: + ret_stdout = va_arg (args, int *); + break; + case EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE: + ret_stderr = va_arg (args, int *); + break; + case EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID: + ret_startup_id = va_arg (args, char **); + break; + + default: + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_UNRECOGNIZED_OPTION, + _("Unrecognized launch option: %d"), + GPOINTER_TO_INT (option)); + success = FALSE; + goto out; + } + } + + if (screen) + { + char *display_name = gdk_screen_make_display_name (screen); + char *display_env = g_strdup_printf ("DISPLAY=%s", display_name); + env = array_putenv (env, display_env); + g_free (display_name); + g_free (display_env); + + display = gdk_screen_get_display (screen); + } + else + { + display = gdk_display_get_default (); + screen = gdk_display_get_default_screen (display); + } + screen_num = gdk_screen_get_number (screen); + + translated_documents = translate_document_list (desktop_file, documents); + docs = translated_documents; + + success = FALSE; + + do + { + command = parse_exec (desktop_file, &docs, error); + if (!command) + goto out; + + if (!g_shell_parse_argv (command, &argc, &argv, error)) + { + g_free (command); + goto out; + } + g_free (command); + +#if GTK_CHECK_VERSION (2, 12, 0) + startup_id = start_startup_notification (display, desktop_file, + argv[0], screen_num, + workspace, launch_time); + if (startup_id) + { + char *startup_id_env = g_strdup_printf ("DESKTOP_STARTUP_ID=%s", + startup_id); + env = array_putenv (env, startup_id_env); + g_free (startup_id_env); + } +#else + startup_id = NULL; +#endif /* GTK 2.12 */ + + if (env != NULL) + g_ptr_array_add (env, NULL); + + current_success = + g_spawn_async_with_pipes (directory, + argv, + env ? (char **)(env->pdata) : NULL, + flags, + setup_func, setup_data, + ret_pid, + ret_stdin, ret_stdout, ret_stderr, + error); + g_strfreev (argv); + + if (startup_id) + { +#if GTK_CHECK_VERSION (2, 12, 0) + if (current_success) + { + set_startup_notification_timeout (display, startup_id); + + if (ret_startup_id) + *ret_startup_id = startup_id; + else + g_free (startup_id); + } + else +#endif /* GTK 2.12 */ + g_free (startup_id); + } + else if (ret_startup_id) + *ret_startup_id = NULL; + + if (current_success) + { + /* If we successfully launch any instances of the app, make + * sure we return TRUE and don't set @error. + */ + success = TRUE; + error = NULL; + + /* Also, only set the output params on the first one */ + ret_pid = NULL; + ret_stdin = ret_stdout = ret_stderr = NULL; + ret_startup_id = NULL; + } + } + while (docs && current_success); + + out: + if (env) + { + g_strfreev ((char **)env->pdata); + g_ptr_array_free (env, FALSE); + } + free_document_list (translated_documents); + + return success; +} + +/** + * egg_desktop_file_launch: + * @desktop_file: an #EggDesktopFile + * @documents: a list of URIs or paths to documents to open + * @error: error pointer + * @...: additional options + * + * Launches @desktop_file with the given arguments. Additional options + * can be specified as follows: + * + * %EGG_DESKTOP_FILE_LAUNCH_CLEARENV: (no arguments) + * clears the environment in the child process + * %EGG_DESKTOP_FILE_LAUNCH_PUTENV: (char **variables) + * adds the NAME=VALUE strings in the given %NULL-terminated + * array to the child process's environment + * %EGG_DESKTOP_FILE_LAUNCH_SCREEN: (GdkScreen *screen) + * causes the application to be launched on the given screen + * %EGG_DESKTOP_FILE_LAUNCH_WORKSPACE: (int workspace) + * causes the application to be launched on the given workspace + * %EGG_DESKTOP_FILE_LAUNCH_DIRECTORY: (char *dir) + * causes the application to be launched in the given directory + * %EGG_DESKTOP_FILE_LAUNCH_TIME: (guint32 launch_time) + * sets the "launch time" for the application. If the user + * interacts with another window after @launch_time but before + * the launched application creates its first window, the window + * manager may choose to not give focus to the new application. + * Passing 0 for @launch_time will explicitly request that the + * application not receive focus. + * %EGG_DESKTOP_FILE_LAUNCH_FLAGS (GSpawnFlags flags) + * Sets additional #GSpawnFlags to use. See g_spawn_async() for + * more details. + * %EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC (GSpawnChildSetupFunc, gpointer) + * Sets the child setup callback and the data to pass to it. + * (See g_spawn_async() for more details.) + * + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_PID (GPid **pid) + * On a successful launch, sets *@pid to the PID of the launched + * application. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID (char **startup_id) + * On a successful launch, sets *@startup_id to the Startup + * Notification "startup id" of the launched application. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE (int *fd) + * On a successful launch, sets *@fd to the file descriptor of + * a pipe connected to the application's stdin. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE (int *fd) + * On a successful launch, sets *@fd to the file descriptor of + * a pipe connected to the application's stdout. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE (int *fd) + * On a successful launch, sets *@fd to the file descriptor of + * a pipe connected to the application's stderr. + * + * The options should be terminated with a single %NULL. + * + * If @documents contains multiple documents, but + * egg_desktop_file_accepts_multiple() returns %FALSE for + * @desktop_file, then egg_desktop_file_launch() will actually launch + * multiple instances of the application. In that case, the return + * value (as well as any values passed via + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_PID, etc) will only reflect the + * first instance of the application that was launched (but the + * %EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC will be called for each + * instance). + * + * Return value: %TRUE if the application was successfully launched. + **/ +gboolean +egg_desktop_file_launch (EggDesktopFile *desktop_file, + GSList *documents, GError **error, + ...) +{ + va_list args; + gboolean success; + EggDesktopFile *app_desktop_file; + + switch (desktop_file->type) + { + case EGG_DESKTOP_FILE_TYPE_APPLICATION: + va_start (args, error); + success = egg_desktop_file_launchv (desktop_file, documents, + args, error); + va_end (args); + break; + + case EGG_DESKTOP_FILE_TYPE_LINK: + if (documents) + { + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE, + _("Can't pass document URIs to a 'Type=Link' desktop entry")); + return FALSE; + } + + if (!parse_link (desktop_file, &app_desktop_file, &documents, error)) + return FALSE; + + va_start (args, error); + success = egg_desktop_file_launchv (app_desktop_file, documents, + args, error); + va_end (args); + + egg_desktop_file_free (app_desktop_file); + free_document_list (documents); + break; + + default: + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE, + _("Not a launchable item")); + success = FALSE; + break; + } + + return success; +} + + +GQuark +egg_desktop_file_error_quark (void) +{ + return g_quark_from_static_string ("egg-desktop_file-error-quark"); +} + + +G_LOCK_DEFINE_STATIC (egg_desktop_file); +static EggDesktopFile *egg_desktop_file; + +/** + * egg_set_desktop_file: + * @desktop_file_path: path to the application's desktop file + * + * Creates an #EggDesktopFile for the application from the data at + * @desktop_file_path. This will also call g_set_application_name() + * with the localized application name from the desktop file, and + * gtk_window_set_default_icon_name() or + * gtk_window_set_default_icon_from_file() with the application's + * icon. Other code may use additional information from the desktop + * file. + * + * Note that for thread safety reasons, this function can only + * be called once. + **/ +void +egg_set_desktop_file (const char *desktop_file_path) +{ + GError *error = NULL; + + G_LOCK (egg_desktop_file); + if (egg_desktop_file) + egg_desktop_file_free (egg_desktop_file); + + egg_desktop_file = egg_desktop_file_new (desktop_file_path, &error); + if (error) + { + g_warning ("Could not load desktop file '%s': %s", + desktop_file_path, error->message); + g_error_free (error); + } + + if (egg_desktop_file) { + /* Set localized application name and default window icon */ + if (egg_desktop_file->name) + g_set_application_name (egg_desktop_file->name); + if (egg_desktop_file->icon) + { + if (g_path_is_absolute (egg_desktop_file->icon)) + gtk_window_set_default_icon_from_file (egg_desktop_file->icon, NULL); + else + gtk_window_set_default_icon_name (egg_desktop_file->icon); + } + } + + G_UNLOCK (egg_desktop_file); +} + +/** + * egg_get_desktop_file: + * + * Gets the application's #EggDesktopFile, as set by + * egg_set_desktop_file(). + * + * Return value: the #EggDesktopFile, or %NULL if it hasn't been set. + **/ +EggDesktopFile * +egg_get_desktop_file (void) +{ + EggDesktopFile *retval; + + G_LOCK (egg_desktop_file); + retval = egg_desktop_file; + G_UNLOCK (egg_desktop_file); + + return retval; +} diff --git a/libeggsmclient/eggdesktopfile.h b/libeggsmclient/eggdesktopfile.h new file mode 100644 index 00000000..f8a3d3e0 --- /dev/null +++ b/libeggsmclient/eggdesktopfile.h @@ -0,0 +1,159 @@ +/* eggdesktopfile.h - Freedesktop.Org Desktop Files + * Copyright (C) 2007 Novell, Inc. + * + * 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 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; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - + * Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __EGG_DESKTOP_FILE_H__ +#define __EGG_DESKTOP_FILE_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct EggDesktopFile EggDesktopFile; + +typedef enum { + EGG_DESKTOP_FILE_TYPE_UNRECOGNIZED, + + EGG_DESKTOP_FILE_TYPE_APPLICATION, + EGG_DESKTOP_FILE_TYPE_LINK, + EGG_DESKTOP_FILE_TYPE_DIRECTORY +} EggDesktopFileType; + +EggDesktopFile *egg_desktop_file_new (const char *desktop_file_path, + GError **error); + +EggDesktopFile *egg_desktop_file_new_from_data_dirs (const char *desktop_file_path, + GError **error); +EggDesktopFile *egg_desktop_file_new_from_dirs (const char *desktop_file_path, + const char **search_dirs, + GError **error); +EggDesktopFile *egg_desktop_file_new_from_key_file (GKeyFile *key_file, + const char *source, + GError **error); + +void egg_desktop_file_free (EggDesktopFile *desktop_file); + +const char *egg_desktop_file_get_source (EggDesktopFile *desktop_file); + +EggDesktopFileType egg_desktop_file_get_desktop_file_type (EggDesktopFile *desktop_file); + +const char *egg_desktop_file_get_name (EggDesktopFile *desktop_file); +const char *egg_desktop_file_get_icon (EggDesktopFile *desktop_file); + +gboolean egg_desktop_file_can_launch (EggDesktopFile *desktop_file, + const char *desktop_environment); + +gboolean egg_desktop_file_accepts_documents (EggDesktopFile *desktop_file); +gboolean egg_desktop_file_accepts_multiple (EggDesktopFile *desktop_file); +gboolean egg_desktop_file_accepts_uris (EggDesktopFile *desktop_file); + +char *egg_desktop_file_parse_exec (EggDesktopFile *desktop_file, + GSList *documents, + GError **error); + +gboolean egg_desktop_file_launch (EggDesktopFile *desktop_file, + GSList *documents, + GError **error, + ...) G_GNUC_NULL_TERMINATED; + +typedef enum { + EGG_DESKTOP_FILE_LAUNCH_CLEARENV = 1, + EGG_DESKTOP_FILE_LAUNCH_PUTENV, + EGG_DESKTOP_FILE_LAUNCH_SCREEN, + EGG_DESKTOP_FILE_LAUNCH_WORKSPACE, + EGG_DESKTOP_FILE_LAUNCH_DIRECTORY, + EGG_DESKTOP_FILE_LAUNCH_TIME, + EGG_DESKTOP_FILE_LAUNCH_FLAGS, + EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC, + EGG_DESKTOP_FILE_LAUNCH_RETURN_PID, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID +} EggDesktopFileLaunchOption; + +/* Standard Keys */ +#define EGG_DESKTOP_FILE_GROUP "Desktop Entry" + +#define EGG_DESKTOP_FILE_KEY_TYPE "Type" +#define EGG_DESKTOP_FILE_KEY_VERSION "Version" +#define EGG_DESKTOP_FILE_KEY_NAME "Name" +#define EGG_DESKTOP_FILE_KEY_GENERIC_NAME "GenericName" +#define EGG_DESKTOP_FILE_KEY_NO_DISPLAY "NoDisplay" +#define EGG_DESKTOP_FILE_KEY_COMMENT "Comment" +#define EGG_DESKTOP_FILE_KEY_ICON "Icon" +#define EGG_DESKTOP_FILE_KEY_HIDDEN "Hidden" +#define EGG_DESKTOP_FILE_KEY_ONLY_SHOW_IN "OnlyShowIn" +#define EGG_DESKTOP_FILE_KEY_NOT_SHOW_IN "NotShowIn" +#define EGG_DESKTOP_FILE_KEY_TRY_EXEC "TryExec" +#define EGG_DESKTOP_FILE_KEY_EXEC "Exec" +#define EGG_DESKTOP_FILE_KEY_PATH "Path" +#define EGG_DESKTOP_FILE_KEY_TERMINAL "Terminal" +#define EGG_DESKTOP_FILE_KEY_MIME_TYPE "MimeType" +#define EGG_DESKTOP_FILE_KEY_CATEGORIES "Categories" +#define EGG_DESKTOP_FILE_KEY_STARTUP_NOTIFY "StartupNotify" +#define EGG_DESKTOP_FILE_KEY_STARTUP_WM_CLASS "StartupWMClass" +#define EGG_DESKTOP_FILE_KEY_URL "URL" + +/* Accessors */ +gboolean egg_desktop_file_has_key (EggDesktopFile *desktop_file, + const char *key, + GError **error); +char *egg_desktop_file_get_string (EggDesktopFile *desktop_file, + const char *key, + GError **error) G_GNUC_MALLOC; +char *egg_desktop_file_get_locale_string (EggDesktopFile *desktop_file, + const char *key, + const char *locale, + GError **error) G_GNUC_MALLOC; +gboolean egg_desktop_file_get_boolean (EggDesktopFile *desktop_file, + const char *key, + GError **error); +double egg_desktop_file_get_numeric (EggDesktopFile *desktop_file, + const char *key, + GError **error); +char **egg_desktop_file_get_string_list (EggDesktopFile *desktop_file, + const char *key, + gsize *length, + GError **error) G_GNUC_MALLOC; +char **egg_desktop_file_get_locale_string_list (EggDesktopFile *desktop_file, + const char *key, + const char *locale, + gsize *length, + GError **error) G_GNUC_MALLOC; + + +/* Errors */ +#define EGG_DESKTOP_FILE_ERROR egg_desktop_file_error_quark() + +GQuark egg_desktop_file_error_quark (void); + +typedef enum { + EGG_DESKTOP_FILE_ERROR_INVALID, + EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE, + EGG_DESKTOP_FILE_ERROR_UNRECOGNIZED_OPTION +} EggDesktopFileError; + +/* Global application desktop file */ +void egg_set_desktop_file (const char *desktop_file_path); +EggDesktopFile *egg_get_desktop_file (void); + + +G_END_DECLS + +#endif /* __EGG_DESKTOP_FILE_H__ */ diff --git a/libeggsmclient/eggsmclient-private.h b/libeggsmclient/eggsmclient-private.h new file mode 100644 index 00000000..ccb10bfc --- /dev/null +++ b/libeggsmclient/eggsmclient-private.h @@ -0,0 +1,53 @@ +/* eggsmclient-private.h + * Copyright (C) 2007 Novell, Inc. + * + * 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 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EGG_SM_CLIENT_PRIVATE_H__ +#define __EGG_SM_CLIENT_PRIVATE_H__ + +#include <gdkconfig.h> +#include "eggsmclient.h" + +G_BEGIN_DECLS + +GKeyFile *egg_sm_client_save_state (EggSMClient *client); +void egg_sm_client_quit_requested (EggSMClient *client); +void egg_sm_client_quit_cancelled (EggSMClient *client); +void egg_sm_client_quit (EggSMClient *client); + +#if defined (GDK_WINDOWING_X11) +# ifdef EGG_SM_CLIENT_BACKEND_XSMP +GType egg_sm_client_xsmp_get_type (void); +EggSMClient *egg_sm_client_xsmp_new (void); +# endif +# ifdef EGG_SM_CLIENT_BACKEND_DBUS +GType egg_sm_client_dbus_get_type (void); +EggSMClient *egg_sm_client_dbus_new (void); +# endif +#elif defined (GDK_WINDOWING_WIN32) +GType egg_sm_client_win32_get_type (void); +EggSMClient *egg_sm_client_win32_new (void); +#elif defined (GDK_WINDOWING_QUARTZ) +GType egg_sm_client_osx_get_type (void); +EggSMClient *egg_sm_client_osx_new (void); +#endif + +G_END_DECLS + + +#endif /* __EGG_SM_CLIENT_PRIVATE_H__ */ diff --git a/libeggsmclient/eggsmclient-xsmp.c b/libeggsmclient/eggsmclient-xsmp.c new file mode 100644 index 00000000..23f40bba --- /dev/null +++ b/libeggsmclient/eggsmclient-xsmp.c @@ -0,0 +1,1370 @@ +/* + * Copyright (C) 2007 Novell, Inc. + * + * Inspired by various other pieces of code including GsmClient (C) + * 2001 Havoc Pennington, MateClient (C) 1998 Carsten Schaar, and twm + * session code (C) 1998 The Open Group. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "eggsmclient.h" +#include "eggsmclient-private.h" + +#include "eggdesktopfile.h" + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <X11/SM/SMlib.h> + +#include <gdk/gdk.h> + +#define EGG_TYPE_SM_CLIENT_XSMP (egg_sm_client_xsmp_get_type ()) +#define EGG_SM_CLIENT_XSMP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_SM_CLIENT_XSMP, EggSMClientXSMP)) +#define EGG_SM_CLIENT_XSMP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_SM_CLIENT_XSMP, EggSMClientXSMPClass)) +#define EGG_IS_SM_CLIENT_XSMP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_SM_CLIENT_XSMP)) +#define EGG_IS_SM_CLIENT_XSMP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_SM_CLIENT_XSMP)) +#define EGG_SM_CLIENT_XSMP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_SM_CLIENT_XSMP, EggSMClientXSMPClass)) + +typedef struct _EggSMClientXSMP EggSMClientXSMP; +typedef struct _EggSMClientXSMPClass EggSMClientXSMPClass; + +/* These mostly correspond to the similarly-named states in section + * 9.1 of the XSMP spec. Some of the states there aren't represented + * here, because we don't need them. SHUTDOWN_CANCELLED is slightly + * different from the spec; we use it when the client is IDLE after a + * ShutdownCancelled message, but the application is still interacting + * and doesn't know the shutdown has been cancelled yet. + */ +typedef enum +{ + XSMP_STATE_IDLE, + XSMP_STATE_SAVE_YOURSELF, + XSMP_STATE_INTERACT_REQUEST, + XSMP_STATE_INTERACT, + XSMP_STATE_SAVE_YOURSELF_DONE, + XSMP_STATE_SHUTDOWN_CANCELLED, + XSMP_STATE_CONNECTION_CLOSED +} EggSMClientXSMPState; + +static const char *state_names[] = { + "idle", + "save-yourself", + "interact-request", + "interact", + "save-yourself-done", + "shutdown-cancelled", + "connection-closed" +}; + +#define EGG_SM_CLIENT_XSMP_STATE(xsmp) (state_names[(xsmp)->state]) + +struct _EggSMClientXSMP +{ + EggSMClient parent; + + SmcConn connection; + char *client_id; + + EggSMClientXSMPState state; + char **restart_command; + gboolean set_restart_command; + int restart_style; + + guint idle; + + /* Current SaveYourself state */ + guint expecting_initial_save_yourself : 1; + guint need_save_state : 1; + guint need_quit_requested : 1; + guint interact_errors : 1; + guint shutting_down : 1; + + /* Todo list */ + guint waiting_to_set_initial_properties : 1; + guint waiting_to_emit_quit : 1; + guint waiting_to_emit_quit_cancelled : 1; + guint waiting_to_save_myself : 1; + +}; + +struct _EggSMClientXSMPClass +{ + EggSMClientClass parent_class; + +}; + +static void sm_client_xsmp_startup (EggSMClient *client, + const char *client_id); +static void sm_client_xsmp_set_restart_command (EggSMClient *client, + int argc, + const char **argv); +static void sm_client_xsmp_will_quit (EggSMClient *client, + gboolean will_quit); +static gboolean sm_client_xsmp_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation); + +static void xsmp_save_yourself (SmcConn smc_conn, + SmPointer client_data, + int save_style, + Bool shutdown, + int interact_style, + Bool fast); +static void xsmp_die (SmcConn smc_conn, + SmPointer client_data); +static void xsmp_save_complete (SmcConn smc_conn, + SmPointer client_data); +static void xsmp_shutdown_cancelled (SmcConn smc_conn, + SmPointer client_data); +static void xsmp_interact (SmcConn smc_conn, + SmPointer client_data); + +static SmProp *array_prop (const char *name, + ...); +static SmProp *ptrarray_prop (const char *name, + GPtrArray *values); +static SmProp *string_prop (const char *name, + const char *value); +static SmProp *card8_prop (const char *name, + unsigned char value); + +static void set_properties (EggSMClientXSMP *xsmp, ...); +static void delete_properties (EggSMClientXSMP *xsmp, ...); + +static GPtrArray *generate_command (char **restart_command, + const char *client_id, + const char *state_file); + +static void save_state (EggSMClientXSMP *xsmp); +static void do_save_yourself (EggSMClientXSMP *xsmp); +static void update_pending_events (EggSMClientXSMP *xsmp); + +static void ice_init (void); +static gboolean process_ice_messages (IceConn ice_conn); +static void smc_error_handler (SmcConn smc_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + SmPointer values); + +G_DEFINE_TYPE (EggSMClientXSMP, egg_sm_client_xsmp, EGG_TYPE_SM_CLIENT) + +static void +egg_sm_client_xsmp_init (EggSMClientXSMP *xsmp) +{ + xsmp->state = XSMP_STATE_CONNECTION_CLOSED; + xsmp->connection = NULL; + xsmp->restart_style = SmRestartIfRunning; +} + +static void +egg_sm_client_xsmp_class_init (EggSMClientXSMPClass *klass) +{ + EggSMClientClass *sm_client_class = EGG_SM_CLIENT_CLASS (klass); + + sm_client_class->startup = sm_client_xsmp_startup; + sm_client_class->set_restart_command = sm_client_xsmp_set_restart_command; + sm_client_class->will_quit = sm_client_xsmp_will_quit; + sm_client_class->end_session = sm_client_xsmp_end_session; +} + +EggSMClient * +egg_sm_client_xsmp_new (void) +{ + if (!g_getenv ("SESSION_MANAGER")) + return NULL; + + return g_object_new (EGG_TYPE_SM_CLIENT_XSMP, NULL); +} + +static gboolean +sm_client_xsmp_set_initial_properties (gpointer user_data) +{ + EggSMClientXSMP *xsmp = user_data; + EggDesktopFile *desktop_file; + GPtrArray *clone, *restart; + char pid_str[64]; + + if (xsmp->idle) + { + g_source_remove (xsmp->idle); + xsmp->idle = 0; + } + xsmp->waiting_to_set_initial_properties = FALSE; + + if (egg_sm_client_get_mode () == EGG_SM_CLIENT_MODE_NO_RESTART) + xsmp->restart_style = SmRestartNever; + + /* Parse info out of desktop file */ + desktop_file = egg_get_desktop_file (); + if (desktop_file) + { + GError *err = NULL; + char *cmdline, **argv; + int argc; + + if (xsmp->restart_style == SmRestartIfRunning) + { + if (egg_desktop_file_get_boolean (desktop_file, + "X-MATE-AutoRestart", NULL)) + xsmp->restart_style = SmRestartImmediately; + } + + if (!xsmp->set_restart_command) + { + cmdline = egg_desktop_file_parse_exec (desktop_file, NULL, &err); + if (cmdline && g_shell_parse_argv (cmdline, &argc, &argv, &err)) + { + egg_sm_client_set_restart_command (EGG_SM_CLIENT (xsmp), + argc, (const char **)argv); + g_strfreev (argv); + } + else + { + g_warning ("Could not parse Exec line in desktop file: %s", + err->message); + g_error_free (err); + } + g_free (cmdline); + } + } + + if (!xsmp->set_restart_command) + xsmp->restart_command = g_strsplit (g_get_prgname (), " ", -1); + + clone = generate_command (xsmp->restart_command, NULL, NULL); + restart = generate_command (xsmp->restart_command, xsmp->client_id, NULL); + + g_debug ("Setting initial properties"); + + /* Program, CloneCommand, RestartCommand, and UserID are required. + * ProcessID isn't required, but the SM may be able to do something + * useful with it. + */ + g_snprintf (pid_str, sizeof (pid_str), "%lu", (gulong) getpid ()); + set_properties (xsmp, + string_prop (SmProgram, g_get_prgname ()), + ptrarray_prop (SmCloneCommand, clone), + ptrarray_prop (SmRestartCommand, restart), + string_prop (SmUserID, g_get_user_name ()), + string_prop (SmProcessID, pid_str), + card8_prop (SmRestartStyleHint, xsmp->restart_style), + NULL); + g_ptr_array_free (clone, TRUE); + g_ptr_array_free (restart, TRUE); + + if (desktop_file) + { + set_properties (xsmp, + string_prop ("_GSM_DesktopFile", egg_desktop_file_get_source (desktop_file)), + NULL); + } + + update_pending_events (xsmp); + return FALSE; +} + +/* This gets called from two different places: xsmp_die() (when the + * server asks us to disconnect) and process_ice_messages() (when the + * server disconnects unexpectedly). + */ +static void +sm_client_xsmp_disconnect (EggSMClientXSMP *xsmp) +{ + SmcConn connection; + + if (!xsmp->connection) + return; + + g_debug ("Disconnecting"); + + connection = xsmp->connection; + xsmp->connection = NULL; + SmcCloseConnection (connection, 0, NULL); + xsmp->state = XSMP_STATE_CONNECTION_CLOSED; + + xsmp->waiting_to_save_myself = FALSE; + update_pending_events (xsmp); +} + +static void +sm_client_xsmp_startup (EggSMClient *client, + const char *client_id) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + SmcCallbacks callbacks; + char *ret_client_id; + char error_string_ret[256]; + + xsmp->client_id = g_strdup (client_id); + + ice_init (); + SmcSetErrorHandler (smc_error_handler); + + callbacks.save_yourself.callback = xsmp_save_yourself; + callbacks.die.callback = xsmp_die; + callbacks.save_complete.callback = xsmp_save_complete; + callbacks.shutdown_cancelled.callback = xsmp_shutdown_cancelled; + + callbacks.save_yourself.client_data = xsmp; + callbacks.die.client_data = xsmp; + callbacks.save_complete.client_data = xsmp; + callbacks.shutdown_cancelled.client_data = xsmp; + + client_id = NULL; + error_string_ret[0] = '\0'; + xsmp->connection = + SmcOpenConnection (NULL, xsmp, SmProtoMajor, SmProtoMinor, + SmcSaveYourselfProcMask | SmcDieProcMask | + SmcSaveCompleteProcMask | + SmcShutdownCancelledProcMask, + &callbacks, + xsmp->client_id, &ret_client_id, + sizeof (error_string_ret), error_string_ret); + + if (!xsmp->connection) + { + g_warning ("Failed to connect to the session manager: %s\n", + error_string_ret[0] ? + error_string_ret : "no error message given"); + xsmp->state = XSMP_STATE_CONNECTION_CLOSED; + return; + } + + /* We expect a pointless initial SaveYourself if either (a) we + * didn't have an initial client ID, or (b) we DID have an initial + * client ID, but the server rejected it and gave us a new one. + */ + if (!xsmp->client_id || + (ret_client_id && strcmp (xsmp->client_id, ret_client_id) != 0)) + xsmp->expecting_initial_save_yourself = TRUE; + + if (ret_client_id) + { + g_free (xsmp->client_id); + xsmp->client_id = g_strdup (ret_client_id); + free (ret_client_id); + + gdk_threads_enter (); + gdk_set_sm_client_id (xsmp->client_id); + gdk_threads_leave (); + + g_debug ("Got client ID \"%s\"", xsmp->client_id); + } + + xsmp->state = XSMP_STATE_IDLE; + + /* Do not set the initial properties until we reach the main loop, + * so that the application has a chance to call + * egg_set_desktop_file(). (This may also help the session manager + * have a better idea of when the application is fully up and + * running.) + */ + xsmp->waiting_to_set_initial_properties = TRUE; + xsmp->idle = g_idle_add (sm_client_xsmp_set_initial_properties, client); +} + +static void +sm_client_xsmp_set_restart_command (EggSMClient *client, + int argc, + const char **argv) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + int i; + + g_strfreev (xsmp->restart_command); + + xsmp->restart_command = g_new (char *, argc + 1); + for (i = 0; i < argc; i++) + xsmp->restart_command[i] = g_strdup (argv[i]); + xsmp->restart_command[i] = NULL; + + xsmp->set_restart_command = TRUE; +} + +static void +sm_client_xsmp_will_quit (EggSMClient *client, + gboolean will_quit) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + + if (xsmp->state == XSMP_STATE_CONNECTION_CLOSED) + { + /* The session manager has already exited! Schedule a quit + * signal. + */ + xsmp->waiting_to_emit_quit = TRUE; + update_pending_events (xsmp); + return; + } + else if (xsmp->state == XSMP_STATE_SHUTDOWN_CANCELLED) + { + /* We received a ShutdownCancelled message while the application + * was interacting; Schedule a quit_cancelled signal. + */ + xsmp->waiting_to_emit_quit_cancelled = TRUE; + update_pending_events (xsmp); + return; + } + + g_return_if_fail (xsmp->state == XSMP_STATE_INTERACT); + + g_debug ("Sending InteractDone(%s)", will_quit ? "False" : "True"); + SmcInteractDone (xsmp->connection, !will_quit); + + if (will_quit && xsmp->need_save_state) + save_state (xsmp); + + g_debug ("Sending SaveYourselfDone(%s)", will_quit ? "True" : "False"); + SmcSaveYourselfDone (xsmp->connection, will_quit); + xsmp->state = XSMP_STATE_SAVE_YOURSELF_DONE; +} + +static gboolean +sm_client_xsmp_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + int save_type; + + /* To end the session via XSMP, we have to send a + * SaveYourselfRequest. We aren't allowed to do that if anything + * else is going on, but we don't want to expose this fact to the + * application. So we do our best to patch things up here... + * + * In the worst case, this method might block for some length of + * time in process_ice_messages, but the only time that code path is + * honestly likely to get hit is if the application tries to end the + * session as the very first thing it does, in which case it + * probably won't actually block anyway. It's not worth gunking up + * the API to try to deal nicely with the other 0.01% of cases where + * this happens. + */ + + while (xsmp->state != XSMP_STATE_IDLE || + xsmp->expecting_initial_save_yourself) + { + /* If we're already shutting down, we don't need to do anything. */ + if (xsmp->shutting_down) + return TRUE; + + switch (xsmp->state) + { + case XSMP_STATE_CONNECTION_CLOSED: + return FALSE; + + case XSMP_STATE_SAVE_YOURSELF: + /* Trying to log out from the save_state callback? Whatever. + * Abort the save_state. + */ + SmcSaveYourselfDone (xsmp->connection, FALSE); + xsmp->state = XSMP_STATE_SAVE_YOURSELF_DONE; + break; + + case XSMP_STATE_INTERACT_REQUEST: + case XSMP_STATE_INTERACT: + case XSMP_STATE_SHUTDOWN_CANCELLED: + /* Already in a shutdown-related state, just ignore + * the new shutdown request... + */ + return TRUE; + + case XSMP_STATE_IDLE: + if (xsmp->waiting_to_set_initial_properties) + sm_client_xsmp_set_initial_properties (xsmp); + + if (!xsmp->expecting_initial_save_yourself) + break; + /* else fall through */ + + case XSMP_STATE_SAVE_YOURSELF_DONE: + /* We need to wait for some response from the server.*/ + process_ice_messages (SmcGetIceConnection (xsmp->connection)); + break; + + default: + /* Hm... shouldn't happen */ + return FALSE; + } + } + + /* xfce4-session will do the wrong thing if we pass SmSaveGlobal and + * the user chooses to save the session. But mate-session will do + * the wrong thing if we pass SmSaveBoth and the user chooses NOT to + * save the session... Sigh. + */ + if (!strcmp (SmcVendor (xsmp->connection), "xfce4-session")) + save_type = SmSaveBoth; + else + save_type = SmSaveGlobal; + + g_debug ("Sending SaveYourselfRequest(SmSaveGlobal, Shutdown, SmInteractStyleAny, %sFast)", request_confirmation ? "!" : ""); + SmcRequestSaveYourself (xsmp->connection, + save_type, + True, /* shutdown */ + SmInteractStyleAny, + !request_confirmation, /* fast */ + True /* global */); + return TRUE; +} + +static gboolean +idle_do_pending_events (gpointer data) +{ + EggSMClientXSMP *xsmp = data; + EggSMClient *client = data; + + gdk_threads_enter (); + + xsmp->idle = 0; + + if (xsmp->waiting_to_emit_quit) + { + xsmp->waiting_to_emit_quit = FALSE; + egg_sm_client_quit (client); + goto out; + } + + if (xsmp->waiting_to_emit_quit_cancelled) + { + xsmp->waiting_to_emit_quit_cancelled = FALSE; + egg_sm_client_quit_cancelled (client); + xsmp->state = XSMP_STATE_IDLE; + } + + if (xsmp->waiting_to_save_myself) + { + xsmp->waiting_to_save_myself = FALSE; + do_save_yourself (xsmp); + } + + out: + gdk_threads_leave (); + return FALSE; +} + +static void +update_pending_events (EggSMClientXSMP *xsmp) +{ + gboolean want_idle = + xsmp->waiting_to_emit_quit || + xsmp->waiting_to_emit_quit_cancelled || + xsmp->waiting_to_save_myself; + + if (want_idle) + { + if (xsmp->idle == 0) + xsmp->idle = g_idle_add (idle_do_pending_events, xsmp); + } + else + { + if (xsmp->idle != 0) + g_source_remove (xsmp->idle); + xsmp->idle = 0; + } +} + +static void +fix_broken_state (EggSMClientXSMP *xsmp, const char *message, + gboolean send_interact_done, + gboolean send_save_yourself_done) +{ + g_warning ("Received XSMP %s message in state %s: client or server error", + message, EGG_SM_CLIENT_XSMP_STATE (xsmp)); + + /* Forget any pending SaveYourself plans we had */ + xsmp->waiting_to_save_myself = FALSE; + update_pending_events (xsmp); + + if (send_interact_done) + SmcInteractDone (xsmp->connection, False); + if (send_save_yourself_done) + SmcSaveYourselfDone (xsmp->connection, True); + + xsmp->state = send_save_yourself_done ? XSMP_STATE_SAVE_YOURSELF_DONE : XSMP_STATE_IDLE; +} + +/* SM callbacks */ + +static void +xsmp_save_yourself (SmcConn smc_conn, + SmPointer client_data, + int save_type, + Bool shutdown, + int interact_style, + Bool fast) +{ + EggSMClientXSMP *xsmp = client_data; + gboolean wants_quit_requested; + + g_debug ("Received SaveYourself(%s, %s, %s, %s) in state %s", + save_type == SmSaveLocal ? "SmSaveLocal" : + save_type == SmSaveGlobal ? "SmSaveGlobal" : "SmSaveBoth", + shutdown ? "Shutdown" : "!Shutdown", + interact_style == SmInteractStyleAny ? "SmInteractStyleAny" : + interact_style == SmInteractStyleErrors ? "SmInteractStyleErrors" : + "SmInteractStyleNone", fast ? "Fast" : "!Fast", + EGG_SM_CLIENT_XSMP_STATE (xsmp)); + + if (xsmp->state != XSMP_STATE_IDLE && + xsmp->state != XSMP_STATE_SHUTDOWN_CANCELLED) + { + fix_broken_state (xsmp, "SaveYourself", FALSE, TRUE); + return; + } + + if (xsmp->waiting_to_set_initial_properties) + sm_client_xsmp_set_initial_properties (xsmp); + + /* If this is the initial SaveYourself, ignore it; we've already set + * properties and there's no reason to actually save state too. + */ + if (xsmp->expecting_initial_save_yourself) + { + xsmp->expecting_initial_save_yourself = FALSE; + + if (save_type == SmSaveLocal && + interact_style == SmInteractStyleNone && + !shutdown && !fast) + { + g_debug ("Sending SaveYourselfDone(True) for initial SaveYourself"); + SmcSaveYourselfDone (xsmp->connection, True); + /* As explained in the comment at the end of + * do_save_yourself(), SAVE_YOURSELF_DONE is the correct + * state here, not IDLE. + */ + xsmp->state = XSMP_STATE_SAVE_YOURSELF_DONE; + return; + } + else + g_warning ("First SaveYourself was not the expected one!"); + } + + /* Even ignoring the "fast" flag completely, there are still 18 + * different combinations of save_type, shutdown and interact_style. + * We interpret them as follows: + * + * Type Shutdown Interact Interpretation + * G F A/E/N do nothing (1) + * G T N do nothing (1)* + * G T A/E quit_requested (2) + * L/B F A/E/N save_state (3) + * L/B T N save_state (3)* + * L/B T A/E quit_requested, then save_state (4) + * + * 1. Do nothing, because the SM asked us to do something + * uninteresting (save open files, but then don't quit + * afterward) or rude (save open files without asking the user + * for confirmation). + * + * 2. Request interaction and then emit ::quit_requested. This + * perhaps isn't quite correct for the SmInteractStyleErrors + * case, but we don't care. + * + * 3. Emit ::save_state. The SmSaveBoth SaveYourselfs in these + * rows essentially get demoted to SmSaveLocal, because their + * Global halves correspond to "do nothing". + * + * 4. Request interaction, emit ::quit_requested, and then emit + * ::save_state after interacting. This is the SmSaveBoth + * equivalent of #2, but we also promote SmSaveLocal shutdown + * SaveYourselfs to SmSaveBoth here, because we want to give + * the user a chance to save open files before quitting. + * + * (* It would be nice if we could do something useful when the + * session manager sends a SaveYourself with shutdown True and + * SmInteractStyleNone. But we can't, so we just pretend it didn't + * even tell us it was shutting down. The docs for ::quit mention + * that it might not always be preceded by ::quit_requested.) + */ + + /* As an optimization, we don't actually request interaction and + * emit ::quit_requested if the application isn't listening to the + * signal. + */ + wants_quit_requested = g_signal_has_handler_pending (xsmp, g_signal_lookup ("quit_requested", EGG_TYPE_SM_CLIENT), 0, FALSE); + + xsmp->need_save_state = (save_type != SmSaveGlobal); + xsmp->need_quit_requested = (shutdown && wants_quit_requested && + interact_style != SmInteractStyleNone); + xsmp->interact_errors = (interact_style == SmInteractStyleErrors); + + xsmp->shutting_down = shutdown; + + do_save_yourself (xsmp); +} + +static void +do_save_yourself (EggSMClientXSMP *xsmp) +{ + if (xsmp->state == XSMP_STATE_SHUTDOWN_CANCELLED) + { + /* The SM cancelled a previous SaveYourself, but we haven't yet + * had a chance to tell the application, so we can't start + * processing this SaveYourself yet. + */ + xsmp->waiting_to_save_myself = TRUE; + update_pending_events (xsmp); + return; + } + + if (xsmp->need_quit_requested) + { + xsmp->state = XSMP_STATE_INTERACT_REQUEST; + + g_debug ("Sending InteractRequest(%s)", + xsmp->interact_errors ? "Error" : "Normal"); + SmcInteractRequest (xsmp->connection, + xsmp->interact_errors ? SmDialogError : SmDialogNormal, + xsmp_interact, + xsmp); + return; + } + + if (xsmp->need_save_state) + { + save_state (xsmp); + + /* Though unlikely, the client could have been disconnected + * while the application was saving its state. + */ + if (!xsmp->connection) + return; + } + + g_debug ("Sending SaveYourselfDone(True)"); + SmcSaveYourselfDone (xsmp->connection, True); + + /* The client state diagram in the XSMP spec says that after a + * non-shutdown SaveYourself, we go directly back to "idle". But + * everything else in both the XSMP spec and the libSM docs + * disagrees. + */ + xsmp->state = XSMP_STATE_SAVE_YOURSELF_DONE; +} + +static void +save_state (EggSMClientXSMP *xsmp) +{ + GKeyFile *state_file; + char *state_file_path, *data; + EggDesktopFile *desktop_file; + GPtrArray *restart; + int offset, fd; + + /* We set xsmp->state before emitting save_state, but our caller is + * responsible for setting it back afterward. + */ + xsmp->state = XSMP_STATE_SAVE_YOURSELF; + + state_file = egg_sm_client_save_state ((EggSMClient *)xsmp); + if (!state_file) + { + restart = generate_command (xsmp->restart_command, xsmp->client_id, NULL); + set_properties (xsmp, + ptrarray_prop (SmRestartCommand, restart), + NULL); + g_ptr_array_free (restart, TRUE); + delete_properties (xsmp, SmDiscardCommand, NULL); + return; + } + + desktop_file = egg_get_desktop_file (); + if (desktop_file) + { + GKeyFile *merged_file; + char *desktop_file_path; + + merged_file = g_key_file_new (); + desktop_file_path = + g_filename_from_uri (egg_desktop_file_get_source (desktop_file), + NULL, NULL); + if (desktop_file_path && + g_key_file_load_from_file (merged_file, desktop_file_path, + G_KEY_FILE_KEEP_COMMENTS | + G_KEY_FILE_KEEP_TRANSLATIONS, NULL)) + { + guint g, k, i; + char **groups, **keys, *value, *exec; + + groups = g_key_file_get_groups (state_file, NULL); + for (g = 0; groups[g]; g++) + { + keys = g_key_file_get_keys (state_file, groups[g], NULL, NULL); + for (k = 0; keys[k]; k++) + { + value = g_key_file_get_value (state_file, groups[g], + keys[k], NULL); + if (value) + { + g_key_file_set_value (merged_file, groups[g], + keys[k], value); + g_free (value); + } + } + g_strfreev (keys); + } + g_strfreev (groups); + + g_key_file_free (state_file); + state_file = merged_file; + + /* Update Exec key using "--sm-client-state-file %k" */ + restart = generate_command (xsmp->restart_command, + NULL, "%k"); + for (i = 0; i < restart->len; i++) + restart->pdata[i] = g_shell_quote (restart->pdata[i]); + g_ptr_array_add (restart, NULL); + exec = g_strjoinv (" ", (char **)restart->pdata); + g_strfreev ((char **)restart->pdata); + g_ptr_array_free (restart, FALSE); + + g_key_file_set_string (state_file, EGG_DESKTOP_FILE_GROUP, + EGG_DESKTOP_FILE_KEY_EXEC, + exec); + g_free (exec); + } + else + desktop_file = NULL; + + g_free (desktop_file_path); + } + + /* Now write state_file to disk. (We can't use mktemp(), because + * that requires the filename to end with "XXXXXX", and we want + * it to end with ".desktop".) + */ + + data = g_key_file_to_data (state_file, NULL, NULL); + g_key_file_free (state_file); + + offset = 0; + while (1) + { + state_file_path = g_strdup_printf ("%s%csession-state%c%s-%ld.%s", + g_get_user_config_dir (), + G_DIR_SEPARATOR, G_DIR_SEPARATOR, + g_get_prgname (), + (long)time (NULL) + offset, + desktop_file ? "desktop" : "state"); + + fd = open (state_file_path, O_WRONLY | O_CREAT | O_EXCL, 0644); + if (fd == -1) + { + if (errno == EEXIST) + { + offset++; + g_free (state_file_path); + continue; + } + else if (errno == ENOTDIR || errno == ENOENT) + { + char *sep = strrchr (state_file_path, G_DIR_SEPARATOR); + + *sep = '\0'; + if (g_mkdir_with_parents (state_file_path, 0755) != 0) + { + g_warning ("Could not create directory '%s'", + state_file_path); + g_free (state_file_path); + state_file_path = NULL; + break; + } + + continue; + } + + g_warning ("Could not create file '%s': %s", + state_file_path, g_strerror (errno)); + g_free (state_file_path); + state_file_path = NULL; + break; + } + + close (fd); + g_file_set_contents (state_file_path, data, -1, NULL); + break; + } + g_free (data); + + restart = generate_command (xsmp->restart_command, xsmp->client_id, + state_file_path); + set_properties (xsmp, + ptrarray_prop (SmRestartCommand, restart), + NULL); + g_ptr_array_free (restart, TRUE); + + if (state_file_path) + { + set_properties (xsmp, + array_prop (SmDiscardCommand, + "/bin/rm", "-rf", state_file_path, + NULL), + NULL); + g_free (state_file_path); + } +} + +static void +xsmp_interact (SmcConn smc_conn, + SmPointer client_data) +{ + EggSMClientXSMP *xsmp = client_data; + EggSMClient *client = client_data; + + g_debug ("Received Interact message in state %s", + EGG_SM_CLIENT_XSMP_STATE (xsmp)); + + if (xsmp->state != XSMP_STATE_INTERACT_REQUEST) + { + fix_broken_state (xsmp, "Interact", TRUE, TRUE); + return; + } + + xsmp->state = XSMP_STATE_INTERACT; + egg_sm_client_quit_requested (client); +} + +static void +xsmp_die (SmcConn smc_conn, + SmPointer client_data) +{ + EggSMClientXSMP *xsmp = client_data; + EggSMClient *client = client_data; + + g_debug ("Received Die message in state %s", + EGG_SM_CLIENT_XSMP_STATE (xsmp)); + + sm_client_xsmp_disconnect (xsmp); + egg_sm_client_quit (client); +} + +static void +xsmp_save_complete (SmcConn smc_conn, + SmPointer client_data) +{ + EggSMClientXSMP *xsmp = client_data; + + g_debug ("Received SaveComplete message in state %s", + EGG_SM_CLIENT_XSMP_STATE (xsmp)); + + if (xsmp->state == XSMP_STATE_SAVE_YOURSELF_DONE) + xsmp->state = XSMP_STATE_IDLE; + else + fix_broken_state (xsmp, "SaveComplete", FALSE, FALSE); +} + +static void +xsmp_shutdown_cancelled (SmcConn smc_conn, + SmPointer client_data) +{ + EggSMClientXSMP *xsmp = client_data; + EggSMClient *client = client_data; + + g_debug ("Received ShutdownCancelled message in state %s", + EGG_SM_CLIENT_XSMP_STATE (xsmp)); + + xsmp->shutting_down = FALSE; + + if (xsmp->state == XSMP_STATE_SAVE_YOURSELF_DONE) + { + /* We've finished interacting and now the SM has agreed to + * cancel the shutdown. + */ + xsmp->state = XSMP_STATE_IDLE; + egg_sm_client_quit_cancelled (client); + } + else if (xsmp->state == XSMP_STATE_SHUTDOWN_CANCELLED) + { + /* Hm... ok, so we got a shutdown SaveYourself, which got + * cancelled, but the application was still interacting, so we + * didn't tell it yet, and then *another* SaveYourself arrived, + * which we must still be waiting to tell the app about, except + * that now that SaveYourself has been cancelled too! Dizzy yet? + */ + xsmp->waiting_to_save_myself = FALSE; + update_pending_events (xsmp); + } + else + { + g_debug ("Sending SaveYourselfDone(False)"); + SmcSaveYourselfDone (xsmp->connection, False); + + if (xsmp->state == XSMP_STATE_INTERACT) + { + /* The application is currently interacting, so we can't + * tell it about the cancellation yet; we will wait until + * after it calls egg_sm_client_will_quit(). + */ + xsmp->state = XSMP_STATE_SHUTDOWN_CANCELLED; + } + else + { + /* The shutdown was cancelled before the application got a + * chance to interact. + */ + xsmp->state = XSMP_STATE_IDLE; + } + } +} + +/* Utilities */ + +/* Create a restart/clone/Exec command based on @restart_command. + * If @client_id is non-%NULL, add "--sm-client-id @client_id". + * If @state_file is non-%NULL, add "--sm-client-state-file @state_file". + * + * None of the input strings are g_strdup()ed; the caller must keep + * them around until it is done with the returned GPtrArray, and must + * then free the array, but not its contents. + */ +static GPtrArray * +generate_command (char **restart_command, const char *client_id, + const char *state_file) +{ + GPtrArray *cmd; + int i; + + cmd = g_ptr_array_new (); + g_ptr_array_add (cmd, restart_command[0]); + + if (client_id) + { + g_ptr_array_add (cmd, "--sm-client-id"); + g_ptr_array_add (cmd, (char *)client_id); + } + + if (state_file) + { + g_ptr_array_add (cmd, "--sm-client-state-file"); + g_ptr_array_add (cmd, (char *)state_file); + } + + for (i = 1; restart_command[i]; i++) + g_ptr_array_add (cmd, restart_command[i]); + + return cmd; +} + +/* Takes a NULL-terminated list of SmProp * values, created by + * array_prop, ptrarray_prop, string_prop, card8_prop, sets them, and + * frees them. + */ +static void +set_properties (EggSMClientXSMP *xsmp, ...) +{ + GPtrArray *props; + SmProp *prop; + va_list ap; + guint i; + + props = g_ptr_array_new (); + + va_start (ap, xsmp); + while ((prop = va_arg (ap, SmProp *))) + g_ptr_array_add (props, prop); + va_end (ap); + + if (xsmp->connection) + { + SmcSetProperties (xsmp->connection, props->len, + (SmProp **)props->pdata); + } + + for (i = 0; i < props->len; i++) + { + prop = props->pdata[i]; + g_free (prop->vals); + g_free (prop); + } + g_ptr_array_free (props, TRUE); +} + +/* Takes a NULL-terminated list of property names and deletes them. */ +static void +delete_properties (EggSMClientXSMP *xsmp, ...) +{ + GPtrArray *props; + char *prop; + va_list ap; + + if (!xsmp->connection) + return; + + props = g_ptr_array_new (); + + va_start (ap, xsmp); + while ((prop = va_arg (ap, char *))) + g_ptr_array_add (props, prop); + va_end (ap); + + SmcDeleteProperties (xsmp->connection, props->len, + (char **)props->pdata); + + g_ptr_array_free (props, TRUE); +} + +/* Takes an array of strings and creates a LISTofARRAY8 property. The + * strings are neither dupped nor freed; they need to remain valid + * until you're done with the SmProp. + */ +static SmProp * +array_prop (const char *name, ...) +{ + SmProp *prop; + SmPropValue pv; + GArray *vals; + char *value; + va_list ap; + + prop = g_new (SmProp, 1); + prop->name = (char *)name; + prop->type = SmLISTofARRAY8; + + vals = g_array_new (FALSE, FALSE, sizeof (SmPropValue)); + + va_start (ap, name); + while ((value = va_arg (ap, char *))) + { + pv.length = strlen (value); + pv.value = value; + g_array_append_val (vals, pv); + } + + prop->num_vals = vals->len; + prop->vals = (SmPropValue *)vals->data; + + g_array_free (vals, FALSE); + + return prop; +} + +/* Takes a GPtrArray of strings and creates a LISTofARRAY8 property. + * The array contents are neither dupped nor freed; they need to + * remain valid until you're done with the SmProp. + */ +static SmProp * +ptrarray_prop (const char *name, GPtrArray *values) +{ + SmProp *prop; + SmPropValue pv; + GArray *vals; + guint i; + + prop = g_new (SmProp, 1); + prop->name = (char *)name; + prop->type = SmLISTofARRAY8; + + vals = g_array_new (FALSE, FALSE, sizeof (SmPropValue)); + + for (i = 0; i < values->len; i++) + { + pv.length = strlen (values->pdata[i]); + pv.value = values->pdata[i]; + g_array_append_val (vals, pv); + } + + prop->num_vals = vals->len; + prop->vals = (SmPropValue *)vals->data; + + g_array_free (vals, FALSE); + + return prop; +} + +/* Takes a string and creates an ARRAY8 property. The string is + * neither dupped nor freed; it needs to remain valid until you're + * done with the SmProp. + */ +static SmProp * +string_prop (const char *name, const char *value) +{ + SmProp *prop; + + prop = g_new (SmProp, 1); + prop->name = (char *)name; + prop->type = SmARRAY8; + + prop->num_vals = 1; + prop->vals = g_new (SmPropValue, 1); + + prop->vals[0].length = strlen (value); + prop->vals[0].value = (char *)value; + + return prop; +} + +/* Takes a char and creates a CARD8 property. */ +static SmProp * +card8_prop (const char *name, unsigned char value) +{ + SmProp *prop; + char *card8val; + + /* To avoid having to allocate and free prop->vals[0], we cheat and + * make vals a 2-element-long array and then use the second element + * to store value. + */ + + prop = g_new (SmProp, 1); + prop->name = (char *)name; + prop->type = SmCARD8; + + prop->num_vals = 1; + prop->vals = g_new (SmPropValue, 2); + card8val = (char *)(&prop->vals[1]); + card8val[0] = value; + + prop->vals[0].length = 1; + prop->vals[0].value = card8val; + + return prop; +} + +/* ICE code. This makes no effort to play nice with anyone else trying + * to use libICE. Fortunately, no one uses libICE for anything other + * than SM. (DCOP uses ICE, but it has its own private copy of + * libICE.) + * + * When this moves to gtk, it will need to be cleverer, to avoid + * tripping over old apps that use MateClient or that use libSM + * directly. + */ + +#include <X11/ICE/ICElib.h> +#include <fcntl.h> + +static void ice_error_handler (IceConn ice_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + IcePointer values); +static void ice_io_error_handler (IceConn ice_conn); +static void ice_connection_watch (IceConn ice_conn, + IcePointer client_data, + Bool opening, + IcePointer *watch_data); + +static void +ice_init (void) +{ + IceSetIOErrorHandler (ice_io_error_handler); + IceSetErrorHandler (ice_error_handler); + IceAddConnectionWatch (ice_connection_watch, NULL); +} + +static gboolean +process_ice_messages (IceConn ice_conn) +{ + IceProcessMessagesStatus status; + + gdk_threads_enter (); + status = IceProcessMessages (ice_conn, NULL, NULL); + gdk_threads_leave (); + + switch (status) + { + case IceProcessMessagesSuccess: + return TRUE; + + case IceProcessMessagesIOError: + sm_client_xsmp_disconnect (IceGetConnectionContext (ice_conn)); + return FALSE; + + case IceProcessMessagesConnectionClosed: + return FALSE; + + default: + g_assert_not_reached (); + } +} + +static gboolean +ice_iochannel_watch (GIOChannel *channel, + GIOCondition condition, + gpointer client_data) +{ + return process_ice_messages (client_data); +} + +static void +ice_connection_watch (IceConn ice_conn, + IcePointer client_data, + Bool opening, + IcePointer *watch_data) +{ + guint watch_id; + + if (opening) + { + GIOChannel *channel; + int fd = IceConnectionNumber (ice_conn); + + fcntl (fd, F_SETFD, fcntl (fd, F_GETFD, 0) | FD_CLOEXEC); + channel = g_io_channel_unix_new (fd); + watch_id = g_io_add_watch (channel, G_IO_IN | G_IO_ERR, + ice_iochannel_watch, ice_conn); + g_io_channel_unref (channel); + + *watch_data = GUINT_TO_POINTER (watch_id); + } + else + { + watch_id = GPOINTER_TO_UINT (*watch_data); + g_source_remove (watch_id); + } +} + +static void +ice_error_handler (IceConn ice_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + IcePointer values) +{ + /* Do nothing */ +} + +static void +ice_io_error_handler (IceConn ice_conn) +{ + /* Do nothing */ +} + +static void +smc_error_handler (SmcConn smc_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + SmPointer values) +{ + /* Do nothing */ +} diff --git a/libeggsmclient/eggsmclient.c b/libeggsmclient/eggsmclient.c new file mode 100644 index 00000000..4b65f283 --- /dev/null +++ b/libeggsmclient/eggsmclient.c @@ -0,0 +1,589 @@ +/* + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "eggsmclient.h" +#include "eggsmclient-private.h" + +static void egg_sm_client_debug_handler (const char *log_domain, + GLogLevelFlags log_level, + const char *message, + gpointer user_data); + +enum { + SAVE_STATE, + QUIT_REQUESTED, + QUIT_CANCELLED, + QUIT, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +struct _EggSMClientPrivate { + GKeyFile *state_file; +}; + +#define EGG_SM_CLIENT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), EGG_TYPE_SM_CLIENT, EggSMClientPrivate)) + +G_DEFINE_TYPE (EggSMClient, egg_sm_client, G_TYPE_OBJECT) + +static EggSMClient *global_client; +static EggSMClientMode global_client_mode = EGG_SM_CLIENT_MODE_NORMAL; + +static void +egg_sm_client_init (EggSMClient *client) +{ + ; +} + +static void +egg_sm_client_class_init (EggSMClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (EggSMClientPrivate)); + + /** + * EggSMClient::save_state: + * @client: the client + * @state_file: a #GKeyFile to save state information into + * + * Emitted when the session manager has requested that the + * application save information about its current state. The + * application should save its state into @state_file, and then the + * session manager may then restart the application in a future + * session and tell it to initialize itself from that state. + * + * You should not save any data into @state_file's "start group" + * (ie, the %NULL group). Instead, applications should save their + * data into groups with names that start with the application name, + * and libraries that connect to this signal should save their data + * into groups with names that start with the library name. + * + * Alternatively, rather than (or in addition to) using @state_file, + * the application can save its state by calling + * egg_sm_client_set_restart_command() during the processing of this + * signal (eg, to include a list of files to open). + **/ + signals[SAVE_STATE] = + g_signal_new ("save_state", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, save_state), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * EggSMClient::quit_requested: + * @client: the client + * + * Emitted when the session manager requests that the application + * exit (generally because the user is logging out). The application + * should decide whether or not it is willing to quit (perhaps after + * asking the user what to do with documents that have unsaved + * changes) and then call egg_sm_client_will_quit(), passing %TRUE + * or %FALSE to give its answer to the session manager. (It does not + * need to give an answer before returning from the signal handler; + * it can interact with the user asynchronously and then give its + * answer later on.) If the application does not connect to this + * signal, then #EggSMClient will automatically return %TRUE on its + * behalf. + * + * The application should not save its session state as part of + * handling this signal; if the user has requested that the session + * be saved when logging out, then ::save_state will be emitted + * separately. + * + * If the application agrees to quit, it should then wait for either + * the ::quit_cancelled or ::quit signals to be emitted. + **/ + signals[QUIT_REQUESTED] = + g_signal_new ("quit_requested", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, quit_requested), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * EggSMClient::quit_cancelled: + * @client: the client + * + * Emitted when the session manager decides to cancel a logout after + * the application has already agreed to quit. After receiving this + * signal, the application can go back to what it was doing before + * receiving the ::quit_requested signal. + **/ + signals[QUIT_CANCELLED] = + g_signal_new ("quit_cancelled", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, quit_cancelled), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * EggSMClient::quit: + * @client: the client + * + * Emitted when the session manager wants the application to quit + * (generally because the user is logging out). The application + * should exit as soon as possible after receiving this signal; if + * it does not, the session manager may choose to forcibly kill it. + * + * Normally a GUI application would only be sent a ::quit if it + * agreed to quit in response to a ::quit_requested signal. However, + * this is not guaranteed; in some situations the session manager + * may decide to end the session without giving applications a + * chance to object. + **/ + signals[QUIT] = + g_signal_new ("quit", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, quit), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + +static gboolean sm_client_disable = FALSE; +static char *sm_client_state_file = NULL; +static char *sm_client_id = NULL; +static char *sm_config_prefix = NULL; + +static gboolean +sm_client_post_parse_func (GOptionContext *context, + GOptionGroup *group, + gpointer data, + GError **error) +{ + EggSMClient *client = egg_sm_client_get (); + + if (sm_client_id == NULL) + { + const gchar *desktop_autostart_id; + + desktop_autostart_id = g_getenv ("DESKTOP_AUTOSTART_ID"); + + if (desktop_autostart_id != NULL) + sm_client_id = g_strdup (desktop_autostart_id); + } + + /* Unset DESKTOP_AUTOSTART_ID in order to avoid child processes to + * use the same client id. */ + g_unsetenv ("DESKTOP_AUTOSTART_ID"); + + if (EGG_SM_CLIENT_GET_CLASS (client)->startup) + EGG_SM_CLIENT_GET_CLASS (client)->startup (client, sm_client_id); + return TRUE; +} + +/** + * egg_sm_client_get_option_group: + * + * Creates a %GOptionGroup containing the session-management-related + * options. You should add this group to the application's + * %GOptionContext if you want to use #EggSMClient. + * + * Return value: the %GOptionGroup + **/ +GOptionGroup * +egg_sm_client_get_option_group (void) +{ + const GOptionEntry entries[] = { + { "sm-client-disable", 0, 0, + G_OPTION_ARG_NONE, &sm_client_disable, + N_("Disable connection to session manager"), NULL }, + { "sm-client-state-file", 0, 0, + G_OPTION_ARG_FILENAME, &sm_client_state_file, + N_("Specify file containing saved configuration"), N_("FILE") }, + { "sm-client-id", 0, 0, + G_OPTION_ARG_STRING, &sm_client_id, + N_("Specify session management ID"), N_("ID") }, + /* MateClient compatibility option */ + { "sm-disable", 0, G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_NONE, &sm_client_disable, + NULL, NULL }, + /* MateClient compatibility option. This is a dummy option that only + * exists so that sessions saved by apps with MateClient can be restored + * later when they've switched to EggSMClient. See bug #575308. + */ + { "sm-config-prefix", 0, G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_STRING, &sm_config_prefix, + NULL, NULL }, + { NULL } + }; + GOptionGroup *group; + + /* Use our own debug handler for the "EggSMClient" domain. */ + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, + egg_sm_client_debug_handler, NULL); + + group = g_option_group_new ("sm-client", + _("Session management options:"), + _("Show session management options"), + NULL, NULL); + g_option_group_add_entries (group, entries); + g_option_group_set_parse_hooks (group, NULL, sm_client_post_parse_func); + + return group; +} + +/** + * egg_sm_client_set_mode: + * @mode: an #EggSMClient mode + * + * Sets the "mode" of #EggSMClient as follows: + * + * %EGG_SM_CLIENT_MODE_DISABLED: Session management is completely + * disabled. The application will not even connect to the session + * manager. (egg_sm_client_get() will still return an #EggSMClient, + * but it will just be a dummy object.) + * + * %EGG_SM_CLIENT_MODE_NO_RESTART: The application will connect to + * the session manager (and thus will receive notification when the + * user is logging out, etc), but will request to not be + * automatically restarted with saved state in future sessions. + * + * %EGG_SM_CLIENT_MODE_NORMAL: The default. #EggSMCLient will + * function normally. + * + * This must be called before the application's main loop begins. + **/ +void +egg_sm_client_set_mode (EggSMClientMode mode) +{ + global_client_mode = mode; +} + +/** + * egg_sm_client_get_mode: + * + * Gets the global #EggSMClientMode. See egg_sm_client_set_mode() + * for details. + * + * Return value: the global #EggSMClientMode + **/ +EggSMClientMode +egg_sm_client_get_mode (void) +{ + return global_client_mode; +} + +/** + * egg_sm_client_get: + * + * Returns the master #EggSMClient for the application. + * + * On platforms that support saved sessions (ie, POSIX/X11), the + * application will only request to be restarted by the session + * manager if you call egg_set_desktop_file() to set an application + * desktop file. In particular, if the desktop file contains the key + * "X + * + * Return value: the master #EggSMClient. + **/ +EggSMClient * +egg_sm_client_get (void) +{ + if (!global_client) + { + if (global_client_mode != EGG_SM_CLIENT_MODE_DISABLED && + !sm_client_disable) + { +#if defined (GDK_WINDOWING_WIN32) + global_client = egg_sm_client_win32_new (); +#elif defined (GDK_WINDOWING_QUARTZ) + global_client = egg_sm_client_osx_new (); +#else + /* If both D-Bus and XSMP are compiled in, try XSMP first + * (since it supports state saving) and fall back to D-Bus + * if XSMP isn't available. + */ +# ifdef EGG_SM_CLIENT_BACKEND_XSMP + global_client = egg_sm_client_xsmp_new (); +# endif +# ifdef EGG_SM_CLIENT_BACKEND_DBUS + if (!global_client) + global_client = egg_sm_client_dbus_new (); +# endif +#endif + } + + /* Fallback: create a dummy client, so that callers don't have + * to worry about a %NULL return value. + */ + if (!global_client) + global_client = g_object_new (EGG_TYPE_SM_CLIENT, NULL); + } + + return global_client; +} + +/** + * egg_sm_client_is_resumed: + * @client: the client + * + * Checks whether or not the current session has been resumed from + * a previous saved session. If so, the application should call + * egg_sm_client_get_state_file() and restore its state from the + * returned #GKeyFile. + * + * Return value: %TRUE if the session has been resumed + **/ +gboolean +egg_sm_client_is_resumed (EggSMClient *client) +{ + g_return_val_if_fail (client == global_client, FALSE); + + return sm_client_state_file != NULL; +} + +/** + * egg_sm_client_get_state_file: + * @client: the client + * + * If the application was resumed by the session manager, this will + * return the #GKeyFile containing its state from the previous + * session. + * + * Note that other libraries and #EggSMClient itself may also store + * state in the key file, so if you call egg_sm_client_get_groups(), + * on it, the return value will likely include groups that you did not + * put there yourself. (It is also not guaranteed that the first + * group created by the application will still be the "start group" + * when it is resumed.) + * + * Return value: the #GKeyFile containing the application's earlier + * state, or %NULL on error. You should not free this key file; it + * is owned by @client. + **/ +GKeyFile * +egg_sm_client_get_state_file (EggSMClient *client) +{ + EggSMClientPrivate *priv = EGG_SM_CLIENT_GET_PRIVATE (client); + char *state_file_path; + GError *err = NULL; + + g_return_val_if_fail (client == global_client, NULL); + + if (!sm_client_state_file) + return NULL; + if (priv->state_file) + return priv->state_file; + + if (!strncmp (sm_client_state_file, "file://", 7)) + state_file_path = g_filename_from_uri (sm_client_state_file, NULL, NULL); + else + state_file_path = g_strdup (sm_client_state_file); + + priv->state_file = g_key_file_new (); + if (!g_key_file_load_from_file (priv->state_file, state_file_path, 0, &err)) + { + g_warning ("Could not load SM state file '%s': %s", + sm_client_state_file, err->message); + g_clear_error (&err); + g_key_file_free (priv->state_file); + priv->state_file = NULL; + } + + g_free (state_file_path); + return priv->state_file; +} + +/** + * egg_sm_client_set_restart_command: + * @client: the client + * @argc: the length of @argv + * @argv: argument vector + * + * Sets the command used to restart @client if it does not have a + * .desktop file that can be used to find its restart command. + * + * This can also be used when handling the ::save_state signal, to + * save the current state via an updated command line. (Eg, providing + * a list of filenames to open when the application is resumed.) + **/ +void +egg_sm_client_set_restart_command (EggSMClient *client, + int argc, + const char **argv) +{ + g_return_if_fail (EGG_IS_SM_CLIENT (client)); + + if (EGG_SM_CLIENT_GET_CLASS (client)->set_restart_command) + EGG_SM_CLIENT_GET_CLASS (client)->set_restart_command (client, argc, argv); +} + +/** + * egg_sm_client_will_quit: + * @client: the client + * @will_quit: whether or not the application is willing to quit + * + * This MUST be called in response to the ::quit_requested signal, to + * indicate whether or not the application is willing to quit. The + * application may call it either directly from the signal handler, or + * at some later point (eg, after asynchronously interacting with the + * user). + * + * If the application does not connect to ::quit_requested, + * #EggSMClient will call this method on its behalf (passing %TRUE + * for @will_quit). + * + * After calling this method, the application should wait to receive + * either ::quit_cancelled or ::quit. + **/ +void +egg_sm_client_will_quit (EggSMClient *client, + gboolean will_quit) +{ + g_return_if_fail (EGG_IS_SM_CLIENT (client)); + + if (EGG_SM_CLIENT_GET_CLASS (client)->will_quit) + EGG_SM_CLIENT_GET_CLASS (client)->will_quit (client, will_quit); +} + +/** + * egg_sm_client_end_session: + * @style: a hint at how to end the session + * @request_confirmation: whether or not the user should get a chance + * to confirm the action + * + * Requests that the session manager end the current session. @style + * indicates how the session should be ended, and + * @request_confirmation indicates whether or not the user should be + * given a chance to confirm the logout/reboot/shutdown. Both of these + * flags are merely hints though; the session manager may choose to + * ignore them. + * + * Return value: %TRUE if the request was sent; %FALSE if it could not + * be (eg, because it could not connect to the session manager). + **/ +gboolean +egg_sm_client_end_session (EggSMClientEndStyle style, + gboolean request_confirmation) +{ + EggSMClient *client = egg_sm_client_get (); + + g_return_val_if_fail (EGG_IS_SM_CLIENT (client), FALSE); + + if (EGG_SM_CLIENT_GET_CLASS (client)->end_session) + { + return EGG_SM_CLIENT_GET_CLASS (client)->end_session (client, style, + request_confirmation); + } + else + return FALSE; +} + +/* Signal-emitting callbacks from platform-specific code */ + +GKeyFile * +egg_sm_client_save_state (EggSMClient *client) +{ + GKeyFile *state_file; + char *group; + + g_return_val_if_fail (client == global_client, NULL); + + state_file = g_key_file_new (); + + g_debug ("Emitting save_state"); + g_signal_emit (client, signals[SAVE_STATE], 0, state_file); + g_debug ("Done emitting save_state"); + + group = g_key_file_get_start_group (state_file); + if (group) + { + g_free (group); + return state_file; + } + else + { + g_key_file_free (state_file); + return NULL; + } +} + +void +egg_sm_client_quit_requested (EggSMClient *client) +{ + g_return_if_fail (client == global_client); + + if (!g_signal_has_handler_pending (client, signals[QUIT_REQUESTED], 0, FALSE)) + { + g_debug ("Not emitting quit_requested because no one is listening"); + egg_sm_client_will_quit (client, TRUE); + return; + } + + g_debug ("Emitting quit_requested"); + g_signal_emit (client, signals[QUIT_REQUESTED], 0); + g_debug ("Done emitting quit_requested"); +} + +void +egg_sm_client_quit_cancelled (EggSMClient *client) +{ + g_return_if_fail (client == global_client); + + g_debug ("Emitting quit_cancelled"); + g_signal_emit (client, signals[QUIT_CANCELLED], 0); + g_debug ("Done emitting quit_cancelled"); +} + +void +egg_sm_client_quit (EggSMClient *client) +{ + g_return_if_fail (client == global_client); + + g_debug ("Emitting quit"); + g_signal_emit (client, signals[QUIT], 0); + g_debug ("Done emitting quit"); + + /* FIXME: should we just call gtk_main_quit() here? */ +} + +static void +egg_sm_client_debug_handler (const char *log_domain, + GLogLevelFlags log_level, + const char *message, + gpointer user_data) +{ + static int debug = -1; + + if (debug < 0) + debug = (g_getenv ("EGG_SM_CLIENT_DEBUG") != NULL); + + if (debug) + g_log_default_handler (log_domain, log_level, message, NULL); +} diff --git a/libeggsmclient/eggsmclient.h b/libeggsmclient/eggsmclient.h new file mode 100644 index 00000000..e620b754 --- /dev/null +++ b/libeggsmclient/eggsmclient.h @@ -0,0 +1,117 @@ +/* eggsmclient.h + * Copyright (C) 2007 Novell, Inc. + * + * 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 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EGG_SM_CLIENT_H__ +#define __EGG_SM_CLIENT_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define EGG_TYPE_SM_CLIENT (egg_sm_client_get_type ()) +#define EGG_SM_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_SM_CLIENT, EggSMClient)) +#define EGG_SM_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_SM_CLIENT, EggSMClientClass)) +#define EGG_IS_SM_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_SM_CLIENT)) +#define EGG_IS_SM_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_SM_CLIENT)) +#define EGG_SM_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_SM_CLIENT, EggSMClientClass)) + +typedef struct _EggSMClient EggSMClient; +typedef struct _EggSMClientClass EggSMClientClass; +typedef struct _EggSMClientPrivate EggSMClientPrivate; + +typedef enum { + EGG_SM_CLIENT_END_SESSION_DEFAULT, + EGG_SM_CLIENT_LOGOUT, + EGG_SM_CLIENT_REBOOT, + EGG_SM_CLIENT_SHUTDOWN +} EggSMClientEndStyle; + +typedef enum { + EGG_SM_CLIENT_MODE_DISABLED, + EGG_SM_CLIENT_MODE_NO_RESTART, + EGG_SM_CLIENT_MODE_NORMAL +} EggSMClientMode; + +struct _EggSMClient +{ + GObject parent; + +}; + +struct _EggSMClientClass +{ + GObjectClass parent_class; + + /* signals */ + void (*save_state) (EggSMClient *client, + GKeyFile *state_file); + + void (*quit_requested) (EggSMClient *client); + void (*quit_cancelled) (EggSMClient *client); + void (*quit) (EggSMClient *client); + + /* virtual methods */ + void (*startup) (EggSMClient *client, + const char *client_id); + void (*set_restart_command) (EggSMClient *client, + int argc, + const char **argv); + void (*will_quit) (EggSMClient *client, + gboolean will_quit); + gboolean (*end_session) (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation); + + /* Padding for future expansion */ + void (*_egg_reserved1) (void); + void (*_egg_reserved2) (void); + void (*_egg_reserved3) (void); + void (*_egg_reserved4) (void); +}; + +GType egg_sm_client_get_type (void) G_GNUC_CONST; + +GOptionGroup *egg_sm_client_get_option_group (void); + +/* Initialization */ +void egg_sm_client_set_mode (EggSMClientMode mode); +EggSMClientMode egg_sm_client_get_mode (void); +EggSMClient *egg_sm_client_get (void); + +/* Resuming a saved session */ +gboolean egg_sm_client_is_resumed (EggSMClient *client); +GKeyFile *egg_sm_client_get_state_file (EggSMClient *client); + +/* Alternate means of saving state */ +void egg_sm_client_set_restart_command (EggSMClient *client, + int argc, + const char **argv); + +/* Handling "quit_requested" signal */ +void egg_sm_client_will_quit (EggSMClient *client, + gboolean will_quit); + +/* Initiate a logout/reboot/shutdown */ +gboolean egg_sm_client_end_session (EggSMClientEndStyle style, + gboolean request_confirmation); + +G_END_DECLS + + +#endif /* __EGG_SM_CLIENT_H__ */ |