Gnome Session Management
========================

Quick Introduction To Using GnomeClient
---------------------------------------
 
These are the key steps to doing SM with gnome-client.c:

static gint
save_yourself (GnomeClient *client, gint phase, GnomeSaveStyle save_style,
               gint is_shutdown, GnomeInteractStyle interact_style,
               gint is_fast, gpointer client_data)
{
        gchar* args[4] = { "rm", "-r", NULL, NULL };

        gnome_config_push_prefix (gnome_client_get_config_prefix (client));

        gnome_config_set... (..)
        gnome_config_set... (..)
        gnome_config_set... (..)

        gnome_config_pop_prefix ();

        args[2] = gnome_config_get_real_path
                    (gnome_client_get_config_prefix (client));
        gnome_client_set_discard_command (client, 3, args);

        return TRUE;
}

int
main (int argc, char** argv)
{
        GnomeClient* client;

        ...

        gnome_init.. (...);

        ...

        client = gnome_master_client ();
        gtk_signal_connect (GTK_OBJECT (client), "save_yourself",
                            GTK_SIGNAL_FUNC (save_yourself), NULL);
        gtk_signal_connect (GTK_OBJECT (client), "die",
                            GTK_SIGNAL_FUNC (gtk_main_quit), NULL);

        gnome_config_push_prefix (gnome_client_get_config_prefix (client));

        ... = gnome_config_get... (..)
        ... = gnome_config_get... (..)
        ... = gnome_config_get... (..)

        gnome_config_pop_prefix ();

        ...

        gtk_main()

        ...

}

Variations are possible but the above framework is where apps 
should start. Some details on interacting with the user during
a save_yourself are given below.

Note that the value returned by gnome_client_get_config_prefix
will change over time. You should not write config information
to this prefix outside the save_yourself handler. The config
which the above code will load for copies of the program which
have NOT been restarted by the SM is located under the prefix
gnome_client_get_global_config_prefix() and may be written safely
at any time.

The save_yourself stuff should write out enough information about
your state to enable you to remap all your windows (excluding
any transients that no one will miss). You should set a different
value for the WM_WINDOW_ROLE on each window using gdk_window_set_role
(preferably before you map the window but definitely before you
leave save_yourself).

When restoring the information (in main() above) you should put
the same WM_WINDOW_ROLE on each window which you map on start up
that the equivalent window had when you saved it. This probably
means that you have to save the role values. You should not save
or restore information that the WM normally maintains (such as
geometry, desktop, etc.)

see gnome-client.h for further details.

NB:  Do not destroy the gnome master client as this breaks the 
     operation of the protocol and in certain circumstances can 
     crash libSM. If you wish to avoid being restarted by the
     session manager then call gnome_client_set_restart_hint. 

User Interaction During A Session Save
--------------------------------------

There may be several apps which all want to interact with the user
during the course of a session save. In order to avoid conflicts,
these apps have to wait until the session manager grants them 
permission before they contact the user. 

The key steps to interacting with the user using gnome-client.c are 
illustrated in this toy example:

void save_more (gint reply, gpointer data)
{
	/* This will be the gnome_master_client for all normal apps: */
	GnomeClient* client  = (GnomeClient*)data;

	if (reply != GNOME_YES)
		return;

        gnome_config_push_prefix (gnome_client_get_config_prefix (client));

        gnome_config_set... (..)
        gnome_config_set... (..)
        gnome_config_set... (..)

        gnome_config_pop_prefix ();

	if (... something went wrong ...) {
		GtkWidget *error_dialog;

		error_dialog = gnome_error_dialog (_("Could not save!"));
		gnome_client_save_error_dialog (client, 
						GNOME_DIALOG (error_dialog));
	}
}

