Mercurial > audlegacy
changeset 4315:c942eaef7bc6
Implement session management.
author | Ivan N. Zlatev <contact@i-nz.net> |
---|---|
date | Mon, 03 Mar 2008 18:42:36 +0000 |
parents | e548627b2004 |
children | e55ecfc55d06 |
files | configure.ac m4/as-ac-expand.m4 src/Makefile src/audacious/Makefile src/audacious/main.c src/audacious/signals.c src/libeggsmclient/ChangeLog src/libeggsmclient/Makefile src/libeggsmclient/NEWS src/libeggsmclient/README src/libeggsmclient/eggdesktopfile.c src/libeggsmclient/eggdesktopfile.h src/libeggsmclient/eggsmclient-osx.c src/libeggsmclient/eggsmclient-private.h src/libeggsmclient/eggsmclient-win32.c src/libeggsmclient/eggsmclient-xsmp.c src/libeggsmclient/eggsmclient.c src/libeggsmclient/eggsmclient.h |
diffstat | 18 files changed, 4549 insertions(+), 5 deletions(-) [+] |
line wrap: on
line diff
--- a/configure.ac Mon Mar 03 22:13:27 2008 +0300 +++ b/configure.ac Mon Mar 03 18:42:36 2008 +0000 @@ -478,6 +478,11 @@ LDFLAGS="$LDFLAGS $RPATH" fi +dnl AUDACIOUS_DESKTOP_FILE check +dnl ======================== +AS_AC_EXPAND(desktop_file,"${datadir}/applications/audacious.desktop") +AC_DEFINE_UNQUOTED(AUDACIOUS_DESKTOP_FILE,"${desktop_file}",["The location of the desktop file"]) + AC_SUBST(beepdir) AC_SUBST(plugindir)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/m4/as-ac-expand.m4 Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,43 @@ +dnl as-ac-expand.m4 0.2.0 +dnl autostars m4 macro for expanding directories using configure's prefix +dnl thomas@apestaart.org + +dnl AS_AC_EXPAND(VAR, CONFIGURE_VAR) +dnl example +dnl AS_AC_EXPAND(SYSCONFDIR, $sysconfdir) +dnl will set SYSCONFDIR to /usr/local/etc if prefix=/usr/local + +AC_DEFUN([AS_AC_EXPAND], +[ + EXP_VAR=[$1] + FROM_VAR=[$2] + + dnl first expand prefix and exec_prefix if necessary + prefix_save=$prefix + exec_prefix_save=$exec_prefix + + dnl if no prefix given, then use /usr/local, the default prefix + if test "x$prefix" = "xNONE"; then + prefix="$ac_default_prefix" + fi + dnl if no exec_prefix given, then use prefix + if test "x$exec_prefix" = "xNONE"; then + exec_prefix=$prefix + fi + + full_var="$FROM_VAR" + dnl loop until it doesn't change anymore + while true; do + new_full_var="`eval echo $full_var`" + if test "x$new_full_var" = "x$full_var"; then break; fi + full_var=$new_full_var + done + + dnl clean up + full_var=$new_full_var + AC_SUBST([$1], "$full_var") + + dnl restore prefix and exec_prefix + prefix=$prefix_save + exec_prefix=$exec_prefix_save +])
--- a/src/Makefile Mon Mar 03 22:13:27 2008 +0300 +++ b/src/Makefile Mon Mar 03 18:42:36 2008 +0000 @@ -1,4 +1,4 @@ -SUBDIRS = libguess libSAD audacious libid3tag +SUBDIRS = libeggsmclient libguess libSAD audacious libid3tag include ../extra.mk
--- a/src/audacious/Makefile Mon Mar 03 22:13:27 2008 +0300 +++ b/src/audacious/Makefile Mon Mar 03 18:42:36 2008 +0000 @@ -141,7 +141,8 @@ CLEAN = build_stamp.c libaudacious.exe.a EXT_DEPS = ../libguess/libguess.a \ - ../libSAD/libSAD.a + ../libSAD/libSAD.a \ + ../libeggsmclient/libeggsmclient.a include ../../buildsys.mk @@ -165,6 +166,7 @@ ${SIMD_CFLAGS} \ -D_AUDACIOUS_CORE \ -I../libSAD \ + -I../libeggsmclient \ -I.. -I../.. \ -I./intl @@ -173,6 +175,7 @@ ${samplerate_LIBS} \ ../libguess/libguess.a \ ../libSAD/libSAD.a \ + ../libeggsmclient/libeggsmclient.a -lSM -lICE \ ${GTK_LIBS} \ ${DBUS_LIBS} \ ${MOWGLI_LIBS} \
--- a/src/audacious/main.c Mon Mar 03 22:13:27 2008 +0300 +++ b/src/audacious/main.c Mon Mar 03 18:42:36 2008 +0000 @@ -84,6 +84,8 @@ #include "signals.h" #include "ui_skinned_window.h" #include "libSAD.h" +#include "eggsmclient.h" +#include "eggdesktopfile.h" #include "icons-stock.h" #include "images/audacious_player.xpm" @@ -1412,14 +1414,14 @@ bind_textdomain_codeset(PACKAGE_NAME "-plugins", "UTF-8"); textdomain(PACKAGE_NAME); +#ifndef _WIN32 + egg_set_desktop_file(AUDACIOUS_DESKTOP_FILE); +#endif bmp_init_paths(); bmp_make_user_dir(); - g_set_application_name(_(application_name)); - cond_scan = g_cond_new(); mutex_scan = g_mutex_new(); - gtk_rc_add_default_file(bmp_paths[BMP_PATH_GTKRC_FILE]); gtk_init_check_ok = gtk_init_check(&argc, &argv); @@ -1430,6 +1432,7 @@ context = g_option_context_new(_("- play multimedia files")); g_option_context_add_main_entries(context, cmd_entries, PACKAGE_NAME); g_option_context_add_group(context, gtk_get_option_group(TRUE)); + g_option_context_add_group(context, egg_sm_client_get_option_group()); g_option_context_parse(context, &argc, &argv, &error); if (error != NULL)
--- a/src/audacious/signals.c Mon Mar 03 22:13:27 2008 +0300 +++ b/src/audacious/signals.c Mon Mar 03 18:42:36 2008 +0000 @@ -36,6 +36,7 @@ #include "ui_main.h" #include "signals.h" #include "build_stamp.h" +#include "eggsmclient.h" gint linuxthread_signal_number = 0; @@ -249,9 +250,36 @@ return FALSE; } +static void +signal_session_quit_cb(EggSMClient *client, gpointer user_data) +{ + g_print("Session quit requested. Saving state and shutting down.\n"); + mainwin_quit_cb(); +} + +static void +signal_session_save_cb(EggSMClient *client, const char *state_dir, gpointer user_data) +{ + g_print("Session save requested. Saving state.\n"); + bmp_config_save(); +} + void signal_handlers_init(void) { + EggSMClient *client; + + client = egg_sm_client_get (); + if (client != NULL) + { + egg_sm_client_set_mode (EGG_SM_CLIENT_MODE_NORMAL); + g_signal_connect (client, "quit", + G_CALLBACK (signal_session_quit_cb), NULL); + g_signal_connect (client, "save-state", + G_CALLBACK (signal_session_save_cb), NULL); + + } + if (signal_check_for_broken_impl() != TRUE) { signal_initialize_blockers();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/ChangeLog Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,152 @@ +2008-03-03 Ivan N. Zlatev <contact@i-nz.net> + + * eggsmclient-xsmp.c (sm_client_xsmp_startup): Connect + immediately, so that windows created before + running the main loop will have SM_CLIENT_ID set. We are not + supposed to modify SM_CLIENT_ID after the visible window has + been created (e.g Metacity will just ignore us). + Delay setting the initial properties until after the main loop, + has started in order to provide a larger time frame for the + user to call egg_set_desktop_file(). + +2007-05-02 Dan Winship <danw@novell.com> + + * egglauncher.c: + * eggdesktopfile.c: Replace EggLauncher with EggDesktopFile, + which is more like GnomeDesktopItem, but with a nicer launching + API. + + * eggsmclient.c (egg_sm_client_register): Gone; apps can use + egg_set_desktop_file() to set the global desktop file, and + egg_sm_client_set_mode() to indicate they don't want to be + restarted. + (egg_sm_client_set_mode): New, lets an app indicate that it wants + to not connect to the SM, or that it wants to not be restarted. + (The common case--wanting to be restarted if the session is + resumed--is now automatic.) + (egg_sm_client_get): Update docs and code for those changes. + + * eggsmclient-libgnomeui.c: add more stuff to make it possible to + set desktop file and EggSMClientMode from gnome_program_new(). + + * eggsmclient-win32.c: Remove unused untested "#ifdef VISTA" stuff + involving state saving. It works in theory, someone else can + implement it later if we actually end up wanting it to work. + + * eggsmclient-xsmp.c (sm_client_xsmp_connect, save_state): Use + egg_get_desktop_file() to see if a desktop file has been set for + the app, and that and egg_sm_client_get_mode() to see what + restart_style to use. + + * egg-launch.c: update for EggDesktopFile + + * README: + * gedit.diff: update + + * NEWS: add this to make it easier for people to see what has + changed API-wise when they grab a new source snapshot. + +2007-04-02 Dan Winship <danw@novell.com> + + * eggsmclient.c (egg_sm_client_end_session): Remove the "client" + argument; eventually this method will not be visibly attached to + the rest of the sm client API. + (EggSMClientPrivate, egg_sm_client_get_state_file): oops, fix bugs + in previous commit + + * egg-session-end.c (main): update + +2007-04-02 Dan Winship <danw@novell.com> + + * eggsmclient.c (egg_sm_client_save_state): Change this signal to + use a GKeyFile rather than a directory name (and return that to + the caller). + (egg_sm_client_get_state_file): replaces get_state_dir, and + returns a GKeyFile + (egg_sm_client_get_config_prefix): Kill for now. Maybe will be + resurrected X11-only later. + + * eggsmclient-xsmp.c (sm_client_xsmp_connect): move some stuff + here from other places; the caller has to call _register() and/or + _set_restart_command() before the main loop starts, so they'll + always have been run before sm_client_xsmp_connect is reached, so + we can do everything from here. + (save_state): update to use a GKeyFile for state rather than a + directory. Merge the app's state into its .desktop file and update + the Exec key in that combined file using %k to point to itself, + thus creating a self-contained launcher/saved state. + + * README: + * gedit.diff: update + +2007-03-16 Dan Winship <danw@novell.com> + + * egglauncher.c (egg_launcher_add_document): use + g_filename_to_uri() and g_filename_from_uri() rather than rolling + our own. + + * egglauncher.h: add two missing prototypes + +2007-03-04 Dan Winship <danw@novell.com> + + * eggsmclient-win32.c: Redo this to fix quit_requested handling, + by listening for WM_QUERYENDSESSION messages in another thread + that can then block waiting for the main thread to handle it. More + of #399315. + (sm_client_win32_end_session): EWX_FORCE means "don't allow + clients to cancel", not "don't show a logout dialog" (which + ExitWindowsEx never does), so don't use it to implement the + request_confirmation flag. + + * Makefile.am: make this work on both unix and windows + + * README: Update. In particular, note that EggSMClientWin32 + requires "-mwindows" and g_thread_init(). + + * egg-session-end.c (main): call g_thread_init + + * logout-test.c (main): call g_thread_init, fix window-closed + handler + + * eggsmclient.c (egg_sm_client_save_state): don't try to compile + this on Windows. (Temporary workaround) + +2007-02-10 Dan Winship <danw@novell.com> + + * egglauncher.c: New, replaces EggDesktopFile (as a replacement + for GnomeDesktopItem...) + + * eggdesktopfile.c: gone + + * egg-launch.c: test program + + * eggsmclient-xsmp.c (set_restart_properties): use EggLauncher + +2007-02-10 Dan Winship <danw@novell.com> + + * eggsmclient-win32.c: Make this compile and at least partially + work (quit_requested doesn't work and probably needs to be + rewritten using a second thread as suggested in the comments in + will_quit()). Patch from Yevgen Muntyan, #399315. + + * eggsmclient-xsmp.c: Use gdk_threads_enter/_leave around gtk + calls and EggSMClient signal emissions. Based on a patch from + Yevgen Muntyan. + + * Makefile.am: + * eggsmclient.c (egg_sm_client_get): Change the #defines from + HAVE_XSMP and HAVE_DBUS to EGG_SM_CLIENT_BACKEND_XSMP and + EGG_SM_CLIENT_BACKEND_DBUS, to avoid being tricked by the app + having its own HAVE_DBUS define. Needs to be fixed better though. + Also change the win32 check from G_OS_WIN32 to + GDK_WINDOWING_WIN32, since it assumes the win32 gdk backend. + + * logout-test.c: New test program to test logout + notification/cancellation support (and demonstrate that it doesn't + really work on Windows). + +2007-01-18 Dan Winship <danw@novell.com> + + * README: add more GnomeClient porting notes based on an email + exchange with Jani Monoses +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/Makefile Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,22 @@ +ifdef PLATFORM_WIN32 +platform_sources = eggsmclient-win32.c +platform_defines = +platform_libs = +else +platform_sources = eggsmclient-xsmp.c \ + eggdesktopfile.c +platform_defines = -DEGG_SM_CLIENT_BACKEND_XSMP +platform_libs = -lSM -lICE +endif + +STATIC_LIB_NOINST = libeggsmclient.a + +SRCS = eggsmclient.c \ + ${platform_sources} + +include ../../buildsys.mk +include ../../extra.mk + +CFLAGS += ${INCLUDES} ${GLIB_CFLAGS} ${GTK_CFLAGS} -I. ${platform_defines} +LIBS += ${GLIB_LIBS} ${GTK_LIBS} ${platform_libs} +CPPFLAGS += ${CFLAGS}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/NEWS Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,22 @@ +2007-05-02 + + * EggLauncher is now replaced by EggDesktopFile + + * egg_sm_client_register() is now gone, replaced by + egg_set_desktop_file() and egg_sm_client_set_mode(). See the + beginning of the "Using EggSMClient" section of README for + details of how these work. + +2007-04-02 + + * The state-saving machinery has changed; it's all based on + keyfiles now. See the "More notes" section of README for an + explanation of why this is cool. + + * egg_sm_client_end_session() no longer takes an EggSMClient; + eventually it could be put somewhere entirely + non-EggSMClient-related in the public API. + +2007-03-04 + + * The win32 backend works now.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/README Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,245 @@ +This is a draft of a replacement for GnomeClient. See also +http://live.gnome.org/SessionManagement and +http://bugzilla.gnome.org/show_bug.cgi?id=79285. + +The eventual target of this code is gtk, gdk, glib, or some +combination of them. Having it entirely in gtk/gdk may not be the best +option, because non-GUI apps like beagled want a chance to exit +cleanly when the session ends too. OTOH, making glib depend on libSM +would obviously suck. Using dlopen may be a good solution. + +EggDesktopItem is proposed gtk-level rewrite of GnomeDesktopItem. See +http://bugzilla.gnome.org/show_bug.cgi?id=415070 + + +Building your app with EggSMClient +---------------------------------- + +If you are using libgnomeui: + + Link to libeggsmclient-gnome.la, #include + "eggsmclient-libgnomeui.h" in your main .c file (and + "eggsmclient.h" elsewhere). + +If you are using plain gtk: + + Link to libeggsmclient.la, and #include "eggsmclient.h". + +If you are building on Windows: + + You need to make sure to link your application as a Windows + GUI binary. (If using GNU tools, use "-mwindows".) If you + build it as a console binary, Windows won't send it the + WM_QUERYENDSESSION (quit_requested) and WM_ENDSESSION (quit) + messages. + + You need to call g_thread_null(NULL) to initialize threads. + + +Using EggSMClient +----------------- + +Initialization with libgnomeui and GnomeProgram: + + Change the gnome_program_new() invocation to use + EGG_SM_CLIENT_LIBGNOMEUI_MODULE instead of LIBGNOMEUI_MODULE. + If you haven't yet switched from popt to GOption for option + parsing, you have to do that now. + + You should also add an EGG_SM_CLIENT_PARAM_DESKTOP_FILE + parameter to the gnome_program_new() invocation, and pass it + the path to your application's installed .desktop file. (This + will be used for some SM-related purposes and will also be + used to initialize your application's localized name and + default window icon. If you are currently calling + g_set_application_name(), gtk_window_set_default_icon(), or + gtk_window_set_default_icon_name(), you can remove those + calls.) + + If you only want to use the logout notification/cancellation + API, and don't want to participate in session saving, you can + pass the EGGSMCLIENT_PARAM_MODE parameter as well, with the + value EGG_SM_CLIENT_MODE_NO_RESTART. + +Initialization with plain gtk: + + Add the GOptionGroup returned by + egg_sm_client_get_option_group() to your GOptionContext when + parsing command line arguments. (If you were previously just + using gtk_init() or the like, you will have to create your own + option context now and also add the group returned by + gtk_get_option_group() to it. See egg-session-end.c for an + example.) + + You should also call egg_set_desktop_file() (in + "eggdesktopfile.h"), passing it the path to your application's + installed .desktop file. (This will be used for some + SM-related purposes and will also be used to initialize your + application's localized name and default window icon. If you + are currently calling g_set_application_name(), + gtk_window_set_default_icon(), or + gtk_window_set_default_icon_name(), you can remove those + calls.) + + If you only want to use the logout notification/cancellation + API, and don't want to participate in session saving, you can + call egg_sm_client_set_mode(EGG_SM_CLIENT_MODE_NO_RESTART). + +After parsing the command-line arguments, call +egg_sm_client_get() to get the EggSMClient object. + +Use egg_sm_client_is_resumed() on the EggSMClient to see if you need +to resume a saved state, and egg_sm_client_get_state_file() to find +that saved state. + +Connect to the client's "save_state" signal if you want to be able to +save your state and be resumed in future sessions. Connect to +"quit_requested" if you want to get a chance to save files before +shutdown (and/or cancel shutting down). + +See the gtk-doc comments in eggsmclient.c for more details. + + +Notes on porting from GnomeClient or raw libSM +---------------------------------------------- + +There's no way to manually set most of the XSMP properties using +EggSMClient (and no need to). Program, ProcessID, and UserID are set +automatically. RestartCommand and CloneCommand are set from the +.desktop file (or from a call to egg_sm_client_set_restart_command), +and DiscardCommand is set automatically as needed after save_state is +emitted. + +RestartStyleHint is set automatically based on a few things: + + - SmRestartNever if you set the mode to + EGG_SM_CLIENT_MODE_NO_RESTART + + - SmRestartImmediately if you initialize with a desktop file + containing the entry "X-GNOME-AutoRestart=true" + + - SmRestartIfRunning if you initialize with no desktop_file, + or with a desktop_file that doesn't set + "X-GNOME-AutoRestart=true". (This is the normal case.) + +There's no way to set the restart style hint to SmRestartAnyway +("restart the client in the resumed session even if it wasn't running +in the saved session"). FDO Autostart is a much better solution for +that class of program. If your program is configured to do autostart, +you don't need to also register for XSMP restart (unless you want to +have it save and resume its state as well). + +CurrentDirectory and Environment are never set. GnomeClient sets +CurrentDirectory, but KDE doesn't restore either of those properties +when resuming a saved session, so it's better for apps to not depend +on them. (If the application must have its working directory restored, +it can just save and restore it itself.) + +(There probably needs to be a way to set _GSM_Priority, for +compatibility with the current gnome-session; this will probably be +done via another .desktop key.) + +The "save_yourself" signal/callback is split into two signals in +EggSMClient: save_state and quit_requested. Most GnomeClient-based +apps only implement the state-saving functionality currently, so they +would only connect to "save_state" in EggSMClient, not +"quit_requested". + +When saving state, EggSMClient provides you with a GKeyFile to store +data in. If you write any data to the key file, EggSMClient will save +it to disk and set an XSMP DiscardCommand to make sure that the +session manager deletes it when it is no longer needed. If you can +record your entire state on the command line, you can use +egg_sm_client_set_restart_command() instead, to set the command that +will be used to restart the app. + +If you implement the "quit_requested" side of the functionality as +well, you don't need to "request interaction" like in +GnomeClient/libSM. You can just start interacting with the user right +away when the signal is emitted. Note that the argument to +egg_sm_client_will_quit() is TRUE if you're willing to quit, and FALSE +if not, which is the opposite of gnome_interaction_key_return() / +SmcInteractDone(). + +quit_requested is only for saving user files and preparing to quit. DO +NOT save the current state from the quit_requested signal. If the user +chooses the "save current session" option when logging out, +EggSMClient will emit quit_requested, let you deal with all of that, +and *then* after you're done, it will emit save_state to give you a +chance to deal with saving the state as well. + +There are a bunch of other bits of GnomeClient API with no analogs: + + - GNOME_CLIENT_IS_CONNECTED - Is this really ever needed? + + - gnome_client_request_phase_2 - This is only needed by window + managers... if someone really wanted it, it could be added + as an EggSMClientXSMP-specific call. (You'd just call it + once, at startup time, to register the fact that you were a + phase2 client, and then when EggSMClientXSMP got a + SaveYourself, it would automatically respond with a + SaveYourselfPhase2Request, and it wouldn't emit the + quit_requested/save_state signals until phase 2.) + + - gnome_client_request_save - Shutdown saves are now done via + egg_sm_client_end_session. There is no way to request a + non-shutdown save, either for yourself, or for the whole + session. (Requesting a local SaveYourself for yourself won't + actually have any effect under most session managers, and + requesting it for the whole session is something that only a + program directly associated with the session manager ought + to do, so there doesn't seem to be a need for a public API + for this.) + + - gnome_client_flush - In most cases this is a no-op. Where + it's not, EggSMClientXSMP should do it automatically + whenever you change anything. (FIXME!) + + - gnome_client_get_id - Why would you need to know? + + +More notes +---------- + +Assuming you provide a .desktop file to gnome_program_init() or +egg_set_desktop_file(), the state file that gets saved when you save +the application state will actually be a copy of that .desktop file, +with the state information appended to the end, and the Exec key +adjusted to include "--sm-client-state-file %k" (where "%k" means "the +path to this .desktop file"). So if you want to, you can move the +state file out of ~/.config/session-state/ and use it directly as a +launcher to restart the application with that saved state. + +Set EGG_SM_CLIENT_DEBUG=1 to debug + +Example code: + + - egg-session-end.c: A replacement for gnome-session-save. + (Well, actually only for "gnome-session-save --kill".) + Yes, the --reboot and --shutdown arguments are only there to + tease you. + + - logout-test.c: Sits around waiting for you to try to log + out, and then asks "are you sure", to test + quit_requested/will_quit handling. + + - gedit.diff: A patch to SVN gedit to make it use EggSMClient + instead of GnomeClient. + +Non-functional backends: + + - OS X: eggsmclient-osx.c is not quite a proof-of-concept. + It's more of a handwave-of-concept. I don't have a working + OS X machine at the moment, so I can't test/finish this. + + See http://developer.apple.com/documentation/MacOSX/Conceptual/BPSystemStartup/Articles/BootProcess.html#//apple_ref/doc/uid/20002130-114618 + and other URLs linked from it for more info on the OS X + logout process. + + - D-Bus: There are a few references in the code to a + non-existent D-Bus session management client. The idea is + that once we have a session manager that implements that, + EggSMClient will pick between the (sane) D-Bus and (nasty) + XSMP backends at runtime depending on what session manager + is running. Or in special environments, it could be compiled + with just XSMP or just D-Bus.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggdesktopfile.c Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,1376 @@ +/* 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 <string.h> +#include <unistd.h> + +#include <glib/gi18n.h> +#include <gdk/gdk.h> +#include <gtk/gtkwindow.h> +#include <gdk/gdkx.h> + +struct EggDesktopFile { + GKeyFile *key_file; + char *source; + + char *name, *icon; + EggDesktopFileType type; + char document_code; +}; + +/** + * egg_desktop_file_new: + * @desktop_file_path: path to a Freedesktop-style Desktop file + * @error: error pointer + * + * Creates a new #EggDesktopFile for @desktop_file. + * + * Return value: the new #EggDesktopFile, or %NULL on error. + **/ +EggDesktopFile * +egg_desktop_file_new (const char *desktop_file_path, GError **error) +{ + EggDesktopFile *desktop_file; + 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; + } + + desktop_file = egg_desktop_file_new_from_key_file (key_file, + desktop_file_path, + error); + if (!desktop_file) + g_key_file_free (key_file); + + return desktop_file; +} + +/** + * 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); + if (!desktop_file) + g_key_file_free (key_file); + + 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 (meaning it will be freed when the desktop_file + * is freed). + * + * 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")); + 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); + return NULL; + } + else + g_free (version); + } + + desktop_file = g_new0 (EggDesktopFile, 1); + + 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; + + /* 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'; + } + } + + desktop_file->key_file = key_file; + 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_key_file: + * @desktop_file: an #EggDesktopFile + * + * Gets the #GKeyFile associated with @desktop_file. You must not free + * this value, and changes made to it will not be reflected by + * @desktop_file. + * + * Return value: the #GKeyFile associated with @desktop_file. + **/ +GKeyFile * +egg_desktop_file_get_key_file (EggDesktopFile *desktop_file) +{ + return desktop_file->key_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; +} + +/** + * 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, *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; + docs = NULL; + translated_documents = 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 */ + + 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_strfreev ((char **)env->pdata); + g_ptr_array_free (env, FALSE); + } + free_document_list (translated_documents); + + return success; +} + +/** + * egg_desktop_file_launch: + * @desktop_file: an #EggDesktopFile + * @documents: a list of URIs or paths to documents to open + * @error: error pointer + * @...: additional options + * + * Launches @desktop_file with the given arguments. Additional options + * can be specified as follows: + * + * %EGG_DESKTOP_FILE_LAUNCH_CLEARENV: (no arguments) + * clears the environment in the child process + * %EGG_DESKTOP_FILE_LAUNCH_PUTENV: (char **variables) + * adds the NAME=VALUE strings in the given %NULL-terminated + * array to the child process's environment + * %EGG_DESKTOP_FILE_LAUNCH_SCREEN: (GdkScreen *screen) + * causes the application to be launched on the given screen + * %EGG_DESKTOP_FILE_LAUNCH_WORKSPACE: (int workspace) + * causes the application to be launched on the given workspace + * %EGG_DESKTOP_FILE_LAUNCH_DIRECTORY: (char *dir) + * causes the application to be launched in the given directory + * %EGG_DESKTOP_FILE_LAUNCH_TIME: (guint32 launch_time) + * sets the "launch time" for the application. If the user + * interacts with another window after @launch_time but before + * the launched application creates its first window, the window + * manager may choose to not give focus to the new application. + * Passing 0 for @launch_time will explicitly request that the + * application not receive focus. + * %EGG_DESKTOP_FILE_LAUNCH_FLAGS (GSpawnFlags flags) + * Sets additional #GSpawnFlags to use. See g_spawn_async() for + * more details. + * %EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC (GSpawnChildSetupFunc, gpointer) + * Sets the child setup callback and the data to pass to it. + * (See g_spawn_async() for more details.) + * + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_PID (GPid **pid) + * On a successful launch, sets *@pid to the PID of the launched + * application. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID (char **startup_id) + * On a successful launch, sets *@startup_id to the Startup + * Notification "startup id" of the launched application. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE (int *fd) + * On a successful launch, sets *@fd to the file descriptor of + * a pipe connected to the application's stdin. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE (int *fd) + * On a successful launch, sets *@fd to the file descriptor of + * a pipe connected to the application's stdout. + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE (int *fd) + * On a successful launch, sets *@fd to the file descriptor of + * a pipe connected to the application's stderr. + * + * The options should be terminated with a single %NULL. + * + * If @documents contains multiple documents, but + * egg_desktop_file_accepts_multiple() returns %FALSE for + * @desktop_file, then egg_desktop_file_launch() will actually launch + * multiple instances of the application. In that case, the return + * value (as well as any values passed via + * %EGG_DESKTOP_FILE_LAUNCH_RETURN_PID, etc) will only reflect the + * first instance of the application that was launched (but the + * %EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC will be called for each + * instance). + * + * Return value: %TRUE if the application was successfully launched. + **/ +gboolean +egg_desktop_file_launch (EggDesktopFile *desktop_file, + GSList *documents, GError **error, + ...) +{ + va_list args; + gboolean success; + EggDesktopFile *app_desktop_file; + + switch (desktop_file->type) + { + case EGG_DESKTOP_FILE_TYPE_APPLICATION: + va_start (args, error); + success = egg_desktop_file_launchv (desktop_file, documents, + args, error); + va_end (args); + break; + + case EGG_DESKTOP_FILE_TYPE_LINK: + if (documents) + { + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE, + _("Can't pass document URIs to a 'Type=Link' desktop entry")); + return FALSE; + } + + if (!parse_link (desktop_file, &app_desktop_file, &documents, error)) + return FALSE; + + va_start (args, error); + success = egg_desktop_file_launchv (app_desktop_file, documents, + args, error); + va_end (args); + + egg_desktop_file_free (app_desktop_file); + free_document_list (documents); + break; + + default: + g_set_error (error, EGG_DESKTOP_FILE_ERROR, + EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE, + _("Not a launchable item")); + success = FALSE; + break; + } + + return success; +} + + +GQuark +egg_desktop_file_error_quark (void) +{ + return g_quark_from_static_string ("egg-desktop_file-error-quark"); +} + + +G_LOCK_DEFINE_STATIC (egg_desktop_file); +static EggDesktopFile *egg_desktop_file; + +/** + * egg_set_desktop_file: + * @desktop_file_path: path to the application's desktop file + * + * Creates an #EggDesktopFile for the application from the data at + * @desktop_file_path. This will also call g_set_application_name() + * with the localized application name from the desktop file, and + * gtk_window_set_default_icon_name() or + * gtk_window_set_default_icon_from_file() with the application's + * icon. Other code may use additional information from the desktop + * file. + * + * Note that for thread safety reasons, this function can only + * be called once. + **/ +void +egg_set_desktop_file (const char *desktop_file_path) +{ + GError *error = NULL; + + G_LOCK (egg_desktop_file); + if (egg_desktop_file) + egg_desktop_file_free (egg_desktop_file); + + egg_desktop_file = egg_desktop_file_new (desktop_file_path, &error); + if (error) + { + g_warning ("Could not load desktop file '%s': %s", + desktop_file_path, error->message); + g_error_free (error); + } + + /* 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; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggdesktopfile.h Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,130 @@ +/* eggdesktopfile.h - Freedesktop.Org Desktop Files + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 59 Temple Place - + * Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __EGG_DESKTOP_FILE_H__ +#define __EGG_DESKTOP_FILE_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct EggDesktopFile EggDesktopFile; + +typedef enum { + EGG_DESKTOP_FILE_TYPE_UNRECOGNIZED, + + EGG_DESKTOP_FILE_TYPE_APPLICATION, + EGG_DESKTOP_FILE_TYPE_LINK, + EGG_DESKTOP_FILE_TYPE_DIRECTORY, +} EggDesktopFileType; + +EggDesktopFile *egg_desktop_file_new (const char *desktop_file_path, + GError **error); + +EggDesktopFile *egg_desktop_file_new_from_data_dirs (const char *desktop_file_path, + GError **error); +EggDesktopFile *egg_desktop_file_new_from_key_file (GKeyFile *desktop, + const char *source, + GError **error); + +void egg_desktop_file_free (EggDesktopFile *desktop_file); + +GKeyFile *egg_desktop_file_get_key_file (EggDesktopFile *desktop_file); + +const char *egg_desktop_file_get_source (EggDesktopFile *desktop_file); + +EggDesktopFileType egg_desktop_file_get_desktop_file_type (EggDesktopFile *desktop_file); + +const char *egg_desktop_file_get_name (EggDesktopFile *desktop_file); +const char *egg_desktop_file_get_icon (EggDesktopFile *desktop_file); + +gboolean egg_desktop_file_can_launch (EggDesktopFile *desktop_file, + const char *desktop_environment); + +gboolean egg_desktop_file_accepts_documents (EggDesktopFile *desktop_file); +gboolean egg_desktop_file_accepts_multiple (EggDesktopFile *desktop_file); +gboolean egg_desktop_file_accepts_uris (EggDesktopFile *desktop_file); + +char *egg_desktop_file_parse_exec (EggDesktopFile *desktop_file, + GSList *documents, + GError **error); + +gboolean egg_desktop_file_launch (EggDesktopFile *desktop_file, + GSList *documents, + GError **error, + ...) G_GNUC_NULL_TERMINATED; + +typedef enum { + EGG_DESKTOP_FILE_LAUNCH_CLEARENV = 1, + EGG_DESKTOP_FILE_LAUNCH_PUTENV, + EGG_DESKTOP_FILE_LAUNCH_SCREEN, + EGG_DESKTOP_FILE_LAUNCH_WORKSPACE, + EGG_DESKTOP_FILE_LAUNCH_DIRECTORY, + EGG_DESKTOP_FILE_LAUNCH_TIME, + EGG_DESKTOP_FILE_LAUNCH_FLAGS, + EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC, + EGG_DESKTOP_FILE_LAUNCH_RETURN_PID, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE, + EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID +} EggDesktopFileLaunchOption; + +/* Standard Keys */ +#define EGG_DESKTOP_FILE_GROUP "Desktop Entry" + +#define EGG_DESKTOP_FILE_KEY_TYPE "Type" +#define EGG_DESKTOP_FILE_KEY_VERSION "Version" +#define EGG_DESKTOP_FILE_KEY_NAME "Name" +#define EGG_DESKTOP_FILE_KEY_GENERIC_NAME "GenericName" +#define EGG_DESKTOP_FILE_KEY_NO_DISPLAY "NoDisplay" +#define EGG_DESKTOP_FILE_KEY_COMMENT "Comment" +#define EGG_DESKTOP_FILE_KEY_ICON "Icon" +#define EGG_DESKTOP_FILE_KEY_HIDDEN "Hidden" +#define EGG_DESKTOP_FILE_KEY_ONLY_SHOW_IN "OnlyShowIn" +#define EGG_DESKTOP_FILE_KEY_NOT_SHOW_IN "NotShowIn" +#define EGG_DESKTOP_FILE_KEY_TRY_EXEC "TryExec" +#define EGG_DESKTOP_FILE_KEY_EXEC "Exec" +#define EGG_DESKTOP_FILE_KEY_PATH "Path" +#define EGG_DESKTOP_FILE_KEY_TERMINAL "Terminal" +#define EGG_DESKTOP_FILE_KEY_MIME_TYPE "MimeType" +#define EGG_DESKTOP_FILE_KEY_CATEGORIES "Categories" +#define EGG_DESKTOP_FILE_KEY_STARTUP_NOTIFY "StartupNotify" +#define EGG_DESKTOP_FILE_KEY_STARTUP_WM_CLASS "StartupWMClass" +#define EGG_DESKTOP_FILE_KEY_URL "URL" + +/* 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__ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggsmclient-osx.c Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#define G_LOG_DOMAIN "EggSMClient" + +#include "eggsmclient.h" + +#define EGG_TYPE_SM_CLIENT_OSX (egg_sm_client_osx_get_type ()) +#define EGG_SM_CLIENT_OSX(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_SM_CLIENT_OSX, EggSMClientOSX)) +#define EGG_SM_CLIENT_OSX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_SM_CLIENT_OSX, EggSMClientOSXClass)) +#define EGG_IS_SM_CLIENT_OSX(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_SM_CLIENT_OSX)) +#define EGG_IS_SM_CLIENT_OSX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_SM_CLIENT_OSX)) +#define EGG_SM_CLIENT_OSX_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_SM_CLIENT_OSX, EggSMClientOSXClass)) + +typedef struct _EggSMClientOSX EggSMClientOSX; +typedef struct _EggSMClientOSXClass EggSMClientOSXClass; + +struct _EggSMClientOSX { + EggSMClient parent; + +} + +struct _EggSMClientOSXClass +{ + EggSMClientClass parent_class; + +}; + +static void sm_client_osx_startup (EggSMClient *client, + const char *client_id); +static void sm_client_osx_will_quit (EggSMClient *client, + gboolean will_quit); +static gboolean sm_client_osx_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation); + +static GdkFilterReturn sm_client_osx_filter (GdkXEvent *xevent, + GdkEvent *event, + gpointer data); + +G_DEFINE_TYPE (EggSMClientOSX, egg_sm_client_osx, EGG_TYPE_SM_CLIENT) + +static void +egg_sm_client_osx_init (EggSMClientOSX *osxclient) +{ + ; +} + +static void +egg_sm_client_osx_class_init (EggSMClientOSXClass *klass) +{ + EggSMClientClass *sm_client_class = EGG_SM_CLIENT_CLASS (klass); + + sm_client_class->startup = sm_client_osx_startup; + sm_client_class->will_quit = sm_client_osx_will_quit; + sm_client_class->end_session = sm_client_osx_end_session; +} + +EggSMClient * +egg_sm_client_osx_new (void) +{ + return g_object_new (EGG_TYPE_SM_CLIENT_OSX, NULL); +} + +static void +sm_client_osx_startup (EggSMClient *client) +{ + gdk_window_add_filter (NULL, sm_client_osx_filter, client); +} + +void +sm_client_osx_will_quit (EggSMClient *client, + gboolean will_quit) +{ + EggSMClientOSX *osxclient = (EggSMClientOSX *)client; + + if (will_quit) + { + /* OS X doesn't send another message. We're supposed to just + * quit after agreeing that it's OK. + * + * FIXME: do it from an idle handler though. + */ + egg_sm_client_quit (client); + } + else + { + /* FIXME: "respond to the event by returning a userCancelledErr + * error" + */ + } +} + +void +sm_client_osx_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation) +{ + EggSMClientOSX *osxclient = (EggSMClientOSX *)client; + FIXME_t event; + + switch (style) + { + case EGG_SM_CLIENT_END_SESSION_DEFAULT: + case EGG_SM_CLIENT_END_LOGOUT: + event = request_confirmation ? kAELogOut : kAEReallyLogOut; + break; + case EGG_SM_CLIENT_END_REBOOT: + event = request_confirmation ? kAEShowRestartDialog : kAERestart; + break; + case EGG_SM_CLIENT_END_SHUTDOWN: + event = request_confirmation ? kAEShowShutdownDialog : kAEShutDown; + break; + } + + /* FIXME: send event to loginwindow process */ +} + +static GdkFilterReturn +egg_sm_client_osx_filter (GdkXEvent *xevent, + GdkEvent *event, + gpointer data) +{ + EggSMClientOSX *osxclient = data; + EggSMClient *client = data; + NSEvent *nsevent = (NSEvent *)xevent; + + switch (FIXME_get_apple_event_type (nsevent)) + { + case kAEQuitApplication: + if (FIXME_app_is_a_foreground_app) + egg_sm_client_quit_requested (client); + else + egg_sm_client_quit (client); + return GDK_FILTER_REMOVE; + + default: + return GDK_FILTER_CONTINUE; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggsmclient-private.h Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,53 @@ +/* eggsmclient-private.h + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EGG_SM_CLIENT_PRIVATE_H__ +#define __EGG_SM_CLIENT_PRIVATE_H__ + +#include <gdkconfig.h> +#include "eggsmclient.h" + +G_BEGIN_DECLS + +GKeyFile *egg_sm_client_save_state (EggSMClient *client); +void egg_sm_client_quit_requested (EggSMClient *client); +void egg_sm_client_quit_cancelled (EggSMClient *client); +void egg_sm_client_quit (EggSMClient *client); + +#if defined (GDK_WINDOWING_X11) +# ifdef EGG_SM_CLIENT_BACKEND_XSMP +GType egg_sm_client_xsmp_get_type (void); +EggSMClient *egg_sm_client_xsmp_new (void); +# endif +# ifdef EGG_SM_CLIENT_BACKEND_DBUS +GType egg_sm_client_dbus_get_type (void); +EggSMClient *egg_sm_client_dbus_new (void); +# endif +#elif defined (GDK_WINDOWING_WIN32) +GType egg_sm_client_win32_get_type (void); +EggSMClient *egg_sm_client_win32_new (void); +#elif defined (GDK_WINDOWING_QUARTZ) +GType egg_sm_client_osx_get_type (void); +EggSMClient *egg_sm_client_osx_new (void); +#endif + +G_END_DECLS + + +#endif /* __EGG_SM_CLIENT_PRIVATE_H__ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggsmclient-win32.c Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "eggsmclient-private.h" +#include <gdk/gdk.h> + +#define WIN32_LEAN_AND_MEAN +#define UNICODE +#include <windows.h> + +#define EGG_TYPE_SM_CLIENT_WIN32 (egg_sm_client_win32_get_type ()) +#define EGG_SM_CLIENT_WIN32(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_SM_CLIENT_WIN32, EggSMClientWin32)) +#define EGG_SM_CLIENT_WIN32_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_SM_CLIENT_WIN32, EggSMClientWin32Class)) +#define EGG_IS_SM_CLIENT_WIN32(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_SM_CLIENT_WIN32)) +#define EGG_IS_SM_CLIENT_WIN32_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_SM_CLIENT_WIN32)) +#define EGG_SM_CLIENT_WIN32_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_SM_CLIENT_WIN32, EggSMClientWin32Class)) + +typedef struct _EggSMClientWin32 EggSMClientWin32; +typedef struct _EggSMClientWin32Class EggSMClientWin32Class; + +struct _EggSMClientWin32 { + EggSMClient parent; + + GAsyncQueue *msg_queue; +}; + +struct _EggSMClientWin32Class +{ + EggSMClientClass parent_class; + +}; + +static void sm_client_win32_startup (EggSMClient *client, + const char *client_id); +static void sm_client_win32_will_quit (EggSMClient *client, + gboolean will_quit); +static gboolean sm_client_win32_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation); + +static gpointer sm_client_thread (gpointer data); + +G_DEFINE_TYPE (EggSMClientWin32, egg_sm_client_win32, EGG_TYPE_SM_CLIENT) + +static void +egg_sm_client_win32_init (EggSMClientWin32 *win32) +{ + ; +} + +static void +egg_sm_client_win32_class_init (EggSMClientWin32Class *klass) +{ + EggSMClientClass *sm_client_class = EGG_SM_CLIENT_CLASS (klass); + + sm_client_class->startup = sm_client_win32_startup; + sm_client_class->will_quit = sm_client_win32_will_quit; + sm_client_class->end_session = sm_client_win32_end_session; +} + +EggSMClient * +egg_sm_client_win32_new (void) +{ + return g_object_new (EGG_TYPE_SM_CLIENT_WIN32, NULL); +} + +static void +sm_client_win32_startup (EggSMClient *client, + const char *client_id) +{ + EggSMClientWin32 *win32 = (EggSMClientWin32 *)client; + + /* spawn another thread to listen for logout signals on */ + win32->msg_queue = g_async_queue_new (); + g_thread_create (sm_client_thread, client, FALSE, NULL); +} + +static void +sm_client_win32_will_quit (EggSMClient *client, + gboolean will_quit) +{ + EggSMClientWin32 *win32 = (EggSMClientWin32 *)client; + + /* Can't push NULL onto a GAsyncQueue, so we add 1 to the value... */ + g_async_queue_push (win32->msg_queue, GINT_TO_POINTER (will_quit + 1)); +} + +static gboolean +sm_client_win32_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation) +{ + UINT uFlags = EWX_LOGOFF; + + switch (style) + { + case EGG_SM_CLIENT_END_SESSION_DEFAULT: + case EGG_SM_CLIENT_LOGOUT: + uFlags = EWX_LOGOFF; + break; + case EGG_SM_CLIENT_REBOOT: + uFlags = EWX_REBOOT; + break; + case EGG_SM_CLIENT_SHUTDOWN: + uFlags = EWX_POWEROFF; + break; + } + + /* There's no way to make ExitWindowsEx() show a logout dialog, so + * we ignore @request_confirmation. + */ + +#ifdef SHTDN_REASON_FLAG_PLANNED + ExitWindowsEx (uFlags, SHTDN_REASON_FLAG_PLANNED); +#else + ExitWindowsEx (uFlags, 0); +#endif + + return TRUE; +} + + +/* callbacks from logout-listener thread */ + +static gboolean +emit_quit_requested (gpointer smclient) +{ + gdk_threads_enter (); + egg_sm_client_quit_requested (smclient); + gdk_threads_leave (); + + return FALSE; +} + +static gboolean +emit_quit (gpointer smclient) +{ + EggSMClientWin32 *win32 = smclient; + + gdk_threads_enter (); + egg_sm_client_quit (smclient); + gdk_threads_leave (); + + g_async_queue_push (win32->msg_queue, GINT_TO_POINTER (1)); + return FALSE; +} + +static gboolean +emit_quit_cancelled (gpointer smclient) +{ + EggSMClientWin32 *win32 = smclient; + + gdk_threads_enter (); + egg_sm_client_quit_cancelled (smclient); + gdk_threads_leave (); + + g_async_queue_push (win32->msg_queue, GINT_TO_POINTER (1)); + return FALSE; +} + + +/* logout-listener thread */ + +static int +async_emit (EggSMClientWin32 *win32, GSourceFunc emitter) +{ + /* ensure message queue is empty */ + while (g_async_queue_try_pop (win32->msg_queue)) + ; + + /* Emit signal in the main thread and wait for a response */ + g_idle_add (emitter, win32); + return GPOINTER_TO_INT (g_async_queue_pop (win32->msg_queue)) - 1; +} + +LRESULT CALLBACK +sm_client_win32_window_procedure (HWND hwnd, + UINT message, + WPARAM wParam, + LPARAM lParam) +{ + EggSMClientWin32 *win32 = + (EggSMClientWin32 *)GetWindowLongPtr (hwnd, GWLP_USERDATA); + + switch (message) + { + case WM_QUERYENDSESSION: + return async_emit (win32, emit_quit_requested); + + case WM_ENDSESSION: + if (wParam) + { + /* The session is ending */ + async_emit (win32, emit_quit); + } + else + { + /* Nope, the session *isn't* ending */ + async_emit (win32, emit_quit_cancelled); + } + return 0; + + default: + return DefWindowProc (hwnd, message, wParam, lParam); + } +} + +static gpointer +sm_client_thread (gpointer smclient) +{ + HINSTANCE instance; + WNDCLASSEXW wcl; + ATOM klass; + HWND window; + MSG msg; + + instance = GetModuleHandle (NULL); + + memset (&wcl, 0, sizeof (WNDCLASSEX)); + wcl.cbSize = sizeof (WNDCLASSEX); + wcl.lpfnWndProc = sm_client_win32_window_procedure; + wcl.hInstance = instance; + wcl.lpszClassName = L"EggSmClientWindow"; + klass = RegisterClassEx (&wcl); + + window = CreateWindowEx (0, MAKEINTRESOURCE (klass), + L"EggSmClientWindow", 0, + 10, 10, 50, 50, GetDesktopWindow (), + NULL, instance, NULL); + SetWindowLongPtr (window, GWLP_USERDATA, (LONG_PTR)smclient); + + /* main loop */ + while (GetMessage (&msg, NULL, 0, 0)) + DispatchMessage (&msg); + + return NULL; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggsmclient-xsmp.c Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,1370 @@ +/* + * 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. + */ + +#include "../../config.h" + +#include "eggsmclient.h" +#include "eggsmclient-private.h" + +#include "eggdesktopfile.h" + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <X11/SM/SMlib.h> + +#include <gdk/gdk.h> + +#define EGG_TYPE_SM_CLIENT_XSMP (egg_sm_client_xsmp_get_type ()) +#define EGG_SM_CLIENT_XSMP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_SM_CLIENT_XSMP, EggSMClientXSMP)) +#define EGG_SM_CLIENT_XSMP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_SM_CLIENT_XSMP, EggSMClientXSMPClass)) +#define EGG_IS_SM_CLIENT_XSMP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_SM_CLIENT_XSMP)) +#define EGG_IS_SM_CLIENT_XSMP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_SM_CLIENT_XSMP)) +#define EGG_SM_CLIENT_XSMP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_SM_CLIENT_XSMP, EggSMClientXSMPClass)) + +typedef struct _EggSMClientXSMP EggSMClientXSMP; +typedef struct _EggSMClientXSMPClass EggSMClientXSMPClass; + +/* These mostly correspond to the similarly-named states in section + * 9.1 of the XSMP spec. Some of the states there aren't represented + * here, because we don't need them. SHUTDOWN_CANCELLED is slightly + * different from the spec; we use it when the client is IDLE after a + * ShutdownCancelled message, but the application is still interacting + * and doesn't know the shutdown has been cancelled yet. + */ +typedef enum +{ + XSMP_STATE_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; +} + +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 void +sm_client_xsmp_connect (EggSMClient *client) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *) client; + SmcCallbacks callbacks; + char *client_id; + char error_string_ret[256]; + + 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, + sizeof (error_string_ret), error_string_ret); + + if (!xsmp->connection) + { + g_warning ("Failed to connect to the session manager: %s\n", + error_string_ret[0] ? + error_string_ret : "no error message given"); + xsmp->state = XSMP_STATE_CONNECTION_CLOSED; + return; + } + + /* We expect a pointless initial SaveYourself if either (a) we + * didn't have an initial client ID, or (b) we DID have an initial + * client ID, but the server rejected it and gave us a new one. + */ + if (!xsmp->client_id || + (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); + } + + xsmp->state = XSMP_STATE_IDLE; +} + +static gboolean +sm_client_xsmp_set_initial_properties (gpointer user_data) +{ + EggSMClientXSMP *xsmp = user_data; + EggDesktopFile *desktop_file; + GPtrArray *clone, *restart; + char pid_str[64]; + + g_source_remove (xsmp->idle); + xsmp->idle = 0; + + if (egg_sm_client_get_mode () == EGG_SM_CLIENT_MODE_NO_RESTART) + xsmp->restart_style = SmRestartNever; + + /* Parse info out of desktop file */ + desktop_file = egg_get_desktop_file (); + if (desktop_file) + { + GKeyFile *key_file; + GError *err = NULL; + char *cmdline, **argv; + int argc; + + key_file = egg_desktop_file_get_key_file (desktop_file); + + if (xsmp->restart_style == SmRestartIfRunning) + { + if (g_key_file_has_key (key_file, EGG_DESKTOP_FILE_GROUP, + "X-GNOME-AutoRestart", NULL) && + g_key_file_get_boolean (key_file, EGG_DESKTOP_FILE_GROUP, + "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); + } + + 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); + + sm_client_xsmp_connect (client); + /* Do not initialize the properties until we reach the main. + * 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_set_initial_properties, client); +} + +static void +sm_client_xsmp_set_restart_command (EggSMClient *client, + int argc, + const char **argv) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + int i; + + g_strfreev (xsmp->restart_command); + + xsmp->restart_command = g_new (char *, argc + 1); + for (i = 0; i < argc; i++) + xsmp->restart_command[i] = g_strdup (argv[i]); + xsmp->restart_command[i] = NULL; + + xsmp->set_restart_command = TRUE; +} + +static void +sm_client_xsmp_will_quit (EggSMClient *client, + gboolean will_quit) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + + if (xsmp->state == XSMP_STATE_CONNECTION_CLOSED) + { + /* The session manager has already exited! Schedule a quit + * signal. + */ + xsmp->waiting_to_emit_quit = TRUE; + update_pending_events (xsmp); + return; + } + else if (xsmp->state == XSMP_STATE_SHUTDOWN_CANCELLED) + { + /* We received a ShutdownCancelled message while the application + * was interacting; Schedule a quit_cancelled signal. + */ + xsmp->waiting_to_emit_quit_cancelled = TRUE; + update_pending_events (xsmp); + return; + } + + g_return_if_fail (xsmp->state == XSMP_STATE_INTERACT); + + g_debug ("Sending InteractDone(%s)", will_quit ? "False" : "True"); + SmcInteractDone (xsmp->connection, !will_quit); + + if (will_quit && xsmp->need_save_state) + save_state (xsmp); + + g_debug ("Sending SaveYourselfDone(%s)", will_quit ? "True" : "False"); + SmcSaveYourselfDone (xsmp->connection, will_quit); + xsmp->state = XSMP_STATE_SAVE_YOURSELF_DONE; +} + +static gboolean +sm_client_xsmp_end_session (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation) +{ + EggSMClientXSMP *xsmp = (EggSMClientXSMP *)client; + int save_type; + + /* To end the session via XSMP, we have to send a + * SaveYourselfRequest. We aren't allowed to do that if anything + * else is going on, but we don't want to expose this fact to the + * application. So we do our best to patch things up here... + * + * In the worst case, this method might block for some length of + * time in process_ice_messages, but the only time that code path is + * honestly likely to get hit is if the application tries to end the + * session as the very first thing it does, in which case it + * probably won't actually block anyway. It's not worth gunking up + * the API to try to deal nicely with the other 0.01% of cases where + * this happens. + */ + + while (xsmp->state != XSMP_STATE_IDLE || + xsmp->expecting_initial_save_yourself) + { + /* If we're already shutting down, we don't need to do anything. */ + if (xsmp->shutting_down) + return TRUE; + + switch (xsmp->state) + { + case XSMP_STATE_START: + /* Force the connection to complete (or fail) now. */ + sm_client_xsmp_connect (client); + 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 +merge_keyfiles (GKeyFile *dest, GKeyFile *source) +{ + int g, k; + char **groups, **keys, *value; + + groups = g_key_file_get_groups (source, NULL); + for (g = 0; groups[g]; g++) + { + keys = g_key_file_get_keys (source, groups[g], NULL, NULL); + for (k = 0; keys[k]; k++) + { + value = g_key_file_get_value (source, groups[g], keys[k], NULL); + if (value) + { + g_key_file_set_value (dest, groups[g], keys[k], value); + g_free (value); + } + } + g_strfreev (keys); + } + g_strfreev (groups); +} + +static void +save_state (EggSMClientXSMP *xsmp) +{ + GKeyFile *state_file; + char *state_file_path, *data; + EggDesktopFile *desktop_file; + GPtrArray *restart; + int offset, fd; + + /* We set xsmp->state before emitting save_state, but our caller is + * responsible for setting it back afterward. + */ + xsmp->state = XSMP_STATE_SAVE_YOURSELF; + + state_file = egg_sm_client_save_state ((EggSMClient *)xsmp); + if (!state_file) + { + restart = generate_command (xsmp->restart_command, xsmp->client_id, NULL); + set_properties (xsmp, + ptrarray_prop (SmRestartCommand, restart), + NULL); + g_ptr_array_free (restart, TRUE); + delete_properties (xsmp, SmDiscardCommand, NULL); + return; + } + + desktop_file = egg_get_desktop_file (); + if (desktop_file) + { + GKeyFile *merged_file; + char *exec; + int i; + + merged_file = g_key_file_new (); + merge_keyfiles (merged_file, egg_desktop_file_get_key_file (desktop_file)); + merge_keyfiles (merged_file, state_file); + + 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 <X11/ICE/ICElib.h> +#include <fcntl.h> + +static void ice_error_handler (IceConn ice_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + IcePointer values); +static void ice_io_error_handler (IceConn ice_conn); +static void ice_connection_watch (IceConn ice_conn, + IcePointer client_data, + Bool opening, + IcePointer *watch_data); + +static void +ice_init (void) +{ + IceSetIOErrorHandler (ice_io_error_handler); + IceSetErrorHandler (ice_error_handler); + IceAddConnectionWatch (ice_connection_watch, NULL); +} + +static gboolean +process_ice_messages (IceConn ice_conn) +{ + IceProcessMessagesStatus status; + + gdk_threads_enter (); + status = IceProcessMessages (ice_conn, NULL, NULL); + gdk_threads_leave (); + + switch (status) + { + case IceProcessMessagesSuccess: + return TRUE; + + case IceProcessMessagesIOError: + sm_client_xsmp_disconnect (IceGetConnectionContext (ice_conn)); + return FALSE; + + case IceProcessMessagesConnectionClosed: + return FALSE; + + default: + g_assert_not_reached (); + } +} + +static gboolean +ice_iochannel_watch (GIOChannel *channel, + GIOCondition condition, + gpointer client_data) +{ + return process_ice_messages (client_data); +} + +static void +ice_connection_watch (IceConn ice_conn, + IcePointer client_data, + Bool opening, + IcePointer *watch_data) +{ + guint watch_id; + + if (opening) + { + GIOChannel *channel; + int fd = IceConnectionNumber (ice_conn); + + fcntl (fd, F_SETFD, fcntl (fd, F_GETFD, 0) | FD_CLOEXEC); + channel = g_io_channel_unix_new (fd); + watch_id = g_io_add_watch (channel, G_IO_IN | G_IO_ERR, + ice_iochannel_watch, ice_conn); + g_io_channel_unref (channel); + + *watch_data = GUINT_TO_POINTER (watch_id); + } + else + { + watch_id = GPOINTER_TO_UINT (*watch_data); + g_source_remove (watch_id); + } +} + +static void +ice_error_handler (IceConn ice_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + IcePointer values) +{ + /* Do nothing */ +} + +static void +ice_io_error_handler (IceConn ice_conn) +{ + /* Do nothing */ +} + +static void +smc_error_handler (SmcConn smc_conn, + Bool swap, + int offending_minor_opcode, + unsigned long offending_sequence, + int error_class, + int severity, + SmPointer values) +{ + /* Do nothing */ +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggsmclient.c Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <string.h> +#include <glib/gi18n.h> + +#include "eggsmclient.h" +#include "eggsmclient-private.h" + +static void egg_sm_client_debug_handler (const char *log_domain, + GLogLevelFlags log_level, + const char *message, + gpointer user_data); + +enum { + SAVE_STATE, + QUIT_REQUESTED, + QUIT_CANCELLED, + QUIT, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 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 EggSMClient *global_client; +static EggSMClientMode global_client_mode = EGG_SM_CLIENT_MODE_NORMAL; + +static void +egg_sm_client_init (EggSMClient *client) +{ + ; +} + +static void +egg_sm_client_class_init (EggSMClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (EggSMClientPrivate)); + + /** + * EggSMClient::save_state: + * @client: the client + * @state_file: a #GKeyFile to save state information into + * + * Emitted when the session manager has requested that the + * application save information about its current state. The + * application should save its state into @state_file, and then the + * session manager may then restart the application in a future + * session and tell it to initialize itself from that state. + * + * You should not save any data into @state_file's "start group" + * (ie, the %NULL group). Instead, applications should save their + * data into groups with names that start with the application name, + * and libraries that connect to this signal should save their data + * into groups with names that start with the library name. + * + * Alternatively, rather than (or in addition to) using @state_file, + * the application can save its state by calling + * egg_sm_client_set_restart_command() during the processing of this + * signal (eg, to include a list of files to open). + **/ + signals[SAVE_STATE] = + g_signal_new ("save_state", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, save_state), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * EggSMClient::quit_requested: + * @client: the client + * + * Emitted when the session manager requests that the application + * exit (generally because the user is logging out). The application + * should decide whether or not it is willing to quit (perhaps after + * asking the user what to do with documents that have unsaved + * changes) and then call egg_sm_client_will_quit(), passing %TRUE + * or %FALSE to give its answer to the session manager. (It does not + * need to give an answer before returning from the signal handler; + * it can interact with the user asynchronously and then give its + * answer later on.) If the application does not connect to this + * signal, then #EggSMClient will automatically return %TRUE on its + * behalf. + * + * The application should not save its session state as part of + * handling this signal; if the user has requested that the session + * be saved when logging out, then ::save_state will be emitted + * separately. + * + * If the application agrees to quit, it should then wait for either + * the ::quit_cancelled or ::quit signals to be emitted. + **/ + signals[QUIT_REQUESTED] = + g_signal_new ("quit_requested", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, quit_requested), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * EggSMClient::quit_cancelled: + * @client: the client + * + * Emitted when the session manager decides to cancel a logout after + * the application has already agreed to quit. After receiving this + * signal, the application can go back to what it was doing before + * receiving the ::quit_requested signal. + **/ + signals[QUIT_CANCELLED] = + g_signal_new ("quit_cancelled", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, quit_cancelled), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * EggSMClient::quit: + * @client: the client + * + * Emitted when the session manager wants the application to quit + * (generally because the user is logging out). The application + * should exit as soon as possible after receiving this signal; if + * it does not, the session manager may choose to forcibly kill it. + * + * Normally a GUI application would only be sent a ::quit if it + * agreed to quit in response to a ::quit_requested signal. However, + * this is not guaranteed; in some situations the session manager + * may decide to end the session without giving applications a + * chance to object. + **/ + signals[QUIT] = + g_signal_new ("quit", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EggSMClientClass, quit), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + +static gboolean sm_client_disable = FALSE; +static char *sm_client_state_file = NULL; +static char *sm_client_id = NULL; + +static 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 } +}; + +static gboolean +sm_client_post_parse_func (GOptionContext *context, + GOptionGroup *group, + gpointer data, + GError **error) +{ + EggSMClient *client = egg_sm_client_get (); + + if (EGG_SM_CLIENT_GET_CLASS (client)->startup) + EGG_SM_CLIENT_GET_CLASS (client)->startup (client, sm_client_id); + return TRUE; +} + +/** + * egg_sm_client_get_option_group: + * + * Creates a %GOptionGroup containing the session-management-related + * options. You should add this group to the application's + * %GOptionContext if you want to use #EggSMClient. + * + * Return value: the %GOptionGroup + **/ +GOptionGroup * +egg_sm_client_get_option_group (void) +{ + GOptionGroup *group; + + /* Use our own debug handler for the "EggSMClient" domain. */ + g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, + egg_sm_client_debug_handler, NULL); + + group = g_option_group_new ("sm-client", + _("Session Management Options"), + _("Show Session Management options"), + NULL, NULL); + g_option_group_add_entries (group, entries); + g_option_group_set_parse_hooks (group, NULL, sm_client_post_parse_func); + + return group; +} + +/** + * egg_sm_client_set_mode: + * @mode: an #EggSMClient mode + * + * Sets the "mode" of #EggSMClient as follows: + * + * %EGG_SM_CLIENT_MODE_DISABLED: Session management is completely + * disabled. The application will not even connect to the session + * manager. (egg_sm_client_get() will still return an #EggSMClient, + * but it will just be a dummy object.) + * + * %EGG_SM_CLIENT_MODE_NO_RESTART: The application will connect to + * the session manager (and thus will receive notification when the + * user is logging out, etc), but will request to not be + * automatically restarted with saved state in future sessions. + * + * %EGG_SM_CLIENT_MODE_NORMAL: The default. #EggSMCLient will + * function normally. + * + * This must be called before the application's main loop begins. + **/ +void +egg_sm_client_set_mode (EggSMClientMode mode) +{ + global_client_mode = mode; +} + +/** + * egg_sm_client_get_mode: + * + * Gets the global #EggSMClientMode. See egg_sm_client_set_mode() + * for details. + * + * Return value: the global #EggSMClientMode + **/ +EggSMClientMode +egg_sm_client_get_mode (void) +{ + return global_client_mode; +} + +/** + * egg_sm_client_get: + * + * Returns the master #EggSMClient for the application. + * + * On platforms that support saved sessions (ie, POSIX/X11), the + * application will only request to be restarted by the session + * manager if you call egg_set_desktop_file() to set an application + * desktop file. In particular, if the desktop file contains the key + * "X + * + * Return value: the master #EggSMClient. + **/ +EggSMClient * +egg_sm_client_get (void) +{ + if (!global_client) + { + if (global_client_mode != EGG_SM_CLIENT_MODE_DISABLED && + !sm_client_disable) + { +#if defined (GDK_WINDOWING_WIN32) + global_client = egg_sm_client_win32_new (); +#elif defined (GDK_WINDOWING_QUARTZ) + global_client = egg_sm_client_osx_new (); +#else + /* If both D-Bus and XSMP are compiled in, try D-Bus first + * and fall back to XSMP if D-Bus session management isn't + * available. + */ +# ifdef EGG_SM_CLIENT_BACKEND_DBUS + global_client = egg_sm_client_dbus_new (); +# endif +# ifdef EGG_SM_CLIENT_BACKEND_XSMP + if (!global_client) + global_client = egg_sm_client_xsmp_new (); +# endif +#endif + } + + /* Fallback: create a dummy client, so that callers don't have + * to worry about a %NULL return value. + */ + if (!global_client) + global_client = g_object_new (EGG_TYPE_SM_CLIENT, NULL); + } + + return global_client; +} + +/** + * egg_sm_client_is_resumed: + * @client: the client + * + * Checks whether or not the current session has been resumed from + * a previous saved session. If so, the application should call + * egg_sm_client_get_state_file() and restore its state from the + * returned #GKeyFile. + * + * Return value: %TRUE if the session has been resumed + **/ +gboolean +egg_sm_client_is_resumed (EggSMClient *client) +{ + g_return_val_if_fail (client == global_client, FALSE); + + return sm_client_state_file != NULL; +} + +/** + * egg_sm_client_get_state_file: + * @client: the client + * + * If the application was resumed by the session manager, this will + * return the #GKeyFile containing its state from the previous + * session. + * + * Note that other libraries and #EggSMClient itself may also store + * state in the key file, so if you call egg_sm_client_get_groups(), + * on it, the return value will likely include groups that you did not + * put there yourself. (It is also not guaranteed that the first + * group created by the application will still be the "start group" + * when it is resumed.) + * + * Return value: the #GKeyFile containing the application's earlier + * state, or %NULL on error. You should not free this key file; it + * is owned by @client. + **/ +GKeyFile * +egg_sm_client_get_state_file (EggSMClient *client) +{ + EggSMClientPrivate *priv = EGG_SM_CLIENT_GET_PRIVATE (client); + char *state_file_path; + GError *err = NULL; + + g_return_val_if_fail (client == global_client, NULL); + + if (!sm_client_state_file) + return NULL; + if (priv->state_file) + return priv->state_file; + + if (!strncmp (sm_client_state_file, "file://", 7)) + state_file_path = g_filename_from_uri (sm_client_state_file, NULL, NULL); + else + state_file_path = g_strdup (sm_client_state_file); + + priv->state_file = g_key_file_new (); + if (!g_key_file_load_from_file (priv->state_file, state_file_path, 0, &err)) + { + g_warning ("Could not load SM state file '%s': %s", + sm_client_state_file, err->message); + g_clear_error (&err); + g_key_file_free (priv->state_file); + priv->state_file = NULL; + } + + g_free (state_file_path); + return priv->state_file; +} + +/** + * egg_sm_client_set_restart_command: + * @client: the client + * @argc: the length of @argv + * @argv: argument vector + * + * Sets the command used to restart @client if it does not have a + * .desktop file that can be used to find its restart command. + * + * This can also be used when handling the ::save_state signal, to + * save the current state via an updated command line. (Eg, providing + * a list of filenames to open when the application is resumed.) + **/ +void +egg_sm_client_set_restart_command (EggSMClient *client, + int argc, + const char **argv) +{ + g_return_if_fail (EGG_IS_SM_CLIENT (client)); + + if (EGG_SM_CLIENT_GET_CLASS (client)->set_restart_command) + EGG_SM_CLIENT_GET_CLASS (client)->set_restart_command (client, argc, argv); +} + +/** + * egg_sm_client_will_quit: + * @client: the client + * @will_quit: whether or not the application is willing to quit + * + * This MUST be called in response to the ::quit_requested signal, to + * indicate whether or not the application is willing to quit. The + * application may call it either directly from the signal handler, or + * at some later point (eg, after asynchronously interacting with the + * user). + * + * If the application does not connect to ::quit_requested, + * #EggSMClient will call this method on its behalf (passing %TRUE + * for @will_quit). + * + * After calling this method, the application should wait to receive + * either ::quit_cancelled or ::quit. + **/ +void +egg_sm_client_will_quit (EggSMClient *client, + gboolean will_quit) +{ + g_return_if_fail (EGG_IS_SM_CLIENT (client)); + + if (EGG_SM_CLIENT_GET_CLASS (client)->will_quit) + EGG_SM_CLIENT_GET_CLASS (client)->will_quit (client, will_quit); +} + +/** + * egg_sm_client_end_session: + * @style: a hint at how to end the session + * @request_confirmation: whether or not the user should get a chance + * to confirm the action + * + * Requests that the session manager end the current session. @style + * indicates how the session should be ended, and + * @request_confirmation indicates whether or not the user should be + * given a chance to confirm the logout/reboot/shutdown. Both of these + * flags are merely hints though; the session manager may choose to + * ignore them. + * + * Return value: %TRUE if the request was sent; %FALSE if it could not + * be (eg, because it could not connect to the session manager). + **/ +gboolean +egg_sm_client_end_session (EggSMClientEndStyle style, + gboolean request_confirmation) +{ + EggSMClient *client = egg_sm_client_get (); + + g_return_val_if_fail (EGG_IS_SM_CLIENT (client), FALSE); + + if (EGG_SM_CLIENT_GET_CLASS (client)->end_session) + { + return EGG_SM_CLIENT_GET_CLASS (client)->end_session (client, style, + request_confirmation); + } + else + return FALSE; +} + +/* Signal-emitting callbacks from platform-specific code */ + +GKeyFile * +egg_sm_client_save_state (EggSMClient *client) +{ + GKeyFile *state_file; + char *group; + + g_return_val_if_fail (client == global_client, NULL); + + state_file = g_key_file_new (); + + g_debug ("Emitting save_state"); + g_signal_emit (client, signals[SAVE_STATE], 0, state_file); + g_debug ("Done emitting save_state"); + + group = g_key_file_get_start_group (state_file); + if (group) + { + g_free (group); + return state_file; + } + else + { + g_key_file_free (state_file); + return NULL; + } +} + +void +egg_sm_client_quit_requested (EggSMClient *client) +{ + g_return_if_fail (client == global_client); + + if (!g_signal_has_handler_pending (client, signals[QUIT_REQUESTED], 0, FALSE)) + { + g_debug ("Not emitting quit_requested because no one is listening"); + egg_sm_client_will_quit (client, TRUE); + return; + } + + g_debug ("Emitting quit_requested"); + g_signal_emit (client, signals[QUIT_REQUESTED], 0); + g_debug ("Done emitting quit_requested"); +} + +void +egg_sm_client_quit_cancelled (EggSMClient *client) +{ + g_return_if_fail (client == global_client); + + g_debug ("Emitting quit_cancelled"); + g_signal_emit (client, signals[QUIT_CANCELLED], 0); + g_debug ("Done emitting quit_cancelled"); +} + +void +egg_sm_client_quit (EggSMClient *client) +{ + g_return_if_fail (client == global_client); + + g_debug ("Emitting quit"); + g_signal_emit (client, signals[QUIT], 0); + g_debug ("Done emitting quit"); + + /* FIXME: should we just call gtk_main_quit() here? */ +} + +static void +egg_sm_client_debug_handler (const char *log_domain, + GLogLevelFlags log_level, + const char *message, + gpointer user_data) +{ + static int debug = -1; + + if (debug < 0) + debug = (g_getenv ("EGG_SM_CLIENT_DEBUG") != NULL); + + if (debug) + g_log_default_handler (log_domain, log_level, message, NULL); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libeggsmclient/eggsmclient.h Mon Mar 03 18:42:36 2008 +0000 @@ -0,0 +1,117 @@ +/* eggsmclient.h + * Copyright (C) 2007 Novell, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EGG_SM_CLIENT_H__ +#define __EGG_SM_CLIENT_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define EGG_TYPE_SM_CLIENT (egg_sm_client_get_type ()) +#define EGG_SM_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_SM_CLIENT, EggSMClient)) +#define EGG_SM_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_SM_CLIENT, EggSMClientClass)) +#define EGG_IS_SM_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_SM_CLIENT)) +#define EGG_IS_SM_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_SM_CLIENT)) +#define EGG_SM_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_SM_CLIENT, EggSMClientClass)) + +typedef struct _EggSMClient EggSMClient; +typedef struct _EggSMClientClass EggSMClientClass; +typedef struct _EggSMClientPrivate EggSMClientPrivate; + +typedef enum { + EGG_SM_CLIENT_END_SESSION_DEFAULT, + EGG_SM_CLIENT_LOGOUT, + EGG_SM_CLIENT_REBOOT, + EGG_SM_CLIENT_SHUTDOWN +} EggSMClientEndStyle; + +typedef enum { + EGG_SM_CLIENT_MODE_DISABLED, + EGG_SM_CLIENT_MODE_NO_RESTART, + EGG_SM_CLIENT_MODE_NORMAL +} EggSMClientMode; + +struct _EggSMClient +{ + GObject parent; + +}; + +struct _EggSMClientClass +{ + GObjectClass parent_class; + + /* signals */ + void (*save_state) (EggSMClient *client, + GKeyFile *state_file); + + void (*quit_requested) (EggSMClient *client); + void (*quit_cancelled) (EggSMClient *client); + void (*quit) (EggSMClient *client); + + /* virtual methods */ + void (*startup) (EggSMClient *client, + const char *client_id); + void (*set_restart_command) (EggSMClient *client, + int argc, + const char **argv); + void (*will_quit) (EggSMClient *client, + gboolean will_quit); + gboolean (*end_session) (EggSMClient *client, + EggSMClientEndStyle style, + gboolean request_confirmation); + + /* Padding for future expansion */ + void (*_egg_reserved1) (void); + void (*_egg_reserved2) (void); + void (*_egg_reserved3) (void); + void (*_egg_reserved4) (void); +}; + +GType egg_sm_client_get_type (void) G_GNUC_CONST; + +GOptionGroup *egg_sm_client_get_option_group (void); + +/* Initialization */ +void egg_sm_client_set_mode (EggSMClientMode mode); +EggSMClientMode egg_sm_client_get_mode (void); +EggSMClient *egg_sm_client_get (void); + +/* Resuming a saved session */ +gboolean egg_sm_client_is_resumed (EggSMClient *client); +GKeyFile *egg_sm_client_get_state_file (EggSMClient *client); + +/* Alternate means of saving state */ +void egg_sm_client_set_restart_command (EggSMClient *client, + int argc, + const char **argv); + +/* Handling "quit_requested" signal */ +void egg_sm_client_will_quit (EggSMClient *client, + gboolean will_quit); + +/* Initiate a logout/reboot/shutdown */ +gboolean egg_sm_client_end_session (EggSMClientEndStyle style, + gboolean request_confirmation); + +G_END_DECLS + + +#endif /* __EGG_SM_CLIENT_H__ */