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 is a number between 0 and the value in the num_clients= field: ,id=default ,Priority= ,RestartCommand= default libgnomeui apps always use "--sm-client-id" as the . 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 */ #include "config.h" #include "esd-server.h" #ifdef HAVE_X11_SM_SMLIB_H #include #include #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: ,id=ghost ,Priority= ,RestartCommand= That is it as of 01-28-99. Felix 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.