static gint
save_yourself (GnomeClient *client, gint phase, GnomeSaveStyle save_style,
               gint is_shutdown, GnomeInteractStyle interact_style,
               gint is_fast, gpointer client_data)
{
        gchar* args[4] = { "rm", "-r", NULL, NULL };
	GtkWidget *save_more_dialog;
	
        gnome_config_push_prefix (gnome_client_get_config_prefix (client));

        gnome_config_set... (..)
        gnome_config_set... (..)
        gnome_config_set... (..)

        gnome_config_pop_prefix ();

        args[2] = gnome_config_get_real_path
                    (gnome_client_get_config_prefix (client));
        gnome_client_set_discard_command (client, 3, args);

	save_more_dialog = gnome_question_dialog_new (_("Save more detail ?"),
						      save_more, client);

	gnome_client_save_any_dialog (client, GNOME_DIALOG (save_more_dialog));

        return TRUE;
}

The value of the interact_style parameter passed to save_yourself determines
whether these dialogs will actually be shown to the user. This allows the
user to request session saves which do not require interaction. You should
use gnome_client_save_error_dialog for dialogs which report errors and
gnome_client_save_any_dialog for other dialogs. The dialogs will be shown
to the user in the order that you call these functions.

Note that the dialog is always modal but is not shown until the session
manager grants permission. Therefore, you must use callbacks to interpret
the users input into the dialog. You can use any GnomeDialog and add extra 
widgets or buttons into it in the usual way.

If the dialog is shown as part of a session logout then an extra button
will automatically be appended which allows the user to cancel the logout.

The Gnome Session Manager
-------------------------

The gnome-session manager implements the SM protocol for X11R6. (specified 
in xc/docs/specs/SM.txt from xc-3.tar.gz in the X distribution). Additional 
details of the support for the SM protocol are given in Appendix A.

This remainder of this document describes any extensions to the SM protocol 
which are implemented by gnome-session.

Avoiding Race Conditions During Session Start Up
------------------------------------------------

The standard SM protocol does not specify a start up ordering for the apps
that form the clients in a session. As a result, apps have traditionally
been faced with race conditions when they rely on other members of the 
session to provide them with services or initialise the environment.

gnome-session avoids these problems by allocating "priority" levels to
the apps in a session. The priority levels range from 0 to 99 with
a default value of 50.

gnome-session starts up a session by executing any apps in priority level 0. 
It then waits until all those apps have registered as clients with the 
session manager before executing any apps with priority level 1.
gnome-session continues in this fashion until all the apps in the session
have been restarted.

gnome-session gives up waiting for clients that fail to register after
a configurable period which defaults to 30 seconds and then goes onto
the next priority level.

The priority levels assigned to apps are ultimately under the control
of the user so that any conflicts can always be resolved. However, apps
which need to start in priority levels outside the default 50 level may 
suggest a priority level to gnome-session. Providing apps make sensible 
and consistent suggestions for these priority levels then the user need 
never become involved.

a) Details of app compliance.
   --------------------------
 
1) The app must implement the rudiments of the standard SM protocol.

2) The app must delay connection to the session manager until it has
   performed whatever initialisation steps might be necessary to allow
   apps in later priority levels to start up correctly.

   libgnomeui normally connects to gnome-session during gnome_init().
   Apps which can not perform their initialisation steps before
   calling gnome_init() may delay the connection by calling:

	gnome_client_disable_master_connection()

   Once the initialisation steps have been completed the connection
   should then be made by calling:

	gnome_client_connect (gnome_master_client());

   Apps which do not use libgnomeui to connect to the session manager
   should ensure that they have completed their initialisation steps
   before calling SmcOpenConnection().

3) Once the app has connected then it must suggest a priority level
   in which it should be restarted to the session manager (unless it 
   has no reason to run outside the default 50 level).

   libgnomeui provides the following API for sending this suggestion:

	gnome_client_set_priority (gnome_master_client (), priority);

   Apps which do not use libgnomeui may send the suggestion by
   including a SMProp with name "_GSM_Priority" and type SmCARD8
   among the properties in a call to SmcSetProperties().

4) If the app is included among the apps that are started up for every
   brand new gnome user then it should record its priority level in 
   the gsm/default.in file in the gnome-core module in cvs.

   The records in this file take the following format where <n> is
   a number between 0 and the value in the num_clients= field:

   <n>,id=default<n>
   <n>,Priority=<the priority level>
   <n>,RestartCommand=<command line> <client id option> default<n>

   libgnomeui apps always use "--sm-client-id" as the <client id option>.

   Apps which not use libgnomeui may use any option names to set their
   client id but it is helpful to the maintainers of default.session
   files if they support the "--sm-client-id" option.


