From 50f79e6323eac5866b1e9793baa21f5db98aba2d Mon Sep 17 00:00:00 2001 From: Marco Pesenti Gritti Date: Fri, 6 Jun 2008 19:13:10 +0200 Subject: [PATCH] First go at session management. Not asking review since it's a bunch of ugly C code imported from gnome-session. Will clean it up a bit but I don't plan to make it too shiny :) --- src/sugar/Makefile.am | 22 +- src/sugar/_sugarext.defs | 141 +++ src/sugar/_sugarext.override | 5 + src/sugar/activity/activity.py | 13 + src/sugar/app.c | 396 +++++++++ src/sugar/app.h | 70 ++ src/sugar/client-xsmp.c | 828 ++++++++++++++++++ src/sugar/client-xsmp.h | 70 ++ src/sugar/client.c | 251 ++++++ src/sugar/client.h | 111 +++ src/sugar/eggdesktopfile.c | 1437 +++++++++++++++++++++++++++++++ src/sugar/eggdesktopfile.h | 156 ++++ src/sugar/eggsmclient-private.h | 54 ++ src/sugar/eggsmclient-xsmp.c | 1359 +++++++++++++++++++++++++++++ src/sugar/eggsmclient.c | 392 +++++++++ src/sugar/eggsmclient.h | 112 +++ src/sugar/session.c | 550 ++++++++++++ src/sugar/session.h | 93 ++ src/sugar/session.py | 46 + src/sugar/xsmp.c | 537 ++++++++++++ src/sugar/xsmp.h | 29 + 21 files changed, 6670 insertions(+), 2 deletions(-) create mode 100644 src/sugar/app.c create mode 100644 src/sugar/app.h create mode 100644 src/sugar/client-xsmp.c create mode 100644 src/sugar/client-xsmp.h create mode 100644 src/sugar/client.c create mode 100644 src/sugar/client.h create mode 100644 src/sugar/eggdesktopfile.c create mode 100644 src/sugar/eggdesktopfile.h create mode 100644 src/sugar/eggsmclient-private.h create mode 100644 src/sugar/eggsmclient-xsmp.c create mode 100644 src/sugar/eggsmclient.c create mode 100644 src/sugar/eggsmclient.h create mode 100644 src/sugar/session.c create mode 100644 src/sugar/session.h create mode 100644 src/sugar/session.py create mode 100644 src/sugar/xsmp.c create mode 100644 src/sugar/xsmp.h diff --git a/src/sugar/Makefile.am b/src/sugar/Makefile.am index e45ead9b..a0e2e1d8 100644 --- a/src/sugar/Makefile.am +++ b/src/sugar/Makefile.am @@ -5,6 +5,7 @@ sugar_PYTHON = \ env.py \ network.py \ profile.py \ + session.py \ util.py \ wm.py @@ -17,13 +18,27 @@ _sugarext_la_CFLAGS = \ $(PYTHON_INCLUDES) _sugarext_la_LDFLAGS = -module -avoid-version -_sugarext_la_LIBADD = $(EXT_LIBS) +_sugarext_la_LIBADD = $(EXT_LIBS) -lSM -lICE _sugarext_la_SOURCES = \ $(BUILT_SOURCES) \ _sugarextmodule.c \ + app.h \ + app.c \ + client.h \ + client.c \ + client-xsmp.h \ + client-xsmp.c \ eggaccelerators.c \ eggaccelerators.h \ + eggdesktopfile.h \ + eggdesktopfile.c \ + eggsmclient.h \ + eggsmclient.c \ + eggsmclient-private.h \ + eggsmclient-xsmp.c \ + session.h \ + session.c \ sexy-icon-entry.h \ sexy-icon-entry.c \ sugar-address-entry.c \ @@ -33,7 +48,9 @@ _sugarext_la_SOURCES = \ sugar-menu.h \ sugar-menu.c \ sugar-preview.h \ - sugar-preview.c + sugar-preview.c \ + xsmp.h \ + xsmp.c BUILT_SOURCES = \ _sugarext.c \ @@ -45,6 +62,7 @@ _sugarext.c: _sugarext.defs _sugarext.override .defs.c: (cd $(srcdir)\ && $(PYGTK_CODEGEN) \ + --register $(PYGTK_DEFSDIR)/gdk-types.defs \ --register $(PYGTK_DEFSDIR)/gdk-types.defs \ --register $(PYGTK_DEFSDIR)/gtk-types.defs \ --override $*.override \ diff --git a/src/sugar/_sugarext.defs b/src/sugar/_sugarext.defs index 1c9812ef..02b673cf 100644 --- a/src/sugar/_sugarext.defs +++ b/src/sugar/_sugarext.defs @@ -36,6 +36,27 @@ (gtype-id "SEXY_TYPE_ICON_ENTRY") ) +(define-object SMClientXSMP + (in-module "Egg") + (parent "EggSMClient") + (c-name "EggSMClientXSMP") + (gtype-id "EGG_TYPE_SM_CLIENT_XSMP") +) + +(define-object SMClient + (in-module "Egg") + (parent "GObject") + (c-name "EggSMClient") + (gtype-id "EGG_TYPE_SM_CLIENT") +) + +(define-object Session + (in-module "Gsm") + (parent "GObject") + (c-name "GsmSession") + (gtype-id "GSM_TYPE_SESSION") +) + ;; Enumerations and flags ... (define-enum IconEntryPosition @@ -194,3 +215,123 @@ (c-name "sugar_preview_get_pixbuf") (return-type "GdkPixbuf*") ) + +;; From eggsmclient.h + +(define-function egg_sm_client_get_type + (c-name "egg_sm_client_get_type") + (return-type "GType") +) + +(define-function egg_sm_client_get_option_group + (c-name "egg_sm_client_get_option_group") + (return-type "GOptionGroup*") +) + +(define-method is_resumed + (of-object "EggSMClient") + (c-name "egg_sm_client_is_resumed") + (return-type "gboolean") +) + +(define-method get_state_file + (of-object "EggSMClient") + (c-name "egg_sm_client_get_state_file") + (return-type "GKeyFile*") +) + +(define-method set_restart_command + (of-object "EggSMClient") + (c-name "egg_sm_client_set_restart_command") + (return-type "none") + (parameters + '("int" "argc") + '("const-char**" "argv") + ) +) + +(define-method startup + (of-object "EggSMClient") + (c-name "egg_sm_client_startup") + (return-type "none") +) + +(define-method will_quit + (of-object "EggSMClient") + (c-name "egg_sm_client_will_quit") + (return-type "none") + (parameters + '("gboolean" "will_quit") + ) +) + +(define-function egg_sm_client_end_session + (c-name "egg_sm_client_end_session") + (return-type "gboolean") + (parameters + '("EggSMClientEndStyle" "style") + '("gboolean" "request_confirmation") + ) +) + +;; From xsmp.h + +(define-function xsmp_init + (c-name "gsm_xsmp_init") + (return-type "char*") +) + +(define-function xsmp_run + (c-name "gsm_xsmp_run") + (return-type "none") +) + +(define-function xsmp_shutdown + (c-name "gsm_xsmp_shutdown") + (return-type "none") +) + +;; From session.h + +(define-method set_name + (of-object "GsmSession") + (c-name "gsm_session_set_name") + (return-type "none") + (parameters + '("const-char*" "name") + ) +) + +(define-method start + (of-object "GsmSession") + (c-name "gsm_session_start") + (return-type "none") +) + +(define-method get_phase + (of-object "GsmSession") + (c-name "gsm_session_get_phase") + (return-type "GsmSessionPhase") +) + +(define-method initiate_shutdown + (of-object "GsmSession") + (c-name "gsm_session_initiate_shutdown") + (return-type "none") +) + +(define-method register_client + (of-object "GsmSession") + (c-name "gsm_session_register_client") + (return-type "char*") + (parameters + '("GsmClient*" "client") + '("const-char*" "previous_id") + ) +) + +(define-function session_create_global + (c-name "gsm_session_create_global") + (return-type "GsmSession*") +) + diff --git a/src/sugar/_sugarext.override b/src/sugar/_sugarext.override index 1d0289e3..271edcee 100644 --- a/src/sugar/_sugarext.override +++ b/src/sugar/_sugarext.override @@ -9,6 +9,11 @@ headers #include "sugar-menu.h" #include "sugar-preview.h" #include "sexy-icon-entry.h" +#include "session.h" + +#define EGG_SM_CLIENT_BACKEND_XSMP +#include "eggsmclient.h" +#include "eggsmclient-private.h" #include #include diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py index 62043547..3d3b9951 100644 --- a/src/sugar/activity/activity.py +++ b/src/sugar/activity/activity.py @@ -69,6 +69,7 @@ from sugar.graphics.toolcombobox import ToolComboBox from sugar.graphics.alert import Alert from sugar.graphics.icon import Icon from sugar.datastore import datastore +from sugar.session import XSMPClient from sugar import wm from sugar import profile from sugar import _sugarext @@ -435,6 +436,11 @@ class Activity(Window, gtk.Container): self._max_participants = 0 self._invites_queue = [] + self._xsmp_client = XSMPClient() + self._xsmp_client.connect('quit-requested', self.__sm_quit_requested_cb) + self._xsmp_client.connect('quit', self.__sm_quit_cb) + self._xsmp_client.startup() + accel_group = gtk.AccelGroup() self.set_data('sugar-accel-group', accel_group) self.add_accel_group(accel_group) @@ -556,6 +562,13 @@ class Activity(Window, gtk.Container): Window.set_canvas(self, canvas) canvas.connect('map', self.__canvas_map_cb) + def __sm_quit_requested_cb(self, client): + client.will_quit(True) + + def __sm_quit_cb(self, client): + print 'sm quit' + self.close(force=True) + def __canvas_map_cb(self, canvas): if self._jobject and self._jobject.file_path: self.read_file(self._jobject.file_path) diff --git a/src/sugar/app.c b/src/sugar/app.c new file mode 100644 index 00000000..785e52da --- /dev/null +++ b/src/sugar/app.c @@ -0,0 +1,396 @@ +/* app.c + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; 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 +#include +#include + +#include "app.h" + +enum { + EXITED, + REGISTERED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +enum { + PROP_0, + + PROP_DESKTOP_FILE, + PROP_CLIENT_ID, + + LAST_PROP +}; + +static void set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec); +static void get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec); +static void dispose (GObject *object); + +static const char *get_basename (GsmApp *app); +static pid_t launch (GsmApp *app, GError **err); + +G_DEFINE_TYPE (GsmApp, gsm_app, G_TYPE_OBJECT) + +static void +gsm_app_init (GsmApp *app) +{ + app->pid = -1; +} + +static void +gsm_app_class_init (GsmAppClass *app_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (app_class); + + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->dispose = dispose; + + app_class->get_basename = get_basename; + app_class->launch = launch; + + g_object_class_install_property (object_class, + PROP_DESKTOP_FILE, + g_param_spec_string ("desktop-file", + "Desktop file", + "Freedesktop .desktop file", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_CLIENT_ID, + g_param_spec_string ("client-id", + "Client ID", + "Session management client ID", + NULL, + G_PARAM_READWRITE)); + + signals[EXITED] = + g_signal_new ("exited", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmAppClass, exited), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[REGISTERED] = + g_signal_new ("registered", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmAppClass, registered), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + +static void +set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GsmApp *app = GSM_APP (object); + const char *desktop_file; + char *phase; + GError *error = NULL; + + switch (prop_id) + { + case PROP_DESKTOP_FILE: + if (app->desktop_file) + egg_desktop_file_free (app->desktop_file); + desktop_file = g_value_get_string (value); + if (!desktop_file) + { + app->desktop_file = NULL; + break; + } + + app->desktop_file = egg_desktop_file_new (desktop_file, &error); + if (!app->desktop_file) + { + g_warning ("Could not parse desktop file %s: %s", + desktop_file, error->message); + g_error_free (error); + break; + } + + phase = egg_desktop_file_get_string (app->desktop_file, + "X-GNOME-Autostart-Phase", NULL); + if (phase) + { + if (!strcmp (phase, "Initialization")) + app->phase = GSM_SESSION_PHASE_INITIALIZATION; + else if (!strcmp (phase, "WindowManager")) + app->phase = GSM_SESSION_PHASE_WINDOW_MANAGER; + else if (!strcmp (phase, "Panel")) + app->phase = GSM_SESSION_PHASE_PANEL; + else if (!strcmp (phase, "Desktop")) + app->phase = GSM_SESSION_PHASE_DESKTOP; + else + app->phase = GSM_SESSION_PHASE_APPLICATION; + + g_free (phase); + } + else + app->phase = GSM_SESSION_PHASE_APPLICATION; + break; + + case PROP_CLIENT_ID: + g_free (app->client_id); + app->client_id = g_value_dup_string (value); + break; + + default: + break; + } +} + +static void +get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GsmApp *app = GSM_APP (object); + + switch (prop_id) + { + case PROP_DESKTOP_FILE: + if (app->desktop_file) + g_value_set_string (value, egg_desktop_file_get_source (app->desktop_file)); + else + g_value_set_string (value, NULL); + break; + + case PROP_CLIENT_ID: + g_value_set_string (value, app->client_id); + break; + + default: + break; + } +} + +static void +dispose(GObject *object) +{ + GsmApp *app = GSM_APP (object); + + if (app->desktop_file) + { + egg_desktop_file_free (app->desktop_file); + app->desktop_file = NULL; + } + + if (app->startup_id) + { + g_free (app->startup_id); + app->startup_id = NULL; + } + + if (app->client_id) + { + g_free (app->client_id); + app->client_id = NULL; + } +} + +/** + * gsm_app_get_basename: + * @app: a %GsmApp + * + * Returns an identifying name for @app, e.g. the basename of the path to + * @app's desktop file (if any). + * + * Return value: an identifying name for @app, or %NULL. + **/ +const char * +gsm_app_get_basename (GsmApp *app) +{ + return GSM_APP_GET_CLASS (app)->get_basename (app); +} + +static const char * +get_basename (GsmApp *app) +{ + const char *location, *slash; + + if (!app->desktop_file) + return NULL; + + location = egg_desktop_file_get_source (app->desktop_file); + + slash = strrchr (location, '/'); + if (slash) + return slash + 1; + else + return location; +} + +/** + * gsm_app_get_phase: + * @app: a %GsmApp + * + * Returns @app's startup phase. + * + * Return value: @app's startup phase + **/ +GsmSessionPhase +gsm_app_get_phase (GsmApp *app) +{ + g_return_val_if_fail (GSM_IS_APP (app), GSM_SESSION_PHASE_APPLICATION); + + return app->phase; +} + +/** + * gsm_app_is_disabled: + * @app: a %GsmApp + * + * Tests if @app is disabled + * + * Return value: whether or not @app is disabled + **/ +gboolean +gsm_app_is_disabled (GsmApp *app) +{ + g_return_val_if_fail (GSM_IS_APP (app), FALSE); + + if (GSM_APP_GET_CLASS (app)->is_disabled) + return GSM_APP_GET_CLASS (app)->is_disabled (app); + else + return FALSE; +} + +gboolean +gsm_app_provides (GsmApp *app, const char *service) +{ + char **provides; + gsize len, i; + + g_return_val_if_fail (GSM_IS_APP (app), FALSE); + + if (!app->desktop_file) + return FALSE; + + provides = egg_desktop_file_get_string_list (app->desktop_file, + "X-GNOME-Provides", + &len, NULL); + if (!provides) + return FALSE; + + for (i = 0; i < len; i++) + { + if (!strcmp (provides[i], service)) + { + g_strfreev (provides); + return TRUE; + } + } + + g_strfreev (provides); + return FALSE; +} + +static void +app_exited (GPid pid, gint status, gpointer data) +{ + if (WIFEXITED (status)) + g_signal_emit (GSM_APP (data), signals[EXITED], 0); +} + +static pid_t +launch (GsmApp *app, + GError **err) +{ + char *env[2] = { NULL, NULL }; + gboolean success; + + g_return_val_if_fail (app->desktop_file != NULL, (pid_t)-1); + + if (egg_desktop_file_get_boolean (app->desktop_file, + "X-GNOME-Autostart-Notify", NULL) || + egg_desktop_file_get_boolean (app->desktop_file, + "AutostartNotify", NULL)) + env[0] = g_strdup_printf ("DESKTOP_AUTOSTART_ID=%s", app->client_id); + +#if 0 + g_debug ("launching %s with client_id %s\n", + gsm_app_get_basename (app), app->client_id); +#endif + + success = + egg_desktop_file_launch (app->desktop_file, NULL, err, + EGG_DESKTOP_FILE_LAUNCH_PUTENV, env, + EGG_DESKTOP_FILE_LAUNCH_FLAGS, G_SPAWN_DO_NOT_REAP_CHILD, + EGG_DESKTOP_FILE_LAUNCH_RETURN_PID, &app->pid, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID, &app->startup_id, + NULL); + + g_free (env[0]); + + if (success) + { + /* In case the app belongs to Initialization phase, we monitor + * if it exits to emit proper "exited" signal to session. */ + if (app->phase == GSM_SESSION_PHASE_INITIALIZATION) + g_child_watch_add ((GPid) app->pid, app_exited, app); + + return app->pid; + } + else + return (pid_t) -1; +} + +/** + * gsm_app_launch: + * @app: a %GsmApp + * @err: an error pointer + * + * Launches @app + * + * Return value: the pid of the new process, or -1 on error + **/ +pid_t +gsm_app_launch (GsmApp *app, GError **err) +{ + return GSM_APP_GET_CLASS (app)->launch (app, err); +} + +/** + * gsm_app_registered: + * @app: a %GsmApp + * + * Emits "registered" signal in @app + **/ +void +gsm_app_registered (GsmApp *app) +{ + g_return_if_fail (GSM_IS_APP (app)); + + g_signal_emit (app, signals[REGISTERED], 0); +} + diff --git a/src/sugar/app.h b/src/sugar/app.h new file mode 100644 index 00000000..be3672c6 --- /dev/null +++ b/src/sugar/app.h @@ -0,0 +1,70 @@ +/* gsmapp.h + * Copyright (C) 2006 Novell, Inc. + * + */ + +#ifndef __GSM_APP_H__ +#define __GSM_APP_H__ + +#include +#include + +#include "eggdesktopfile.h" +#include "session.h" + +G_BEGIN_DECLS + +#define GSM_TYPE_APP (gsm_app_get_type ()) +#define GSM_APP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSM_TYPE_APP, GsmApp)) +#define GSM_APP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSM_TYPE_APP, GsmAppClass)) +#define GSM_IS_APP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSM_TYPE_APP)) +#define GSM_IS_APP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSM_TYPE_APP)) +#define GSM_APP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSM_TYPE_APP, GsmAppClass)) + +typedef struct _GsmApp GsmApp; +typedef struct _GsmAppClass GsmAppClass; +typedef struct _GsmAppPrivate GsmAppPrivate; + +struct _GsmApp +{ + GObject parent; + + EggDesktopFile *desktop_file; + GsmSessionPhase phase; + + pid_t pid; + char *startup_id, *client_id; +}; + +struct _GsmAppClass +{ + GObjectClass parent_class; + + /* signals */ + void (*exited) (GsmApp *app, int status); + void (*registered) (GsmApp *app); + + /* virtual methods */ + const char *(*get_basename) (GsmApp *app); + gboolean (*is_disabled) (GsmApp *app); + pid_t (*launch) (GsmApp *app, GError **err); + void (*set_client) (GsmApp *app, GsmClient *client); +}; + +GType gsm_app_get_type (void) G_GNUC_CONST; + +const char *gsm_app_get_basename (GsmApp *app); +GsmSessionPhase gsm_app_get_phase (GsmApp *app); +gboolean gsm_app_provides (GsmApp *app, + const char *service); +gboolean gsm_app_is_disabled (GsmApp *app); +pid_t gsm_app_launch (GsmApp *app, + GError **err); +void gsm_app_set_client (GsmApp *app, + GsmClient *client); + +void gsm_app_registered (GsmApp *app); + +G_END_DECLS + +#endif /* __GSM_APP_H__ */ diff --git a/src/sugar/client-xsmp.c b/src/sugar/client-xsmp.c new file mode 100644 index 00000000..d4226839 --- /dev/null +++ b/src/sugar/client-xsmp.c @@ -0,0 +1,828 @@ +/* client-xsmp.c + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; 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 +#include +#include +#include +#include + +#include "client-xsmp.h" +#include "session.h" + +/* FIXME */ +#define GsmDesktopFile "_Gsm_DesktopFile" + +static gboolean client_iochannel_watch (GIOChannel *channel, + GIOCondition condition, + gpointer data); +static gboolean client_protocol_timeout (gpointer data); + +static void set_description (GsmClientXSMP *xsmp); + +static const char *xsmp_get_client_id (GsmClient *client); +static pid_t xsmp_get_pid (GsmClient *client); +static char *xsmp_get_desktop_file (GsmClient *client); +static char *xsmp_get_restart_command (GsmClient *client); +static char *xsmp_get_discard_command (GsmClient *client); +static gboolean xsmp_get_autorestart (GsmClient *client); + +static void xsmp_finalize (GObject *object); +static void xsmp_restart (GsmClient *client, + GError **error); +static void xsmp_save_yourself (GsmClient *client, + gboolean save_state); +static void xsmp_save_yourself_phase2 (GsmClient *client); +static void xsmp_interact (GsmClient *client); +static void xsmp_shutdown_cancelled (GsmClient *client); +static void xsmp_die (GsmClient *client); + +G_DEFINE_TYPE (GsmClientXSMP, gsm_client_xsmp, GSM_TYPE_CLIENT) + +static void +gsm_client_xsmp_init (GsmClientXSMP *xsmp) +{ + ; +} + +static void +gsm_client_xsmp_class_init (GsmClientXSMPClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsmClientClass *client_class = GSM_CLIENT_CLASS (klass); + + object_class->finalize = xsmp_finalize; + + client_class->get_client_id = xsmp_get_client_id; + client_class->get_pid = xsmp_get_pid; + client_class->get_desktop_file = xsmp_get_desktop_file; + client_class->get_restart_command = xsmp_get_restart_command; + client_class->get_discard_command = xsmp_get_discard_command; + client_class->get_autorestart = xsmp_get_autorestart; + + client_class->restart = xsmp_restart; + client_class->save_yourself = xsmp_save_yourself; + client_class->save_yourself_phase2 = xsmp_save_yourself_phase2; + client_class->interact = xsmp_interact; + client_class->shutdown_cancelled = xsmp_shutdown_cancelled; + client_class->die = xsmp_die; +} + +GsmClientXSMP * +gsm_client_xsmp_new (IceConn ice_conn) +{ + GsmClientXSMP *xsmp; + GIOChannel *channel; + int fd; + + xsmp = g_object_new (GSM_TYPE_CLIENT_XSMP, NULL); + xsmp->props = g_ptr_array_new (); + + xsmp->ice_conn = ice_conn; + xsmp->current_save_yourself = -1; + xsmp->next_save_yourself = -1; + + fd = IceConnectionNumber (ice_conn); + fcntl (fd, F_SETFD, fcntl (fd, F_GETFD, 0) | FD_CLOEXEC); + + channel = g_io_channel_unix_new (fd); + xsmp->watch_id = g_io_add_watch (channel, G_IO_IN | G_IO_ERR, + client_iochannel_watch, xsmp); + g_io_channel_unref (channel); + + xsmp->protocol_timeout = g_timeout_add (5000, client_protocol_timeout, xsmp); + + set_description (xsmp); + g_debug ("New client '%s'", xsmp->description); + + return xsmp; +} + +static void +xsmp_finalize (GObject *object) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) object; + + g_debug ("xsmp_finalize (%s)", xsmp->description); + + if (xsmp->watch_id) + g_source_remove (xsmp->watch_id); + + if (xsmp->conn) + SmsCleanUp (xsmp->conn); + else + IceCloseConnection (xsmp->ice_conn); + + if (xsmp->protocol_timeout) + g_source_remove (xsmp->protocol_timeout); + + G_OBJECT_CLASS (gsm_client_xsmp_parent_class)->finalize (object); +} + +static gboolean +client_iochannel_watch (GIOChannel *channel, + GIOCondition condition, + gpointer data) +{ + GsmClient *client = data; + GsmClientXSMP *xsmp = data; + + switch (IceProcessMessages (xsmp->ice_conn, NULL, NULL)) + { + case IceProcessMessagesSuccess: + return TRUE; + + case IceProcessMessagesIOError: + g_debug ("IceProcessMessagesIOError on '%s'", xsmp->description); + gsm_client_disconnected (client); + return FALSE; + + case IceProcessMessagesConnectionClosed: + g_debug ("IceProcessMessagesConnectionClosed on '%s'", + xsmp->description); + return FALSE; + + default: + g_assert_not_reached (); + } +} + +/* Called if too much time passes between the initial connection and + * the XSMP protocol setup. + */ +static gboolean +client_protocol_timeout (gpointer data) +{ + GsmClient *client = data; + GsmClientXSMP *xsmp = data; + + g_debug ("client_protocol_timeout for client '%s' in ICE status %d", + xsmp->description, IceConnectionStatus (xsmp->ice_conn)); + gsm_client_disconnected (client); + + return FALSE; +} + +static Status +register_client_callback (SmsConn conn, + SmPointer manager_data, + char *previous_id) +{ + GsmClient *client = manager_data; + GsmClientXSMP *xsmp = manager_data; + char *id; + + g_debug ("Client '%s' received RegisterClient(%s)", + xsmp->description, + previous_id ? previous_id : "NULL"); + + id = gsm_session_register_client (global_session, client, previous_id); + + if (id == NULL) + { + g_debug (" rejected: invalid previous_id"); + free (previous_id); + return FALSE; + } + + xsmp->id = id; + + set_description (xsmp); + + g_debug ("Sending RegisterClientReply to '%s'", xsmp->description); + + SmsRegisterClientReply (conn, xsmp->id); + + if (!previous_id) + { + /* Send the initial SaveYourself. */ + g_debug ("Sending initial SaveYourself"); + SmsSaveYourself (conn, SmSaveLocal, False, SmInteractStyleNone, False); + xsmp->current_save_yourself = SmSaveLocal; + + free (previous_id); + } + + return TRUE; +} + +static void +do_save_yourself (GsmClientXSMP *xsmp, int save_type) +{ + if (xsmp->next_save_yourself != -1) + { + /* Either we're currently doing a shutdown and there's a checkpoint + * queued after it, or vice versa. Either way, the new SaveYourself + * is redundant. + */ + g_debug (" skipping redundant SaveYourself for '%s'", + xsmp->description); + } + else if (xsmp->current_save_yourself != -1) + { + g_debug (" queuing new SaveYourself for '%s'", + xsmp->description); + xsmp->next_save_yourself = save_type; + } + else + { + xsmp->current_save_yourself = save_type; + + switch (save_type) + { + case SmSaveLocal: + /* Save state */ + SmsSaveYourself (xsmp->conn, SmSaveLocal, FALSE, + SmInteractStyleNone, FALSE); + break; + + default: + /* Logout */ + SmsSaveYourself (xsmp->conn, save_type, TRUE, + SmInteractStyleAny, FALSE); + break; + } + } +} + +static void +save_yourself_request_callback (SmsConn conn, + SmPointer manager_data, + int save_type, + Bool shutdown, + int interact_style, + Bool fast, + Bool global) +{ + GsmClientXSMP *xsmp = manager_data; + + g_debug ("Client '%s' received SaveYourselfRequest(%s, %s, %s, %s, %s)", + xsmp->description, + save_type == SmSaveLocal ? "SmSaveLocal" : + save_type == SmSaveGlobal ? "SmSaveGlobal" : "SmSaveBoth", + shutdown ? "Shutdown" : "!Shutdown", + interact_style == SmInteractStyleAny ? "SmInteractStyleAny" : + interact_style == SmInteractStyleErrors ? "SmInteractStyleErrors" : + "SmInteractStyleNone", fast ? "Fast" : "!Fast", + global ? "Global" : "!Global"); + + /* Examining the g_debug above, you can see that there are a total + * of 72 different combinations of options that this could have been + * called with. However, most of them are stupid. + * + * If @shutdown and @global are both TRUE, that means the caller is + * requesting that a logout message be sent to all clients, so we do + * that. We use @fast to decide whether or not to show a + * confirmation dialog. (This isn't really what @fast is for, but + * the old gnome-session and ksmserver both interpret it that way, + * so we do too.) We ignore @save_type because we pick the correct + * save_type ourselves later based on user prefs, dialog choices, + * etc, and we ignore @interact_style, because clients have not used + * it correctly consistently enough to make it worth honoring. + * + * If @shutdown is TRUE and @global is FALSE, the caller is + * confused, so we ignore the request. + * + * If @shutdown is FALSE and @save_type is SmSaveGlobal or + * SmSaveBoth, then the client wants us to ask some or all open + * applications to save open files to disk, but NOT quit. This is + * silly and so we ignore the request. + * + * If @shutdown is FALSE and @save_type is SmSaveLocal, then the + * client wants us to ask some or all open applications to update + * their current saved state, but not log out. At the moment, the + * code only supports this for the !global case (ie, a client + * requesting that it be allowed to update *its own* saved state, + * but not having everyone else update their saved state). + */ + + if (shutdown && global) + { + g_debug (" initiating shutdown"); +/* gsm_session_initiate_shutdown (global_session, + !fast, + GSM_SESSION_LOGOUT_TYPE_LOGOUT); +*/ + } + else if (!shutdown && !global) + { + g_debug (" initiating checkpoint"); + do_save_yourself (xsmp, SmSaveLocal); + } + else + g_debug (" ignoring"); +} + +static void +xsmp_restart (GsmClient *client, GError **error) +{ + char *restart_cmd = gsm_client_get_restart_command (client); + + g_spawn_command_line_async (restart_cmd, error); + + g_free (restart_cmd); +} + +static void +xsmp_save_yourself (GsmClient *client, gboolean save_state) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *)client; + + g_debug ("xsmp_save_yourself ('%s', %s)", xsmp->description, + save_state ? "True" : "False"); + + do_save_yourself (xsmp, save_state ? SmSaveBoth : SmSaveGlobal); +} + +static void +save_yourself_phase2_request_callback (SmsConn conn, + SmPointer manager_data) +{ + GsmClient *client = manager_data; + GsmClientXSMP *xsmp = manager_data; + + g_debug ("Client '%s' received SaveYourselfPhase2Request", + xsmp->description); + + if (xsmp->current_save_yourself == SmSaveLocal) + { + /* WTF? Anyway, if it's checkpointing, it doesn't have to wait + * for anyone else. + */ + SmsSaveYourselfPhase2 (xsmp->conn); + } + else + gsm_client_request_phase2 (client); +} + +static void +xsmp_save_yourself_phase2 (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *)client; + + g_debug ("xsmp_save_yourself_phase2 ('%s')", xsmp->description); + + SmsSaveYourselfPhase2 (xsmp->conn); +} + +static void +interact_request_callback (SmsConn conn, + SmPointer manager_data, + int dialog_type) +{ + GsmClient *client = manager_data; + GsmClientXSMP *xsmp = manager_data; + + g_debug ("Client '%s' received InteractRequest(%s)", xsmp->description, + dialog_type == SmInteractStyleAny ? "Any" : "Errors"); + + gsm_client_request_interaction (client); +} + +static void +xsmp_interact (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + + g_debug ("xsmp_interact ('%s')", xsmp->description); + + SmsInteract (xsmp->conn); +} + +static void +interact_done_callback (SmsConn conn, + SmPointer manager_data, + Bool cancel_shutdown) +{ + GsmClient *client = manager_data; + GsmClientXSMP *xsmp = manager_data; + + g_debug ("Client '%s' received InteractDone(cancel_shutdown = %s)", + xsmp->description, cancel_shutdown ? "True" : "False"); + + gsm_client_interaction_done (client, cancel_shutdown); +} + +static void +xsmp_shutdown_cancelled (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + + g_debug ("xsmp_shutdown_cancelled ('%s')", xsmp->description); + + SmsShutdownCancelled (xsmp->conn); +} + +static void +xsmp_die (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + + g_debug ("xsmp_die ('%s')", xsmp->description); + + SmsDie (xsmp->conn); +} + +static void +save_yourself_done_callback (SmsConn conn, + SmPointer manager_data, + Bool success) +{ + GsmClient *client = manager_data; + GsmClientXSMP *xsmp = manager_data; + + g_debug ("Client '%s' received SaveYourselfDone(success = %s)", + xsmp->description, success ? "True" : "False"); + + if (xsmp->current_save_yourself == SmSaveLocal) + { + xsmp->current_save_yourself = -1; + SmsSaveComplete (xsmp->conn); + gsm_client_saved_state (client); + } + else + { + xsmp->current_save_yourself = -1; + gsm_client_save_yourself_done (client); + } + + if (xsmp->next_save_yourself) + { + int save_type = xsmp->next_save_yourself; + + xsmp->next_save_yourself = -1; + do_save_yourself (xsmp, save_type); + } +} + +static void +close_connection_callback (SmsConn conn, + SmPointer manager_data, + int count, + char **reason_msgs) +{ + GsmClient *client = manager_data; + GsmClientXSMP *xsmp = manager_data; + int i; + + g_debug ("Client '%s' received CloseConnection", xsmp->description); + for (i = 0; i < count; i++) + g_debug (" close reason: '%s'", reason_msgs[i]); + SmFreeReasons (count, reason_msgs); + + gsm_client_disconnected (client); +} + +static void +debug_print_property (SmProp *prop) +{ + GString *tmp; + int i; + + switch (prop->type[0]) + { + case 'C': /* CARD8 */ + g_debug (" %s = %d", prop->name, *(unsigned char *)prop->vals[0].value); + break; + + case 'A': /* ARRAY8 */ + g_debug (" %s = '%s'", prop->name, (char *)prop->vals[0].value); + break; + + case 'L': /* LISTofARRAY8 */ + tmp = g_string_new (NULL); + for (i = 0; i < prop->num_vals; i++) + { + g_string_append_printf (tmp, "'%.*s' ", prop->vals[i].length, + (char *)prop->vals[i].value); + } + g_debug (" %s = %s", prop->name, tmp->str); + g_string_free (tmp, TRUE); + break; + + default: + g_debug (" %s = ??? (%s)", prop->name, prop->type); + break; + } +} + +static SmProp * +find_property (GsmClientXSMP *client, const char *name, int *index) +{ + SmProp *prop; + int i; + + for (i = 0; i < client->props->len; i++) + { + prop = client->props->pdata[i]; + + if (!strcmp (prop->name, name)) + { + if (index) + *index = i; + return prop; + } + } + + return NULL; +} + +static void +delete_property (GsmClientXSMP *client, const char *name) +{ + int index; + SmProp *prop; + + prop = find_property (client, name, &index); + if (!prop) + return; + +#if 0 + /* This is wrong anyway; we can't unconditionally run the current + * discard command; if this client corresponds to a GsmAppResumed, + * and the current discard command is identical to the app's + * discard_command, then we don't run the discard command now, + * because that would delete a saved state we may want to resume + * again later. + */ + if (!strcmp (name, SmDiscardCommand)) + gsm_client_run_discard (client); +#endif + + g_ptr_array_remove_index_fast (client->props, index); + SmFreeProperty (prop); +} + +static void +set_properties_callback (SmsConn conn, + SmPointer manager_data, + int num_props, + SmProp **props) +{ + GsmClientXSMP *client = manager_data; + int i; + + g_debug ("Set properties from client '%s'", client->description); + + for (i = 0; i < num_props; i++) + { + delete_property (client, props[i]->name); + g_ptr_array_add (client->props, props[i]); + + debug_print_property (props[i]); + + if (!strcmp (props[i]->name, SmProgram)) + set_description (client); + } + + free (props); + +} + +static void +delete_properties_callback (SmsConn conn, + SmPointer manager_data, + int num_props, + char **prop_names) +{ + GsmClientXSMP *client = manager_data; + int i; + + g_debug ("Delete properties from '%s'", client->description); + + for (i = 0; i < num_props; i++) + { + delete_property (client, prop_names[i]); + + g_debug (" %s", prop_names[i]); + } + + free (prop_names); +} + +static void +get_properties_callback (SmsConn conn, + SmPointer manager_data) +{ + GsmClientXSMP *client = manager_data; + + g_debug ("Get properties request from '%s'", client->description); + + SmsReturnProperties (conn, client->props->len, + (SmProp **)client->props->pdata); +} + +static const char * +xsmp_get_client_id (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + + return xsmp->id; +} + +static pid_t +xsmp_get_pid (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + SmProp *prop = find_property (xsmp, SmProcessID, NULL); + char buf[32]; + + if (!prop || strcmp (prop->type, SmARRAY8) != 0) + return (pid_t)-1; + + /* prop->vals[0].value might not be '\0'-terminated... */ + g_strlcpy (buf, prop->vals[0].value, MIN (prop->vals[0].length, sizeof (buf))); + return (pid_t)strtoul (buf, NULL, 10); +} + +static char * +xsmp_get_desktop_file (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + SmProp *prop = find_property (xsmp, GsmDesktopFile, NULL); + + if (!prop || strcmp (prop->type, SmARRAY8) != 0) + return NULL; + + return g_strndup (prop->vals[0].value, prop->vals[0].length); +} + +static char * +prop_to_command (SmProp *prop) +{ + GString *str; + int i, j; + gboolean need_quotes; + + str = g_string_new (NULL); + for (i = 0; i < prop->num_vals; i++) + { + char *val = prop->vals[i].value; + + need_quotes = FALSE; + for (j = 0; j < prop->vals[i].length; j++) + { + if (!g_ascii_isalnum (val[j]) && !strchr ("-_=:./", val[j])) + { + need_quotes = TRUE; + break; + } + } + + if (i > 0) + g_string_append_c (str, ' '); + + if (!need_quotes) + { + g_string_append_printf (str, "%.*s", prop->vals[i].length, + (char *)prop->vals[i].value); + } + else + { + g_string_append_c (str, '\''); + while (val < (char *)prop->vals[i].value + prop->vals[i].length) + { + if (*val == '\'') + g_string_append (str, "'\''"); + else + g_string_append_c (str, *val); + val++; + } + g_string_append_c (str, '\''); + } + } + + return g_string_free (str, FALSE); +} + +static char * +xsmp_get_restart_command (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + SmProp *prop = find_property (xsmp, SmRestartCommand, NULL); + + if (!prop || strcmp (prop->type, SmLISTofARRAY8) != 0) + return NULL; + + return prop_to_command (prop); +} + +static char * +xsmp_get_discard_command (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + SmProp *prop = find_property (xsmp, SmDiscardCommand, NULL); + + if (!prop || strcmp (prop->type, SmLISTofARRAY8) != 0) + return NULL; + + return prop_to_command (prop); +} + +static gboolean +xsmp_get_autorestart (GsmClient *client) +{ + GsmClientXSMP *xsmp = (GsmClientXSMP *) client; + SmProp *prop = find_property (xsmp, SmRestartStyleHint, NULL); + + if (!prop || strcmp (prop->type, SmCARD8) != 0) + return FALSE; + + return ((unsigned char *)prop->vals[0].value)[0] == SmRestartImmediately; +} + +static void +set_description (GsmClientXSMP *client) +{ + SmProp *prop = find_property (client, SmProgram, NULL); + + g_free (client->description); + if (prop) + { + client->description = g_strdup_printf ("%p [%.*s %s]", client, + prop->vals[0].length, + (char *)prop->vals[0].value, + client->id); + } + else if (client->id) + client->description = g_strdup_printf ("%p [%s]", client, client->id); + else + client->description = g_strdup_printf ("%p", client); +} + +void +gsm_client_xsmp_connect (GsmClientXSMP *client, SmsConn conn, + unsigned long *mask_ret, SmsCallbacks *callbacks_ret) +{ + client->conn = conn; + + if (client->protocol_timeout) + { + g_source_remove (client->protocol_timeout); + client->protocol_timeout = 0; + } + + g_debug ("Initializing client %s", client->description); + + *mask_ret = 0; + + *mask_ret |= SmsRegisterClientProcMask; + callbacks_ret->register_client.callback = register_client_callback; + callbacks_ret->register_client.manager_data = client; + + *mask_ret |= SmsInteractRequestProcMask; + callbacks_ret->interact_request.callback = interact_request_callback; + callbacks_ret->interact_request.manager_data = client; + + *mask_ret |= SmsInteractDoneProcMask; + callbacks_ret->interact_done.callback = interact_done_callback; + callbacks_ret->interact_done.manager_data = client; + + *mask_ret |= SmsSaveYourselfRequestProcMask; + callbacks_ret->save_yourself_request.callback = save_yourself_request_callback; + callbacks_ret->save_yourself_request.manager_data = client; + + *mask_ret |= SmsSaveYourselfP2RequestProcMask; + callbacks_ret->save_yourself_phase2_request.callback = save_yourself_phase2_request_callback; + callbacks_ret->save_yourself_phase2_request.manager_data = client; + + *mask_ret |= SmsSaveYourselfDoneProcMask; + callbacks_ret->save_yourself_done.callback = save_yourself_done_callback; + callbacks_ret->save_yourself_done.manager_data = client; + + *mask_ret |= SmsCloseConnectionProcMask; + callbacks_ret->close_connection.callback = close_connection_callback; + callbacks_ret->close_connection.manager_data = client; + + *mask_ret |= SmsSetPropertiesProcMask; + callbacks_ret->set_properties.callback = set_properties_callback; + callbacks_ret->set_properties.manager_data = client; + + *mask_ret |= SmsDeletePropertiesProcMask; + callbacks_ret->delete_properties.callback = delete_properties_callback; + callbacks_ret->delete_properties.manager_data = client; + + *mask_ret |= SmsGetPropertiesProcMask; + callbacks_ret->get_properties.callback = get_properties_callback; + callbacks_ret->get_properties.manager_data = client; +} diff --git a/src/sugar/client-xsmp.h b/src/sugar/client-xsmp.h new file mode 100644 index 00000000..c744ee13 --- /dev/null +++ b/src/sugar/client-xsmp.h @@ -0,0 +1,70 @@ +/* client-xsmp.h + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef __GSM_CLIENT_XSMP_H__ +#define __GSM_CLIENT_XSMP_H__ + +#include "client.h" + +#include + +G_BEGIN_DECLS + +#define GSM_TYPE_CLIENT_XSMP (gsm_client_xsmp_get_type ()) +#define GSM_CLIENT_XSMP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSM_TYPE_CLIENT_XSMP, GsmClientXSMP)) +#define GSM_CLIENT_XSMP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSM_TYPE_CLIENT_XSMP, GsmClientXSMPClass)) +#define GSM_IS_CLIENT_XSMP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSM_TYPE_CLIENT_XSMP)) +#define GSM_IS_CLIENT_XSMP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSM_TYPE_CLIENT_XSMP)) +#define GSM_CLIENT_XSMP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSM_TYPE_CLIENT_XSMP, GsmClientXSMPClass)) + +typedef struct _GsmClientXSMP GsmClientXSMP; +typedef struct _GsmClientXSMPClass GsmClientXSMPClass; + +struct _GsmClientXSMP +{ + GsmClient parent; + + SmsConn conn; + IceConn ice_conn; + + guint watch_id, protocol_timeout; + + int current_save_yourself, next_save_yourself; + char *id, *description; + GPtrArray *props; +}; + +struct _GsmClientXSMPClass +{ + GsmClientClass parent_class; + +}; + +GType gsm_client_xsmp_get_type (void) G_GNUC_CONST; + +GsmClientXSMP *gsm_client_xsmp_new (IceConn ice_conn); + +void gsm_client_xsmp_connect (GsmClientXSMP *client, + SmsConn conn, + unsigned long *mask_ret, + SmsCallbacks *callbacks_ret); + +G_END_DECLS + +#endif /* __GSM_CLIENT_XSMP_H__ */ diff --git a/src/sugar/client.c b/src/sugar/client.c new file mode 100644 index 00000000..48655237 --- /dev/null +++ b/src/sugar/client.c @@ -0,0 +1,251 @@ +/* client.c + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; 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 "client.h" + +enum { + SAVED_STATE, + REQUEST_PHASE2, + REQUEST_INTERACTION, + INTERACTION_DONE, + SAVE_YOURSELF_DONE, + DISCONNECTED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (GsmClient, gsm_client, G_TYPE_OBJECT) + +static void +gsm_client_init (GsmClient *client) +{ + ; +} + +static void +gsm_client_class_init (GsmClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + signals[SAVED_STATE] = + g_signal_new ("saved_state", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmClientClass, saved_state), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[REQUEST_PHASE2] = + g_signal_new ("request_phase2", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmClientClass, request_phase2), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[REQUEST_INTERACTION] = + g_signal_new ("request_interaction", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmClientClass, request_interaction), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[INTERACTION_DONE] = + g_signal_new ("interaction_done", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmClientClass, interaction_done), + NULL, NULL, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, + 1, G_TYPE_BOOLEAN); + + signals[SAVE_YOURSELF_DONE] = + g_signal_new ("save_yourself_done", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmClientClass, save_yourself_done), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[DISCONNECTED] = + g_signal_new ("disconnected", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmClientClass, disconnected), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + +} + +const char * +gsm_client_get_client_id (GsmClient *client) +{ + g_return_val_if_fail (GSM_IS_CLIENT (client), NULL); + + return GSM_CLIENT_GET_CLASS (client)->get_client_id (client); +} + +pid_t +gsm_client_get_pid (GsmClient *client) +{ + g_return_val_if_fail (GSM_IS_CLIENT (client), -1); + + return GSM_CLIENT_GET_CLASS (client)->get_pid (client); +} + +char * +gsm_client_get_desktop_file (GsmClient *client) +{ + g_return_val_if_fail (GSM_IS_CLIENT (client), NULL); + + return GSM_CLIENT_GET_CLASS (client)->get_desktop_file (client); +} + +char * +gsm_client_get_restart_command (GsmClient *client) +{ + g_return_val_if_fail (GSM_IS_CLIENT (client), NULL); + + return GSM_CLIENT_GET_CLASS (client)->get_restart_command (client); +} + +char * +gsm_client_get_discard_command (GsmClient *client) +{ + g_return_val_if_fail (GSM_IS_CLIENT (client), NULL); + + return GSM_CLIENT_GET_CLASS (client)->get_discard_command (client); +} + +gboolean +gsm_client_get_autorestart (GsmClient *client) +{ + g_return_val_if_fail (GSM_IS_CLIENT (client), FALSE); + + return GSM_CLIENT_GET_CLASS (client)->get_autorestart (client); +} + +void +gsm_client_save_state (GsmClient *client) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); +} + +void +gsm_client_restart (GsmClient *client, GError **error) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); + + GSM_CLIENT_GET_CLASS (client)->restart (client, error); +} + +void +gsm_client_save_yourself (GsmClient *client, + gboolean save_state) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); + + GSM_CLIENT_GET_CLASS (client)->save_yourself (client, save_state); +} + +void +gsm_client_save_yourself_phase2 (GsmClient *client) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); + + GSM_CLIENT_GET_CLASS (client)->save_yourself_phase2 (client); +} + +void +gsm_client_interact (GsmClient *client) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); + + GSM_CLIENT_GET_CLASS (client)->interact (client); +} + +void +gsm_client_shutdown_cancelled (GsmClient *client) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); + + GSM_CLIENT_GET_CLASS (client)->shutdown_cancelled (client); +} + +void +gsm_client_die (GsmClient *client) +{ + g_return_if_fail (GSM_IS_CLIENT (client)); + + GSM_CLIENT_GET_CLASS (client)->die (client); +} + +void +gsm_client_saved_state (GsmClient *client) +{ + g_signal_emit (client, signals[SAVED_STATE], 0); +} + +void +gsm_client_request_phase2 (GsmClient *client) +{ + g_signal_emit (client, signals[REQUEST_PHASE2], 0); +} + +void +gsm_client_request_interaction (GsmClient *client) +{ + g_signal_emit (client, signals[REQUEST_INTERACTION], 0); +} + +void +gsm_client_interaction_done (GsmClient *client, gboolean cancel_shutdown) +{ + g_signal_emit (client, signals[INTERACTION_DONE], 0, cancel_shutdown); +} + +void +gsm_client_save_yourself_done (GsmClient *client) +{ + g_signal_emit (client, signals[SAVE_YOURSELF_DONE], 0); +} + +void +gsm_client_disconnected (GsmClient *client) +{ + g_signal_emit (client, signals[DISCONNECTED], 0); +} + diff --git a/src/sugar/client.h b/src/sugar/client.h new file mode 100644 index 00000000..3e4e751f --- /dev/null +++ b/src/sugar/client.h @@ -0,0 +1,111 @@ +/* client.h + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef __GSM_CLIENT_H__ +#define __GSM_CLIENT_H__ + +#include +#include + +G_BEGIN_DECLS + +#define GSM_TYPE_CLIENT (gsm_client_get_type ()) +#define GSM_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSM_TYPE_CLIENT, GsmClient)) +#define GSM_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSM_TYPE_CLIENT, GsmClientClass)) +#define GSM_IS_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSM_TYPE_CLIENT)) +#define GSM_IS_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSM_TYPE_CLIENT)) +#define GSM_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSM_TYPE_CLIENT, GsmClientClass)) + +typedef struct _GsmClient GsmClient; +typedef struct _GsmClientClass GsmClientClass; + +struct _GsmClient +{ + GObject parent; + +}; + +struct _GsmClientClass +{ + GObjectClass parent_class; + + /* signals */ + void (*saved_state) (GsmClient *client); + + void (*request_phase2) (GsmClient *client); + + void (*request_interaction) (GsmClient *client); + void (*interaction_done) (GsmClient *client, + gboolean cancel_shutdown); + + void (*save_yourself_done) (GsmClient *client); + + void (*disconnected) (GsmClient *client); + + /* virtual methods */ + const char * (*get_client_id) (GsmClient *client); + pid_t (*get_pid) (GsmClient *client); + char * (*get_desktop_file) (GsmClient *client); + char * (*get_restart_command) (GsmClient *client); + char * (*get_discard_command) (GsmClient *client); + gboolean (*get_autorestart) (GsmClient *client); + + void (*restart) (GsmClient *client, + GError **error); + void (*save_yourself) (GsmClient *client, + gboolean save_state); + void (*save_yourself_phase2) (GsmClient *client); + void (*interact) (GsmClient *client); + void (*shutdown_cancelled) (GsmClient *client); + void (*die) (GsmClient *client); +}; + +GType gsm_client_get_type (void) G_GNUC_CONST; + +const char *gsm_client_get_client_id (GsmClient *client); + +pid_t gsm_client_get_pid (GsmClient *client); +char *gsm_client_get_desktop_file (GsmClient *client); +char *gsm_client_get_restart_command (GsmClient *client); +char *gsm_client_get_discard_command (GsmClient *client); +gboolean gsm_client_get_autorestart (GsmClient *client); + +void gsm_client_save_state (GsmClient *client); + +void gsm_client_restart (GsmClient *client, + GError **error); +void gsm_client_save_yourself (GsmClient *client, + gboolean save_state); +void gsm_client_save_yourself_phase2 (GsmClient *client); +void gsm_client_interact (GsmClient *client); +void gsm_client_shutdown_cancelled (GsmClient *client); +void gsm_client_die (GsmClient *client); + +/* protected */ +void gsm_client_saved_state (GsmClient *client); +void gsm_client_request_phase2 (GsmClient *client); +void gsm_client_request_interaction (GsmClient *client); +void gsm_client_interaction_done (GsmClient *client, + gboolean cancel_shutdown); +void gsm_client_save_yourself_done (GsmClient *client); +void gsm_client_disconnected (GsmClient *client); + +G_END_DECLS + +#endif /* __GSM_CLIENT_H__ */ diff --git a/src/sugar/eggdesktopfile.c b/src/sugar/eggdesktopfile.c new file mode 100644 index 00000000..ad783451 --- /dev/null +++ b/src/sugar/eggdesktopfile.c @@ -0,0 +1,1437 @@ +/* eggdesktopfile.c - Freedesktop.Org Desktop Files + * Copyright (C) 2007 Novell, Inc. + * + * Based on gnome-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 +#include + +#include +#include +#include +#include + +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_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; + } + else + 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); + 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); + 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) +{ +#if 0 + return g_key_file_get_double (desktop_file->key_file, + EGG_DESKTOP_FILE_GROUP, key, + error); +#else + return 0.0; +#endif +} + +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; +} + +#ifdef HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE +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 */ * 1000) + +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 (EGG_DESKTOP_FILE_SN_TIMEOUT_LENGTH, + startup_notification_timeout, sn_data); +} +#endif /* HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE */ + +extern char **environ; + +static GPtrArray * +array_putenv (GPtrArray *env, char *variable) +{ + int i, keylen; + + if (!env) + { + env = g_ptr_array_new (); + + for (i = 0; environ[i]; i++) + g_ptr_array_add (env, g_strdup (environ[i])); + } + + 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; + 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); + +#ifdef HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE + 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 /* HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE */ + + if (env != NULL) + { + /* Add NULL item in the end of array */ + 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) + { +#ifdef HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE + 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 /* HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE */ + 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_ptr_array_free (env, TRUE); + } + 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); + } + + /* 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/src/sugar/eggdesktopfile.h b/src/sugar/eggdesktopfile.h new file mode 100644 index 00000000..270aec88 --- /dev/null +++ b/src/sugar/eggdesktopfile.h @@ -0,0 +1,156 @@ +/* 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 + +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_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) G_GNUC_PURE; + +EggDesktopFileType egg_desktop_file_get_desktop_file_type (EggDesktopFile *desktop_file) G_GNUC_PURE; + +const char *egg_desktop_file_get_name (EggDesktopFile *desktop_file) G_GNUC_PURE; +const char *egg_desktop_file_get_icon (EggDesktopFile *desktop_file) G_GNUC_PURE; + +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/src/sugar/eggsmclient-private.h b/src/sugar/eggsmclient-private.h new file mode 100644 index 00000000..0b4ec408 --- /dev/null +++ b/src/sugar/eggsmclient-private.h @@ -0,0 +1,54 @@ +/* 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 +#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 +#define EGG_TYPE_SM_CLIENT_XSMP (egg_sm_client_xsmp_get_type ()) +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/src/sugar/eggsmclient-xsmp.c b/src/sugar/eggsmclient-xsmp.c new file mode 100644 index 00000000..13eb5d5b --- /dev/null +++ b/src/sugar/eggsmclient-xsmp.c @@ -0,0 +1,1359 @@ +/* + * Copyright (C) 2007 Novell, Inc. + * + * Inspired by various other pieces of code including GsmClient (C) + * 2001 Havoc Pennington, GnomeClient (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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "eggsmclient.h" +#include "eggsmclient-private.h" + +#include "eggdesktopfile.h" + +#include +#include +#include +#include +#include +#include + +#include + +#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_START, + 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[] = { + "start", + "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_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; + xsmp->client_id = NULL; +} + +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_connect (gpointer user_data) +{ + EggSMClientXSMP *xsmp = user_data; + SmcCallbacks callbacks; + char *client_id; + char error_string_ret[256]; + char pid_str[64]; + EggDesktopFile *desktop_file; + GPtrArray *clone, *restart; + + g_source_remove (xsmp->idle); + xsmp->idle = 0; + + 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, &client_id, + NULL, &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 FALSE; + } + + /* 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 || + (client_id && strcmp (xsmp->client_id, client_id) != 0)) + xsmp->expecting_initial_save_yourself = TRUE; + + if (client_id) + { + g_free (xsmp->client_id); + xsmp->client_id = g_strdup (client_id); + free (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); + } + + /* 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-GNOME-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); + } + } + } + + 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); + } + + xsmp->state = XSMP_STATE_IDLE; + 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; + + xsmp->state = XSMP_STATE_START; + if (xsmp->client_id) + g_free (xsmp->client_id); + xsmp->client_id = g_strdup (client_id); + + /* Don't connect to the session manager until we reach the main + * loop, since the session manager may assume we're fully up and + * running once we connect. (This also gives the application a + * chance to call egg_set_desktop_file() before we set the initial + * properties.) + */ + xsmp->idle = g_idle_add (sm_client_xsmp_connect, 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_START: + /* Force the connection to complete (or fail) now. */ + sm_client_xsmp_connect (xsmp); + break; + + 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->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 gnome-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 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; + + merged_file = g_key_file_new (); + if (g_key_file_load_from_file (merged_file, + egg_desktop_file_get_source (desktop_file), + G_KEY_FILE_KEEP_COMMENTS | + G_KEY_FILE_KEEP_TRANSLATIONS, NULL)) + { + int 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); + + } + } + + /* 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; + int 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; + int 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 GnomeClient or that use libSM + * directly. + */ + +#include +#include + +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/src/sugar/eggsmclient.c b/src/sugar/eggsmclient.c new file mode 100644 index 00000000..86036f91 --- /dev/null +++ b/src/sugar/eggsmclient.c @@ -0,0 +1,392 @@ +/* + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#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] = { 0 }; + +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 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 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_STRING, &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") }, + { NULL } +}; + +/** + * 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) +{ + 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; + + 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); +} + +/* Signal-emitting callbacks from platform-specific code */ + +GKeyFile * +egg_sm_client_save_state (EggSMClient *client) +{ + GKeyFile *state_file; + char *group; + + 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) +{ + 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_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_debug ("Emitting quit"); + g_signal_emit (client, signals[QUIT], 0); + g_debug ("Done emitting quit"); + + /* FIXME: should we just call gtk_main_quit() here? */ +} + +void +egg_sm_client_startup (EggSMClient *client) +{ + if (EGG_SM_CLIENT_GET_CLASS (client)->startup) + EGG_SM_CLIENT_GET_CLASS (client)->startup (client, sm_client_id); +} + +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/src/sugar/eggsmclient.h b/src/sugar/eggsmclient.h new file mode 100644 index 00000000..52d85de3 --- /dev/null +++ b/src/sugar/eggsmclient.h @@ -0,0 +1,112 @@ +/* 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 + +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; + +/* 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); + +void egg_sm_client_startup (EggSMClient *client); + +/* Initiate a logout/reboot/shutdown */ +gboolean egg_sm_client_end_session (EggSMClientEndStyle style, + gboolean request_confirmation); + +G_END_DECLS + + +#endif /* __EGG_SM_CLIENT_H__ */ diff --git a/src/sugar/session.c b/src/sugar/session.c new file mode 100644 index 00000000..37e3587a --- /dev/null +++ b/src/sugar/session.c @@ -0,0 +1,550 @@ +/* session.c + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include + +#include "session.h" +#include "app.h" +#include "xsmp.h" + +GsmSession *global_session; + +static void initiate_shutdown (GsmSession *session); + +static void session_shutdown (GsmSession *session); + +static void client_saved_state (GsmClient *client, + gpointer data); +static void client_request_phase2 (GsmClient *client, + gpointer data); +static void client_request_interaction (GsmClient *client, + gpointer data); +static void client_interaction_done (GsmClient *client, + gboolean cancel_shutdown, + gpointer data); +static void client_save_yourself_done (GsmClient *client, + gpointer data); +static void client_disconnected (GsmClient *client, + gpointer data); + +struct _GsmSession { + GObject parent; + + char *name; + + /* Current status */ + GsmSessionPhase phase; + guint timeout; + GSList *pending_apps; + + /* SM clients */ + GSList *clients; + + /* When shutdown starts, all clients are put into shutdown_clients. + * If they request phase2, they are moved from shutdown_clients to + * phase2_clients. If they request interaction, they are appended + * to interact_clients (the first client in interact_clients is + * the one currently interacting). If they report that they're done, + * they're removed from shutdown_clients/phase2_clients. + * + * Once shutdown_clients is empty, phase2 starts. Once phase2_clients + * is empty, shutdown is complete. + */ + GSList *shutdown_clients; + GSList *interact_clients; + GSList *phase2_clients; + + /* List of clients which were disconnected due to disabled condition + * and shouldn't be automatically restarted */ + GSList *condition_clients; +}; + +struct _GsmSessionClass +{ + GObjectClass parent_class; + + void (* shutdown_completed) (GsmSession *client); +}; + +enum { + SHUTDOWN_COMPLETED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (GsmSession, gsm_session, G_TYPE_OBJECT) + +#define GSM_SESSION_PHASE_TIMEOUT 10 /* seconds */ + +void +gsm_session_init (GsmSession *session) +{ + session->name = NULL; + session->clients = NULL; + session->condition_clients = NULL; +} + +static void +gsm_session_class_init (GsmSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + signals[SHUTDOWN_COMPLETED] = + g_signal_new ("shutdown_completed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsmSessionClass, shutdown_completed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + + +/** + * gsm_session_set_name: + * @session: session instance + * @name: name of the session + * + * Sets the name of a running session. + **/ +void +gsm_session_set_name (GsmSession *session, const char *name) +{ + if (session->name) + g_free (session->name); + + session->name = g_strdup (name); +} + +static void start_phase (GsmSession *session); + +static void +end_phase (GsmSession *session) +{ + g_slist_free (session->pending_apps); + session->pending_apps = NULL; + + g_debug ("ending phase %d\n", session->phase); + + session->phase++; + + if (session->phase < GSM_SESSION_PHASE_RUNNING) + start_phase (session); +} + +static void +app_condition_changed (GsmApp *app, gboolean condition, gpointer data) +{ + GsmSession *session; + GsmClient *client = NULL; + GSList *cl = NULL; + + g_return_if_fail (data != NULL); + + session = (GsmSession *) data; + + /* Check for an existing session client for this app */ + for (cl = session->clients; cl; cl = cl->next) + { + GsmClient *c = GSM_CLIENT (cl->data); + + if (!strcmp (app->client_id, gsm_client_get_client_id (c))) + client = c; + } + + if (condition) + { + GError *error = NULL; + + if (app->pid <= 0 && client == NULL) + gsm_app_launch (app, &error); + + if (error != NULL) + { + g_warning ("Not able to launch autostart app from its condition: %s", + error->message); + + g_error_free (error); + } + } + else + { + /* Kill client in case condition if false and make sure it won't + * be automatically restarted by adding the client to + * condition_clients */ + session->condition_clients = + g_slist_prepend (session->condition_clients, client); + gsm_client_die (client); + app->pid = -1; + } +} + +static void +app_registered (GsmApp *app, gpointer data) +{ + GsmSession *session = data; + + session->pending_apps = g_slist_remove (session->pending_apps, app); + g_signal_handlers_disconnect_by_func (app, app_registered, session); + + if (!session->pending_apps) + { + if (session->timeout > 0) + { + g_source_remove (session->timeout); + session->timeout = 0; + } + + end_phase (session); + } +} + +static gboolean +phase_timeout (gpointer data) +{ + GsmSession *session = data; + GSList *a; + + session->timeout = 0; + + for (a = session->pending_apps; a; a = a->next) + { + g_warning ("Application '%s' failed to register before timeout", + gsm_app_get_basename (a->data)); + g_signal_handlers_disconnect_by_func (a->data, app_registered, session); + + /* FIXME: what if the app was filling in a required slot? */ + } + + end_phase (session); + return FALSE; +} + +static void +start_phase (GsmSession *session) +{ + GsmApp *app; + GSList *a; + GError *err = NULL; + + g_debug ("starting phase %d\n", session->phase); + + g_slist_free (session->pending_apps); + session->pending_apps = NULL; + + if (session->pending_apps) + { + if (session->phase < GSM_SESSION_PHASE_APPLICATION) + { + session->timeout = g_timeout_add (GSM_SESSION_PHASE_TIMEOUT * 1000, + phase_timeout, session); + } + } + else + end_phase (session); +} + +void +gsm_session_start (GsmSession *session) +{ + session->phase = GSM_SESSION_PHASE_INITIALIZATION; + + start_phase (session); +} + +GsmSessionPhase +gsm_session_get_phase (GsmSession *session) +{ + return session->phase; +} + +char * +gsm_session_register_client (GsmSession *session, + GsmClient *client, + const char *id) +{ + GSList *a; + char *client_id = NULL; + + /* If we're shutting down, we don't accept any new session + clients. */ + if (session->phase == GSM_SESSION_PHASE_SHUTDOWN) + return FALSE; + + if (id == NULL) + client_id = gsm_xsmp_generate_client_id (); + else + { + for (a = session->clients; a; a = a->next) + { + GsmClient *client = GSM_CLIENT (a->data); + + /* We can't have two clients with the same id. */ + if (!strcmp (id, gsm_client_get_client_id (client))) + { + return NULL; + } + } + + client_id = g_strdup (id); + } + + g_debug ("Adding new client %s to session", id); + + g_signal_connect (client, "saved_state", + G_CALLBACK (client_saved_state), session); + g_signal_connect (client, "request_phase2", + G_CALLBACK (client_request_phase2), session); + g_signal_connect (client, "request_interaction", + G_CALLBACK (client_request_interaction), session); + g_signal_connect (client, "interaction_done", + G_CALLBACK (client_interaction_done), session); + g_signal_connect (client, "save_yourself_done", + G_CALLBACK (client_save_yourself_done), session); + g_signal_connect (client, "disconnected", + G_CALLBACK (client_disconnected), session); + + session->clients = g_slist_prepend (session->clients, client); + + /* If it's a brand new client id, we just accept the client*/ + if (id == NULL) + return client_id; + + /* If we're starting up the session, try to match the new client + * with one pending apps for the current phase. If not, try to match + * with any of the autostarted apps. */ + if (session->phase < GSM_SESSION_PHASE_APPLICATION) + a = session->pending_apps; + + for (; a; a = a->next) + { + GsmApp *app = GSM_APP (a->data); + + if (!strcmp (client_id, app->client_id)) + { + gsm_app_registered (app); + return client_id; + } + } + + g_free (client_id); + + return NULL; +} + +static void +client_saved_state (GsmClient *client, gpointer data) +{ + /* FIXME */ +} + +void +gsm_session_initiate_shutdown (GsmSession *session) +{ + gboolean logout_prompt; + + if (session->phase == GSM_SESSION_PHASE_SHUTDOWN) + { + /* Already shutting down, nothing more to do */ + return; + } + + initiate_shutdown (session); +} + +static void +session_shutdown_phase2 (GsmSession *session) +{ + GSList *cl; + + for (cl = session->phase2_clients; cl; cl = cl->next) + gsm_client_save_yourself_phase2 (cl->data); +} + +static void +session_cancel_shutdown (GsmSession *session) +{ + GSList *cl; + + session->phase = GSM_SESSION_PHASE_RUNNING; + + g_slist_free (session->shutdown_clients); + session->shutdown_clients = NULL; + g_slist_free (session->interact_clients); + session->interact_clients = NULL; + g_slist_free (session->phase2_clients); + session->phase2_clients = NULL; + + for (cl = session->clients; cl; cl = cl->next) + gsm_client_shutdown_cancelled (cl->data); +} + +static void +initiate_shutdown (GsmSession *session) +{ + GSList *cl; + + session->phase = GSM_SESSION_PHASE_SHUTDOWN; + + if (session->clients == NULL) + session_shutdown (session); + + for (cl = session->clients; cl; cl = cl->next) + { + GsmClient *client = GSM_CLIENT (cl->data); + + session->shutdown_clients = + g_slist_prepend (session->shutdown_clients, client); + + gsm_client_save_yourself (client, FALSE); + } +} + +static void +session_shutdown (GsmSession *session) +{ + GSList *cl; + + /* FIXME: do this in reverse phase order */ + for (cl = session->clients; cl; cl = cl->next) + gsm_client_die (cl->data); +} + +static void +client_request_phase2 (GsmClient *client, gpointer data) +{ + GsmSession *session = data; + + /* Move the client from shutdown_clients to phase2_clients */ + + session->shutdown_clients = + g_slist_remove (session->shutdown_clients, client); + session->phase2_clients = + g_slist_prepend (session->phase2_clients, client); +} + +static void +client_request_interaction (GsmClient *client, gpointer data) +{ + GsmSession *session = data; + + session->interact_clients = + g_slist_append (session->interact_clients, client); + + if (!session->interact_clients->next) + gsm_client_interact (client); +} + +static void +client_interaction_done (GsmClient *client, gboolean cancel_shutdown, + gpointer data) +{ + GsmSession *session = data; + + g_return_if_fail (session->interact_clients && + (GsmClient *)session->interact_clients->data == client); + + if (cancel_shutdown) + { + session_cancel_shutdown (session); + return; + } + + /* Remove this client from interact_clients, and if there's another + * client waiting to interact, let it interact now. + */ + session->interact_clients = + g_slist_remove (session->interact_clients, client); + if (session->interact_clients) + gsm_client_interact (session->interact_clients->data); +} + +static void +client_save_yourself_done (GsmClient *client, gpointer data) +{ + GsmSession *session = data; + + session->shutdown_clients = + g_slist_remove (session->shutdown_clients, client); + session->interact_clients = + g_slist_remove (session->interact_clients, client); + session->phase2_clients = + g_slist_remove (session->phase2_clients, client); + + if (session->phase == GSM_SESSION_PHASE_SHUTDOWN && + !session->shutdown_clients) + { + if (session->phase2_clients) + session_shutdown_phase2 (session); + else + session_shutdown (session); + } +} + +static void +client_disconnected (GsmClient *client, gpointer data) +{ + GsmSession *session = data; + gboolean is_condition_client = FALSE; + + session->clients = + g_slist_remove (session->clients, client); + session->shutdown_clients = + g_slist_remove (session->shutdown_clients, client); + session->interact_clients = + g_slist_remove (session->interact_clients, client); + session->phase2_clients = + g_slist_remove (session->phase2_clients, client); + + if (g_slist_find (session->condition_clients, client)) + { + session->condition_clients = + g_slist_remove (session->condition_clients, client); + + is_condition_client = TRUE; + } + + if (session->phase != GSM_SESSION_PHASE_SHUTDOWN && + gsm_client_get_autorestart (client) && + !is_condition_client) + { + GError *error = NULL; + + gsm_client_restart (client, &error); + + if (error) + { + g_warning ("Error on restarting session client: %s", error->message); + g_clear_error (&error); + } + } + + g_object_unref (client); + + g_signal_emit (session, signals[SHUTDOWN_COMPLETED], 0); +} + +GsmSession * +gsm_session_create_global (void) +{ + global_session = GSM_SESSION(g_object_new (GSM_TYPE_SESSION, NULL)); + return global_session; +} diff --git a/src/sugar/session.h b/src/sugar/session.h new file mode 100644 index 00000000..cc4376af --- /dev/null +++ b/src/sugar/session.h @@ -0,0 +1,93 @@ +/* session.h + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef __GSM_SESSION_H__ +#define __GSM_SESSION_H__ + +#include +#include "client.h" + +G_BEGIN_DECLS + +#define GSM_TYPE_SESSION (gsm_session_get_type ()) +#define GSM_SESSION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSM_TYPE_SESSION, GsmSession)) +#define GSM_SESSION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSM_TYPE_SESSION, GsmSessionClass)) +#define GSM_IS_SESSION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSM_TYPE_SESSION)) +#define GSM_IS_SESSION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSM_TYPE_SESSION)) +#define GSM_SESSION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSM_TYPE_SESSION, GsmSessionClass)) + +typedef struct _GsmSession GsmSession; +typedef struct _GsmSessionClass GsmSessionClass; +extern GsmSession *global_session; + +typedef enum { + /* gsm's own startup/initialization phase */ + GSM_SESSION_PHASE_STARTUP, + + /* xrandr setup, gnome-settings-daemon, etc */ + GSM_SESSION_PHASE_INITIALIZATION, + + /* window/compositing managers */ + GSM_SESSION_PHASE_WINDOW_MANAGER, + + /* apps that will create _NET_WM_WINDOW_TYPE_PANEL windows */ + GSM_SESSION_PHASE_PANEL, + + /* apps that will create _NET_WM_WINDOW_TYPE_DESKTOP windows */ + GSM_SESSION_PHASE_DESKTOP, + + /* everything else */ + GSM_SESSION_PHASE_APPLICATION, + + /* done launching */ + GSM_SESSION_PHASE_RUNNING, + + /* shutting down */ + GSM_SESSION_PHASE_SHUTDOWN +} GsmSessionPhase; + +typedef enum { + GSM_SESSION_LOGOUT_TYPE_LOGOUT, + GSM_SESSION_LOGOUT_TYPE_SHUTDOWN +} GsmSessionLogoutType; + +typedef enum { + GSM_SESSION_LOGOUT_MODE_NORMAL, + GSM_SESSION_LOGOUT_MODE_NO_CONFIRMATION, + GSM_SESSION_LOGOUT_MODE_FORCE +} GsmSessionLogoutMode; + +void gsm_session_set_name (GsmSession *session, + const char *name); + +void gsm_session_start (GsmSession *session); + +GsmSessionPhase gsm_session_get_phase (GsmSession *session); + +void gsm_session_initiate_shutdown (GsmSession *session); + +char *gsm_session_register_client (GsmSession *session, + GsmClient *client, + const char *previous_id); + +GsmSession *gsm_session_create_global (void); + +G_END_DECLS + +#endif /* __GSM_SESSION_H__ */ diff --git a/src/sugar/session.py b/src/sugar/session.py new file mode 100644 index 00000000..21251814 --- /dev/null +++ b/src/sugar/session.py @@ -0,0 +1,46 @@ +# Copyright (C) 2008, Red Hat, 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. + +import os + +from sugar import _sugarext + +class XSMPClient(_sugarext.SMClientXSMP): + def __init__(self): + _sugarext.SMClientXSMP.__init__(self) + +class Session(object): + def __init__(self): + address = _sugarext.xsmp_init() + os.environ['SESSION_MANAGER'] = address + _sugarext.xsmp_run() + + self.session = _sugarext.session_create_global() + + def start(self): + self.session.start() + self.session.connect('shutdown_completed', + self.__shutdown_completed_cb) + + def initiate_shutdown(self, ): + self.session.initiate_shutdown() + + def shutdown_completed(self): + pass + + def __shutdown_completed_cb(self, session): + self.shutdown_completed() diff --git a/src/sugar/xsmp.c b/src/sugar/xsmp.c new file mode 100644 index 00000000..d1260b29 --- /dev/null +++ b/src/sugar/xsmp.c @@ -0,0 +1,537 @@ +/* xsmp.c + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; 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 +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "client-xsmp.h" +//#include "gsm.h" +//#include "util.h" +#include "xsmp.h" + +#include +#include +#include +#include + +#ifdef HAVE_X11_XTRANS_XTRANS_H +/* Get the proto for _IceTransNoListen */ +#define ICE_t +#define TRANS_SERVER +#include +#undef ICE_t +#undef TRANS_SERVER +#endif /* HAVE_X11_XTRANS_XTRANS_H */ + +static IceListenObj *xsmp_sockets; +static int num_xsmp_sockets, num_local_xsmp_sockets; + +static gboolean update_iceauthority (gboolean adding); + +static gboolean accept_ice_connection (GIOChannel *source, + GIOCondition condition, + gpointer data); +static Status accept_xsmp_connection (SmsConn conn, + SmPointer manager_data, + unsigned long *mask_ret, + SmsCallbacks *callbacks_ret, + char **failure_reason_ret); + +static void ice_error_handler (IceConn conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence_num, + int error_class, + int severity, + IcePointer values); +static void ice_io_error_handler (IceConn conn); +static void sms_error_handler (SmsConn sms_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence_num, + int error_class, + int severity, + IcePointer values); +/** + * gsm_xsmp_init: + * + * Initializes XSMP. Notably, it creates the XSMP listening socket and + * sets the SESSION_MANAGER environment variable to point to it. + **/ +char * +gsm_xsmp_init (void) +{ + char error[256]; + mode_t saved_umask; + char *network_id_list; + int i; + + /* Set up sane error handlers */ + IceSetErrorHandler (ice_error_handler); + IceSetIOErrorHandler (ice_io_error_handler); + SmsSetErrorHandler (sms_error_handler); + + /* Initialize libSM; we pass NULL for hostBasedAuthProc to disable + * host-based authentication. + */ + if (!SmsInitialize (PACKAGE, VERSION, accept_xsmp_connection, + NULL, NULL, sizeof (error), error)) + g_error("Could not initialize libSM: %s", error); + +#ifdef HAVE_X11_XTRANS_XTRANS_H + /* By default, IceListenForConnections will open one socket for each + * transport type known to X. We don't want connections from remote + * hosts, so for security reasons it would be best if ICE didn't + * even open any non-local sockets. So we use an internal ICElib + * method to disable them here. Unfortunately, there is no way to + * ask X what transport types it knows about, so we're forced to + * guess. + */ + _IceTransNoListen ("tcp"); +#endif + + /* Create the XSMP socket. Older versions of IceListenForConnections + * have a bug which causes the umask to be set to 0 on certain types + * of failures. Probably not an issue on any modern systems, but + * we'll play it safe. + */ + saved_umask = umask (0); + umask (saved_umask); + if (!IceListenForConnections (&num_xsmp_sockets, &xsmp_sockets, + sizeof (error), error)) + g_error ("Could not create ICE listening socket: %s", error); + umask (saved_umask); + + /* Find the local sockets in the returned socket list and move them + * to the start of the list. + */ + for (i = num_local_xsmp_sockets = 0; i < num_xsmp_sockets; i++) + { + char *id = IceGetListenConnectionString (xsmp_sockets[i]); + + if (!strncmp (id, "local/", sizeof ("local/") - 1) || + !strncmp (id, "unix/", sizeof ("unix/") - 1)) + { + if (i > num_local_xsmp_sockets) + { + IceListenObj tmp = xsmp_sockets[i]; + xsmp_sockets[i] = xsmp_sockets[num_local_xsmp_sockets]; + xsmp_sockets[num_local_xsmp_sockets] = tmp; + } + num_local_xsmp_sockets++; + } + free (id); + } + + if (num_local_xsmp_sockets == 0) + g_error ("IceListenForConnections did not return a local listener!"); + +#ifdef HAVE_X11_XTRANS_XTRANS_H + if (num_local_xsmp_sockets != num_xsmp_sockets) + { + /* Xtrans was apparently compiled with support for some + * non-local transport besides TCP (which we disabled above); we + * won't create IO watches on those extra sockets, so + * connections to them will never be noticed, but they're still + * there, which is inelegant. + * + * If the g_warning below is triggering for you and you want to + * stop it, the fix is to add additional _IceTransNoListen() + * calls above. + */ + network_id_list = + IceComposeNetworkIdList (num_xsmp_sockets - num_local_xsmp_sockets, + xsmp_sockets + num_local_xsmp_sockets); + g_warning ("IceListenForConnections returned %d non-local listeners: %s", + num_xsmp_sockets - num_local_xsmp_sockets, network_id_list); + free (network_id_list); + } +#endif + + /* Update .ICEauthority with new auth entries for our socket */ + if (!update_iceauthority (TRUE)) + { + /* FIXME: is this really fatal? Hm... */ + g_error ("Could not update ICEauthority file %s", + IceAuthFileName ()); + } + + network_id_list = IceComposeNetworkIdList (num_local_xsmp_sockets, + xsmp_sockets); + + return network_id_list; +} + +/** + * gsm_xsmp_run: + * + * Sets the XSMP server to start accepting connections. + **/ +void +gsm_xsmp_run (void) +{ + GIOChannel *channel; + int i; + + for (i = 0; i < num_local_xsmp_sockets; i++) + { + channel = g_io_channel_unix_new (IceGetListenConnectionNumber (xsmp_sockets[i])); + g_io_add_watch (channel, G_IO_IN | G_IO_HUP | G_IO_ERR, + accept_ice_connection, xsmp_sockets[i]); + g_io_channel_unref (channel); + } +} + +/** + * gsm_xsmp_shutdown: + * + * Shuts down the XSMP server and closes the ICE listening socket + **/ +void +gsm_xsmp_shutdown (void) +{ + update_iceauthority (FALSE); + + IceFreeListenObjs (num_xsmp_sockets, xsmp_sockets); + xsmp_sockets = NULL; +} + +/** + * gsm_xsmp_generate_client_id: + * + * Generates a new XSMP client ID. + * + * Return value: an XSMP client ID. + **/ +char * +gsm_xsmp_generate_client_id (void) +{ + static int sequence = -1; + static guint rand1 = 0, rand2 = 0; + static pid_t pid = 0; + struct timeval tv; + + /* The XSMP spec defines the ID as: + * + * Version: "1" + * Address type and address: + * "1" + an IPv4 address as 8 hex digits + * "2" + a DECNET address as 12 hex digits + * "6" + an IPv6 address as 32 hex digits + * Time stamp: milliseconds since UNIX epoch as 13 decimal digits + * Process-ID type and process-ID: + * "1" + POSIX PID as 10 decimal digits + * Sequence number as 4 decimal digits + * + * XSMP client IDs are supposed to be globally unique: if + * SmsGenerateClientID() is unable to determine a network + * address for the machine, it gives up and returns %NULL. + * GNOME and KDE have traditionally used a fourth address + * format in this case: + * "0" + 16 random hex digits + * + * We don't even bother trying SmsGenerateClientID(), since the + * user's IP address is probably "192.168.1.*" anyway, so a random + * number is actually more likely to be globally unique. + */ + + if (!rand1) + { + rand1 = g_random_int (); + rand2 = g_random_int (); + pid = getpid (); + } + + sequence = (sequence + 1) % 10000; + gettimeofday (&tv, NULL); + return g_strdup_printf ("10%.04x%.04x%.10lu%.3u%.10lu%.4d", + rand1, rand2, + (unsigned long) tv.tv_sec, + (unsigned) tv.tv_usec, + (unsigned long) pid, + sequence); +} + +/* This is called (by glib via xsmp->ice_connection_watch) when a + * connection is first received on the ICE listening socket. (We + * expect that the client will then initiate XSMP on the connection; + * if it does not, GsmClientXSMP will eventually time out and close + * the connection.) + * + * FIXME: it would probably make more sense to not create a + * GsmClientXSMP object until accept_xsmp_connection, below (and to do + * the timing-out here in xsmp.c). + */ +static gboolean +accept_ice_connection (GIOChannel *source, + GIOCondition condition, + gpointer data) +{ + IceListenObj listener = data; + IceConn ice_conn; + IceAcceptStatus status; + GsmClientXSMP *client; + + g_debug ("accept_ice_connection()"); + + ice_conn = IceAcceptConnection (listener, &status); + if (status != IceAcceptSuccess) + { + g_debug ("IceAcceptConnection returned %d", status); + return TRUE; + } + + client = gsm_client_xsmp_new (ice_conn); + ice_conn->context = client; + return TRUE; +} + +/* This is called (by libSM) when XSMP is initiated on an ICE + * connection that was already accepted by accept_ice_connection. + */ +static Status +accept_xsmp_connection (SmsConn sms_conn, SmPointer manager_data, + unsigned long *mask_ret, SmsCallbacks *callbacks_ret, + char **failure_reason_ret) +{ + IceConn ice_conn; + GsmClientXSMP *client; + + /* FIXME: what about during shutdown but before gsm_xsmp_shutdown? */ + if (!xsmp_sockets) + { + g_debug ("In shutdown, rejecting new client"); + + *failure_reason_ret = + strdup (_("Refusing new client connection because the session is currently being shut down\n")); + return FALSE; + } + + ice_conn = SmsGetIceConnection (sms_conn); + client = ice_conn->context; + + g_return_val_if_fail (client != NULL, TRUE); + + gsm_client_xsmp_connect (client, sms_conn, mask_ret, callbacks_ret); + return TRUE; +} + +/* ICEauthority stuff */ + +/* Various magic numbers stolen from iceauth.c */ +#define GSM_ICE_AUTH_RETRIES 10 +#define GSM_ICE_AUTH_INTERVAL 2 /* 2 seconds */ +#define GSM_ICE_AUTH_LOCK_TIMEOUT 600 /* 10 minutes */ + +#define GSM_ICE_MAGIC_COOKIE_AUTH_NAME "MIT-MAGIC-COOKIE-1" +#define GSM_ICE_MAGIC_COOKIE_LEN 16 + +static IceAuthFileEntry * +auth_entry_new (const char *protocol, const char *network_id) +{ + IceAuthFileEntry *file_entry; + IceAuthDataEntry data_entry; + + file_entry = malloc (sizeof (IceAuthFileEntry)); + + file_entry->protocol_name = strdup (protocol); + file_entry->protocol_data = NULL; + file_entry->protocol_data_length = 0; + file_entry->network_id = strdup (network_id); + file_entry->auth_name = strdup (GSM_ICE_MAGIC_COOKIE_AUTH_NAME); + file_entry->auth_data = IceGenerateMagicCookie (GSM_ICE_MAGIC_COOKIE_LEN); + file_entry->auth_data_length = GSM_ICE_MAGIC_COOKIE_LEN; + + /* Also create an in-memory copy, which is what the server will + * actually use for checking client auth. + */ + data_entry.protocol_name = file_entry->protocol_name; + data_entry.network_id = file_entry->network_id; + data_entry.auth_name = file_entry->auth_name; + data_entry.auth_data = file_entry->auth_data; + data_entry.auth_data_length = file_entry->auth_data_length; + IceSetPaAuthData (1, &data_entry); + + return file_entry; +} + +static gboolean +update_iceauthority (gboolean adding) +{ + char *filename = IceAuthFileName (); + char **our_network_ids; + FILE *fp; + IceAuthFileEntry *auth_entry; + GSList *entries, *e; + int i; + gboolean ok = FALSE; + + if (IceLockAuthFile (filename, GSM_ICE_AUTH_RETRIES, GSM_ICE_AUTH_INTERVAL, + GSM_ICE_AUTH_LOCK_TIMEOUT) != IceAuthLockSuccess) + return FALSE; + + our_network_ids = g_malloc (num_local_xsmp_sockets * sizeof (char *)); + for (i = 0; i < num_local_xsmp_sockets; i++) + our_network_ids[i] = IceGetListenConnectionString (xsmp_sockets[i]); + + entries = NULL; + + fp = fopen (filename, "r+"); + if (fp) + { + while ((auth_entry = IceReadAuthFileEntry (fp)) != NULL) + { + /* Skip/delete entries with no network ID (invalid), or with + * our network ID; if we're starting up, an entry with our + * ID must be a stale entry left behind by an old process, + * and if we're shutting down, it won't be valid in the + * future, so either way we want to remove it from the list. + */ + if (!auth_entry->network_id) + { + IceFreeAuthFileEntry (auth_entry); + continue; + } + + for (i = 0; i < num_local_xsmp_sockets; i++) + { + if (!strcmp (auth_entry->network_id, our_network_ids[i])) + { + IceFreeAuthFileEntry (auth_entry); + break; + } + } + if (i != num_local_xsmp_sockets) + continue; + + entries = g_slist_prepend (entries, auth_entry); + } + + rewind (fp); + } + else + { + int fd; + + if (g_file_test (filename, G_FILE_TEST_EXISTS)) + { + g_warning ("Unable to read ICE authority file: %s", filename); + goto cleanup; + } + + fd = open (filename, O_CREAT | O_WRONLY, 0600); + fp = fdopen (fd, "w"); + if (!fp) + { + g_warning ("Unable to write to ICE authority file: %s", filename); + if (fd != -1) + close (fd); + goto cleanup; + } + } + + if (adding) + { + for (i = 0; i < num_local_xsmp_sockets; i++) + { + entries = g_slist_append (entries, + auth_entry_new ("ICE", our_network_ids[i])); + entries = g_slist_prepend (entries, + auth_entry_new ("XSMP", our_network_ids[i])); + } + } + + for (e = entries; e; e = e->next) + { + IceAuthFileEntry *auth_entry = e->data; + IceWriteAuthFileEntry (fp, auth_entry); + IceFreeAuthFileEntry (auth_entry); + } + g_slist_free (entries); + + fclose (fp); + ok = TRUE; + + cleanup: + IceUnlockAuthFile (filename); + for (i = 0; i < num_local_xsmp_sockets; i++) + free (our_network_ids[i]); + g_free (our_network_ids); + + return ok; +} + +/* Error handlers */ + +static void +ice_error_handler (IceConn conn, Bool swap, int offending_minor_opcode, + unsigned long offending_sequence, int error_class, + int severity, IcePointer values) +{ + g_debug ("ice_error_handler (%p, %s, %d, %lx, %d, %d)", + conn, swap ? "TRUE" : "FALSE", offending_minor_opcode, + offending_sequence, error_class, severity); + + if (severity == IceCanContinue) + return; + + /* FIXME: the ICElib docs are completely vague about what we're + * supposed to do in this case. Need to verify that calling + * IceCloseConnection() here is guaranteed to cause neither + * free-memory-reads nor leaks. + */ + IceCloseConnection (conn); +} + +static void +ice_io_error_handler (IceConn conn) +{ + g_debug ("ice_io_error_handler (%p)", conn); + + /* We don't need to do anything here; the next call to + * IceProcessMessages() for this connection will receive + * IceProcessMessagesIOError and we can handle the error there. + */ +} + +static void +sms_error_handler (SmsConn conn, Bool swap, int offending_minor_opcode, + unsigned long offending_sequence_num, int error_class, + int severity, IcePointer values) +{ + g_debug ("sms_error_handler (%p, %s, %d, %lx, %d, %d)", + conn, swap ? "TRUE" : "FALSE", offending_minor_opcode, + offending_sequence_num, error_class, severity); + + /* We don't need to do anything here; if the connection needs to be + * closed, libSM will do that itself. + */ +} diff --git a/src/sugar/xsmp.h b/src/sugar/xsmp.h new file mode 100644 index 00000000..b4b535fb --- /dev/null +++ b/src/sugar/xsmp.h @@ -0,0 +1,29 @@ +/* xsmp.h + * Copyright (C) 2007 Novell, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef __GSM_XSMP_H__ +#define __GSM_XSMP_H__ + +char *gsm_xsmp_init (void); +void gsm_xsmp_run (void); +void gsm_xsmp_shutdown (void); + +char *gsm_xsmp_generate_client_id (void); + +#endif /* __GSM_XSMP_H__ */