b) An Example server: esd
   ----------------------

Since many of the servers that might wish to participate in this protocol
extension are unlikely to implement SM at present the following example
showing how esd implements SM may be useful:

session.c:
-------------
/* Session management support for a server to support 
   gnome-session start up, close down and re-spawning. 

   Author: Felix Bellaby <felix@pooh.u-net.com> */

#include "config.h"
#include "esd-server.h"
#ifdef HAVE_X11_SM_SMLIB_H
#include <X11/SM/SMlib.h>
#include <pwd.h>
#define GnomePriority "_GSM_Priority"
#endif

/* imported stuff: esd options */
extern int listen_socket;
extern int esd_port;
extern int esd_beeps;

/* exported stuff: */
void session_init (char* argv0, char* id);
void poll_ice_msgs(void);
void add_ice_fd (fd_set *rd_fds, int *max_fd);

#ifdef HAVE_X11_SM_SMLIB_H
/* Useful if you want to change the SM properties outside this file. */
SmcConn sm_conn = NULL;
#endif

/* static stuff: */
#ifdef HAVE_X11_SM_SMLIB_H

static IceConn ice_conn = NULL;
static int     ice_fd = -1;
static int     set_props = 0;
static char*   sm_client_id = "";
static char*   command = NULL;

static void
callback_die(SmcConn smc_conn, SmPointer client_data)
{
    esd_client_t * client = esd_clients_list;

    SmcCloseConnection(smc_conn, 0, NULL);

    /* this graceful closedown stuff needs to be kept in sync: */
    esd_audio_close();

    close( listen_socket );

    while ( client != NULL )
    {
	close( client->fd );
	client = client->next;
    }

    exit( 0 );
}

static void
callback_save_yourself(SmcConn smc_conn, SmPointer client_data,
		       int save_style, Bool shutdown, int interact_style,
		       Bool fast)
{
    if (set_props) {
        struct {
            SmPropValue program[1];
            SmPropValue user[1];
            SmPropValue hint[1];
            SmPropValue priority[1];
            SmPropValue restart[15];
	} vals;
	SmProp prop[] = {
	    { SmProgram,          SmLISTofARRAY8, 1, vals.program },
	    { SmUserID,           SmLISTofARRAY8, 1, vals.user    },
	    { SmRestartStyleHint, SmCARD8,        1, vals.hint    },
	    { GnomePriority,      SmCARD8,        1, vals.priority},
	    { SmCloneCommand,     SmLISTofARRAY8, 0, vals.restart },
	    { SmRestartCommand,   SmLISTofARRAY8, 0, vals.restart }
	};
	SmProp *props[] = { 
	    &prop[0], 
	    &prop[1], 
	    &prop[2],
	    &prop[3], 
	    &prop[4],
	    &prop[5]
	};
	char           priority = 5;
	char           restart_style = SmRestartImmediately;
	struct passwd *pw = getpwuid (getuid());
	int            n = 0, i;

	char           port[10];
	char           rate[10];
	char           as[10];
	
	vals.program->value  = command;
	vals.program->length = strlen(vals.program->value);
	
	vals.user->value  = pw ? pw->pw_name : "";
	vals.user->length = strlen(vals.user->value);
	
	vals.hint->value  = &restart_style;
	vals.hint->length = 1;
	
	vals.priority->value  = &priority;
	vals.priority->length = 1;

	/* This stuff MUST be kept in sync with esd.c: */
	sprintf (port, "%9d", esd_port);
	sprintf (rate, "%9d", esd_audio_rate);
	sprintf (as, "%9d", esd_autostandby_secs);
	
	vals.restart[n++].value = command;
	vals.restart[n++].value = "-port";
	vals.restart[n++].value = port;
	vals.restart[n++].value = "-r";
	vals.restart[n++].value = rate;
	if (! esd_beeps) {
	    vals.restart[n++].value = "-nobeeps";
	}
	if (esd_audio_format & ESD_BITS8)
	    vals.restart[n++].value = "-b";
	if (esd_audio_device) {
	    vals.restart[n++].value = "-d";
	    vals.restart[n++].value = esd_audio_device;
	}
	if (esd_autostandby_secs > -1) {
	    vals.restart[n++].value = "-as";
	    vals.restart[n++].value = as;
	}
	/* end of stuff to keep in sync */
	
	prop[4].num_vals = n;

	vals.restart[n++].value = "--sm-client-id";
	vals.restart[n++].value = sm_client_id;
	
	prop[5].num_vals = n;

	for (i = 0; i < n; i++)
	    vals.restart[i].length = strlen(vals.restart[i].value);
		
	SmcSetProperties(smc_conn, sizeof(props)/sizeof(SmProp*), props);

	set_props = 0;
    }

    /* Nothing to save */
    SmcSaveYourselfDone(smc_conn, 1);
}

static void 
callback_shutdown_cancelled(SmcConn smc_conn, SmPointer client_data)
{
    /* We are not really interested in this message. */
}

static void 
callback_save_complete(SmcConn smc_conn, SmPointer client_data)
{
    /* We are not really interested in this message. */
}

static void 
ice_io_error_handler(IceConn connection)
{
    /* The less we do here the better - the default handler does an
       exit(1) instead of closing the losing connection. */
}    

static void 
process_ice_msgs(void)
{
    IceProcessMessagesStatus status;

    status = IceProcessMessages(ice_conn, NULL, NULL);

    if (status == IceProcessMessagesIOError) {
	SmcCloseConnection (sm_conn, 0, NULL);
	ice_fd   = -1;
	ice_conn = NULL; 
	sm_conn  = NULL;
    }
}

void 
session_init(char* argv0, char* id)
{
    SmcCallbacks        callbacks;

    callbacks.save_yourself.callback = callback_save_yourself;
    callbacks.die.callback = callback_die;
    callbacks.save_complete.callback = callback_save_complete;
    callbacks.shutdown_cancelled.callback = callback_shutdown_cancelled;
    
    callbacks.save_yourself.client_data =
        callbacks.die.client_data =
        callbacks.save_complete.client_data =
        callbacks.shutdown_cancelled.client_data = (SmPointer) NULL;

    command = argv0;
    sm_client_id = id;
    
    IceSetIOErrorHandler (ice_io_error_handler);

    if (getenv("SESSION_MANAGER")) {
        char  error_string_ret[4096] = "";
	char* client_id = NULL; 

	if (sm_client_id)
	    client_id = strdup (sm_client_id);

	sm_conn = SmcOpenConnection(NULL, NULL, SmProtoMajor, SmProtoMinor,
				    SmcSaveYourselfProcMask | SmcDieProcMask |
				    SmcSaveCompleteProcMask |
				    SmcShutdownCancelledProcMask,
				    &callbacks, client_id, &sm_client_id,
				    4096, error_string_ret);

	if (!client_id)
	    set_props = 1;
	else {
	    set_props = strcmp (client_id, sm_client_id);
	    free(client_id);
	}

	if (error_string_ret[0])
	    fprintf(stderr, "While connecting to session manager:\n%s.",
		    error_string_ret);
    }
    if (sm_conn) {
        ice_conn = SmcGetIceConnection(sm_conn);
	ice_fd = IceConnectionNumber(ice_conn);

	/* Not really needed... */
	fcntl(ice_fd, F_SETFD, fcntl(ice_fd, F_GETFD, 0) | FD_CLOEXEC);
    }
}

void 
poll_ice_msgs(void)
{
    if (ice_fd != -1) {
	fd_set rd_fds;
	struct timeval timeout;

        timeout.tv_sec = 0;
	timeout.tv_usec = 0;
	FD_ZERO( &rd_fds );
	FD_SET( ice_fd, &rd_fds );

	if (select( ice_fd + 1, &rd_fds, NULL, NULL, &timeout ))
	    process_ice_msgs();
    }
}

void add_ice_fd (fd_set *rd_fds, int *max_fd)
{
    if (ice_fd != -1) {
	FD_SET( ice_fd, rd_fds );
	if (ice_fd > *max_fd)
	    *max_fd = ice_fd;
    }
}

#else /* HAVE_X11_SM_SMLIB_H */ 

void 
session_init(char* argv0, char* id)
{
    /* Nothing to do */ 
}

void 
poll_ice_msgs(void)
{
    /* Nothing to do */ 
}

void add_ice_fd (fd_set *rd_fds, int *max_fd)
{
    /* Nothing to do */
}

#endif /* HAVE_X11_SM_SMLIB_H */ 
---------

The session_init() function is called after esd has created the socket on
which it accepts connections from its clients. It is passed the value of 
argv[0] and the client id specified on the command line using the
"--sm-client-id" option.

The add_ice_fd() function is called before entering a blocking select on 
the sockets which esd uses and adds the ICE connection fd into the select 
so that gnome-session can get esd's attention when necessary.

The poll_ice_msgs() function checks for messages from gnome-session and 
dispatches them to the callback_* functions detailed above.

Most of the session.c file could be used for any server. However, there are 
two pieces of esd specific code.

Firstly, the callback_die() function performs the steps required to close 
down the server gracefully (typically on a session logout).

Secondly, the callback_save_yourself() function sets a gnome-session priority 
level of 5 (because E starts at level 10), requests that gnome-session 
respawns the server if it dies and specifies the command line arguments that 
are peculiar to esd.

c) Legacy app support
   ------------------

gnome-session provides some support for controlling the start up order of 
apps that do not implement this protocol extension. 

Any app can be assigned a priority level within the default.in or 
~/.gnome/session files. gnome-session will provide a GUI for the user to
simplify this task.

X11R6 SM compliant apps are all handled as if they implemented the protocol
extension and performed their initialisation steps before connecting to 
gnome-session. This is an approximation of the truth.

X11R5 SM compliant apps are handled by gnome-smproxy and delay their
connections to gnome-session until they map their first windows on the 
X display. They may well have completed any initialisation steps by then.

Non-SM apps are started within their allocated priority level but no
attempt is made to wait for them to complete initialisation. Non-SM apps 
must be entered into the default.in and ~/.gnome/session files with a
record in the following format so that gnome-session does not wait for
them to register: 

    <n>,id=ghost
    <n>,Priority=<the proirity level>
    <n>,RestartCommand=<command line> 

That is it as of 01-28-99.

Felix 

<felix@pooh.u-net.com>


APPENDIX A: X11R6 SM Protocol Support
-------------------------------------

The SM protocol is a bit vague about the types that should be used for
the properties that are set on the session manager. gnome-session uses
the types recommended for POSIX systems in the protocol specs:

------------------------------------
Name                 Type           
------------------------------------
SmCloneCommand       SmLISTofARRAY8 
SmCurrentDirectory   SmARRAY8       
SmDiscardCommand     SmLISTofARRAY8 
SmEnvironment        SmLISTofARRAY8 
SmProcessID          SmARRAY8       
SmProgram            SmARRAY8       
SmRestartCommand     SmLISTofARRAY8 
SmResignCommand      SmLISTofARRAY8 
SmRestartStyleHint   SmCARD8        
SmShutdownCommand    SmLISTofARRAY8 
SmUserID             SmARRAY8       
------------------------------------

In addition, gnome-session exactly reproduces the behavior of xsm when sent 
a SmDiscardCommand of type SmARRAY8. However, the SmLISTofARRAY8 type is 
strongly recommmended because the SmARRAY8 type commands are always run on 
the host running the session manager whilst the SmLISTofARRAY8 type command
will be run on the host running the client.

xsm does not send a SaveComplete message in response to the initial
SaveYourself message sent on registration (section 7.2 of the specs).
gnome-session does not reproduce this violation of the protocol as
it might leave properly compliant clients in a frozen state.

xsm does not support the SmcSaveYourselfRequest call. gnome-session does.