diff options
Diffstat (limited to 'src/core/session.c')
-rw-r--r-- | src/core/session.c | 1831 |
1 files changed, 1831 insertions, 0 deletions
diff --git a/src/core/session.c b/src/core/session.c new file mode 100644 index 00000000..80d22365 --- /dev/null +++ b/src/core/session.c @@ -0,0 +1,1831 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* Marco Session Management */ + +/* + * Copyright (C) 2001 Havoc Pennington (some code in here from + * libmateui, (C) Tom Tromey, Carsten Schaar) + * Copyright (C) 2004, 2005 Elijah Newren + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include <config.h> + +#include "session.h" +#include <X11/Xatom.h> + +#include <time.h> +#include <sys/wait.h> + +#ifndef HAVE_SM +void +meta_session_init (const char *client_id, + const char *save_file) +{ + meta_topic (META_DEBUG_SM, "Compiled without session management support\n"); +} + +void +meta_session_shutdown (void) +{ + /* nothing */ +} + +const MetaWindowSessionInfo* +meta_window_lookup_saved_state (MetaWindow *window) +{ + return NULL; +} + +void +meta_window_release_saved_state (const MetaWindowSessionInfo *info) +{ + ; +} +#else /* HAVE_SM */ + +#include <X11/ICE/ICElib.h> +#include <X11/SM/SMlib.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <fcntl.h> +#include <errno.h> +#include <glib.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include "main.h" +#include "util.h" +#include "display-private.h" +#include "workspace.h" + +static void ice_io_error_handler (IceConn connection); + +static void new_ice_connection (IceConn connection, IcePointer client_data, + Bool opening, IcePointer *watch_data); + +static void save_state (void); +static char* load_state (const char *previous_save_file); +static void regenerate_save_file (void); +static const char* full_save_file (void); +static void warn_about_lame_clients_and_finish_interact (gboolean shutdown); +static void disconnect (void); + +/* This is called when data is available on an ICE connection. */ +static gboolean +process_ice_messages (GIOChannel *channel, + GIOCondition condition, + gpointer client_data) +{ + IceConn connection = (IceConn) client_data; + IceProcessMessagesStatus status; + + /* This blocks infinitely sometimes. I don't know what + * to do about it. Checking "condition" just breaks + * session management. + */ + status = IceProcessMessages (connection, NULL, NULL); + + if (status == IceProcessMessagesIOError) + { +#if 0 + IcePointer context = IceGetConnectionContext (connection); +#endif + + /* We were disconnected; close our connection to the + * session manager, this will result in the ICE connection + * being cleaned up, since it is owned by libSM. + */ + disconnect (); + meta_quit (META_EXIT_SUCCESS); + + return FALSE; + } + + return TRUE; +} + +/* This is called when a new ICE connection is made. It arranges for + the ICE connection to be handled via the event loop. */ +static void +new_ice_connection (IceConn connection, IcePointer client_data, Bool opening, + IcePointer *watch_data) +{ + guint input_id; + + if (opening) + { + /* Make sure we don't pass on these file descriptors to any + * exec'ed children + */ + GIOChannel *channel; + + fcntl (IceConnectionNumber (connection), F_SETFD, + fcntl (IceConnectionNumber (connection), F_GETFD, 0) | FD_CLOEXEC); + + channel = g_io_channel_unix_new (IceConnectionNumber (connection)); + + input_id = g_io_add_watch (channel, + G_IO_IN | G_IO_ERR, + process_ice_messages, + connection); + + g_io_channel_unref (channel); + + *watch_data = (IcePointer) GUINT_TO_POINTER (input_id); + } + else + { + input_id = GPOINTER_TO_UINT ((gpointer) *watch_data); + + g_source_remove (input_id); + } +} + +static IceIOErrorHandler ice_installed_handler; + +/* We call any handler installed before (or after) mate_ice_init but + avoid calling the default libICE handler which does an exit() */ +static void +ice_io_error_handler (IceConn connection) +{ + if (ice_installed_handler) + (*ice_installed_handler) (connection); +} + +static void +ice_init (void) +{ + static gboolean ice_initted = FALSE; + + if (! ice_initted) + { + IceIOErrorHandler default_handler; + + ice_installed_handler = IceSetIOErrorHandler (NULL); + default_handler = IceSetIOErrorHandler (ice_io_error_handler); + + if (ice_installed_handler == default_handler) + ice_installed_handler = NULL; + + IceAddConnectionWatch (new_ice_connection, NULL); + + ice_initted = TRUE; + } +} + +typedef enum +{ + STATE_DISCONNECTED, + STATE_IDLE, + STATE_SAVING_PHASE_1, + STATE_WAITING_FOR_PHASE_2, + STATE_SAVING_PHASE_2, + STATE_WAITING_FOR_INTERACT, + STATE_DONE_WITH_INTERACT, + STATE_SKIPPING_GLOBAL_SAVE, + STATE_FROZEN, + STATE_REGISTERING +} ClientState; + +static void save_phase_2_callback (SmcConn smc_conn, + SmPointer client_data); +static void interact_callback (SmcConn smc_conn, + SmPointer client_data); +static void shutdown_cancelled_callback (SmcConn smc_conn, + SmPointer client_data); +static void save_complete_callback (SmcConn smc_conn, + SmPointer client_data); +static void die_callback (SmcConn smc_conn, + SmPointer client_data); +static void save_yourself_callback (SmcConn smc_conn, + SmPointer client_data, + int save_style, + Bool shutdown, + int interact_style, + Bool fast); +static void set_clone_restart_commands (void); + +static char *client_id = NULL; +static gpointer session_connection = NULL; +static ClientState current_state = STATE_DISCONNECTED; +static gboolean interaction_allowed = FALSE; + +void +meta_session_init (const char *previous_client_id, + const char *previous_save_file) +{ + /* Some code here from twm */ + char buf[256]; + unsigned long mask; + SmcCallbacks callbacks; + char *saved_client_id; + + meta_topic (META_DEBUG_SM, "Initializing session with save file '%s'\n", + previous_save_file ? previous_save_file : "(none)"); + + if (previous_save_file) + { + saved_client_id = load_state (previous_save_file); + previous_client_id = saved_client_id; + } + else if (previous_client_id) + { + char *save_file = g_strconcat (previous_client_id, ".ms", NULL); + saved_client_id = load_state (save_file); + g_free (save_file); + } + else + { + saved_client_id = NULL; + } + + ice_init (); + + mask = SmcSaveYourselfProcMask | SmcDieProcMask | + SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask; + + callbacks.save_yourself.callback = save_yourself_callback; + callbacks.save_yourself.client_data = NULL; + + callbacks.die.callback = die_callback; + callbacks.die.client_data = NULL; + + callbacks.save_complete.callback = save_complete_callback; + callbacks.save_complete.client_data = NULL; + + callbacks.shutdown_cancelled.callback = shutdown_cancelled_callback; + callbacks.shutdown_cancelled.client_data = NULL; + + session_connection = + SmcOpenConnection (NULL, /* use SESSION_MANAGER env */ + NULL, /* means use existing ICE connection */ + SmProtoMajor, + SmProtoMinor, + mask, + &callbacks, + (char*) previous_client_id, + &client_id, + 255, buf); + + if (session_connection == NULL) + { + meta_topic (META_DEBUG_SM, + "Failed to a open connection to a session manager, so window positions will not be saved: %s\n", + buf); + + goto out; + } + else + { + if (client_id == NULL) + meta_bug ("Session manager gave us a NULL client ID?"); + meta_topic (META_DEBUG_SM, "Obtained session ID '%s'\n", client_id); + } + + if (previous_client_id && strcmp (previous_client_id, client_id) == 0) + current_state = STATE_IDLE; + else + current_state = STATE_REGISTERING; + + { + SmProp prop1, prop2, prop3, prop4, prop5, prop6, *props[6]; + SmPropValue prop1val, prop2val, prop3val, prop4val, prop5val, prop6val; + char pid[32]; + char hint = SmRestartImmediately; + char priority = 20; /* low to run before other apps */ + + prop1.name = SmProgram; + prop1.type = SmARRAY8; + prop1.num_vals = 1; + prop1.vals = &prop1val; + prop1val.value = "marco"; + prop1val.length = strlen ("marco"); + + /* twm sets getuid() for this, but the SM spec plainly + * says pw_name, twm is on crack + */ + prop2.name = SmUserID; + prop2.type = SmARRAY8; + prop2.num_vals = 1; + prop2.vals = &prop2val; + prop2val.value = (char*) g_get_user_name (); + prop2val.length = strlen (prop2val.value); + + prop3.name = SmRestartStyleHint; + prop3.type = SmCARD8; + prop3.num_vals = 1; + prop3.vals = &prop3val; + prop3val.value = &hint; + prop3val.length = 1; + + sprintf (pid, "%d", getpid ()); + prop4.name = SmProcessID; + prop4.type = SmARRAY8; + prop4.num_vals = 1; + prop4.vals = &prop4val; + prop4val.value = pid; + prop4val.length = strlen (prop4val.value); + + /* Always start in home directory */ + prop5.name = SmCurrentDirectory; + prop5.type = SmARRAY8; + prop5.num_vals = 1; + prop5.vals = &prop5val; + prop5val.value = (char*) g_get_home_dir (); + prop5val.length = strlen (prop5val.value); + + prop6.name = "_GSM_Priority"; + prop6.type = SmCARD8; + prop6.num_vals = 1; + prop6.vals = &prop6val; + prop6val.value = &priority; + prop6val.length = 1; + + props[0] = &prop1; + props[1] = &prop2; + props[2] = &prop3; + props[3] = &prop4; + props[4] = &prop5; + props[5] = &prop6; + + SmcSetProperties (session_connection, 6, props); + } + + out: + g_free (saved_client_id); +} + +void +meta_session_shutdown (void) +{ + /* Change our restart mode to IfRunning */ + + SmProp prop1; + SmPropValue prop1val; + SmProp *props[1]; + char hint = SmRestartIfRunning; + + if (!meta_get_display ()) + { + meta_verbose ("Cannot close session because there is no display"); + return; + } + + warn_about_lame_clients_and_finish_interact (FALSE); + + if (session_connection == NULL) + return; + + prop1.name = SmRestartStyleHint; + prop1.type = SmCARD8; + prop1.num_vals = 1; + prop1.vals = &prop1val; + prop1val.value = &hint; + prop1val.length = 1; + + props[0] = &prop1; + + SmcSetProperties (session_connection, 1, props); +} + +static void +disconnect (void) +{ + SmcCloseConnection (session_connection, 0, NULL); + session_connection = NULL; + current_state = STATE_DISCONNECTED; +} + +static void +save_yourself_possibly_done (gboolean shutdown, + gboolean successful) +{ + meta_topic (META_DEBUG_SM, + "save possibly done shutdown = %d success = %d\n", + shutdown, successful); + + if (current_state == STATE_SAVING_PHASE_1) + { + Status status; + + status = SmcRequestSaveYourselfPhase2 (session_connection, + save_phase_2_callback, + GINT_TO_POINTER (shutdown)); + + if (status) + current_state = STATE_WAITING_FOR_PHASE_2; + + meta_topic (META_DEBUG_SM, + "Requested phase 2, status = %d\n", status); + } + + if (current_state == STATE_SAVING_PHASE_2 && + interaction_allowed) + { + Status status; + + status = SmcInteractRequest (session_connection, + /* ignore this feature of the protocol by always + * claiming normal + */ + SmDialogNormal, + interact_callback, + GINT_TO_POINTER (shutdown)); + + if (status) + current_state = STATE_WAITING_FOR_INTERACT; + + meta_topic (META_DEBUG_SM, + "Requested interact, status = %d\n", status); + } + + if (current_state == STATE_SAVING_PHASE_1 || + current_state == STATE_SAVING_PHASE_2 || + current_state == STATE_DONE_WITH_INTERACT || + current_state == STATE_SKIPPING_GLOBAL_SAVE) + { + meta_topic (META_DEBUG_SM, "Sending SaveYourselfDone\n"); + + SmcSaveYourselfDone (session_connection, + successful); + + if (shutdown) + current_state = STATE_FROZEN; + else + current_state = STATE_IDLE; + } +} + +static void +save_phase_2_callback (SmcConn smc_conn, SmPointer client_data) +{ + gboolean shutdown; + + meta_topic (META_DEBUG_SM, "Phase 2 save"); + + shutdown = GPOINTER_TO_INT (client_data); + + current_state = STATE_SAVING_PHASE_2; + + save_state (); + + save_yourself_possibly_done (shutdown, TRUE); +} + +static void +save_yourself_callback (SmcConn smc_conn, + SmPointer client_data, + int save_style, + Bool shutdown, + int interact_style, + Bool fast) +{ + gboolean successful; + + meta_topic (META_DEBUG_SM, "SaveYourself received"); + + successful = TRUE; + + /* The first SaveYourself after registering for the first time + * is a special case (SM specs 7.2). + */ + +#if 0 /* I think the MateClient rationale for this doesn't apply */ + if (current_state == STATE_REGISTERING) + { + current_state = STATE_IDLE; + /* Double check that this is a section 7.2 SaveYourself: */ + + if (save_style == SmSaveLocal && + interact_style == SmInteractStyleNone && + !shutdown && !fast) + { + /* The protocol requires this even if xsm ignores it. */ + SmcSaveYourselfDone (session_connection, successful); + return; + } + } +#endif + + /* ignore Global style saves + * + * This interpretaion of the Local/Global/Both styles + * was discussed extensively on the xdg-list. See: + * + * https://listman.redhat.com/pipermail/xdg-list/2002-July/000615.html + */ + if (save_style == SmSaveGlobal) + { + current_state = STATE_SKIPPING_GLOBAL_SAVE; + save_yourself_possibly_done (shutdown, successful); + return; + } + + interaction_allowed = interact_style != SmInteractStyleNone; + + current_state = STATE_SAVING_PHASE_1; + + regenerate_save_file (); + + set_clone_restart_commands (); + + save_yourself_possibly_done (shutdown, successful); +} + + +static void +die_callback (SmcConn smc_conn, SmPointer client_data) +{ + meta_topic (META_DEBUG_SM, "Exiting at request of session manager\n"); + disconnect (); + meta_quit (META_EXIT_SUCCESS); +} + +static void +save_complete_callback (SmcConn smc_conn, SmPointer client_data) +{ + /* nothing */ + meta_topic (META_DEBUG_SM, "SaveComplete received\n"); +} + +static void +shutdown_cancelled_callback (SmcConn smc_conn, SmPointer client_data) +{ + meta_topic (META_DEBUG_SM, "Shutdown cancelled received\n"); + + if (session_connection != NULL && + (current_state != STATE_IDLE && current_state != STATE_FROZEN)) + { + SmcSaveYourselfDone (session_connection, True); + current_state = STATE_IDLE; + } +} + +static void +interact_callback (SmcConn smc_conn, SmPointer client_data) +{ + /* nothing */ + gboolean shutdown; + + meta_topic (META_DEBUG_SM, "Interaction permission received\n"); + + shutdown = GPOINTER_TO_INT (client_data); + + current_state = STATE_DONE_WITH_INTERACT; + + warn_about_lame_clients_and_finish_interact (shutdown); +} + +static void +set_clone_restart_commands (void) +{ + char *restartv[10]; + char *clonev[10]; + char *discardv[10]; + int i; + SmProp prop1, prop2, prop3, *props[3]; + + /* Restart (use same client ID) */ + + prop1.name = SmRestartCommand; + prop1.type = SmLISTofARRAY8; + + g_return_if_fail (client_id); + + i = 0; + restartv[i] = "marco"; + ++i; + restartv[i] = "--sm-client-id"; + ++i; + restartv[i] = client_id; + ++i; + restartv[i] = NULL; + + prop1.vals = g_new (SmPropValue, i); + i = 0; + while (restartv[i]) + { + prop1.vals[i].value = restartv[i]; + prop1.vals[i].length = strlen (restartv[i]); + ++i; + } + prop1.num_vals = i; + + /* Clone (no client ID) */ + + i = 0; + clonev[i] = "marco"; + ++i; + clonev[i] = NULL; + + prop2.name = SmCloneCommand; + prop2.type = SmLISTofARRAY8; + + prop2.vals = g_new (SmPropValue, i); + i = 0; + while (clonev[i]) + { + prop2.vals[i].value = clonev[i]; + prop2.vals[i].length = strlen (clonev[i]); + ++i; + } + prop2.num_vals = i; + + /* Discard */ + + i = 0; + discardv[i] = "rm"; + ++i; + discardv[i] = "-f"; + ++i; + discardv[i] = (char*) full_save_file (); + ++i; + discardv[i] = NULL; + + prop3.name = SmDiscardCommand; + prop3.type = SmLISTofARRAY8; + + prop3.vals = g_new (SmPropValue, i); + i = 0; + while (discardv[i]) + { + prop3.vals[i].value = discardv[i]; + prop3.vals[i].length = strlen (discardv[i]); + ++i; + } + prop3.num_vals = i; + + + props[0] = &prop1; + props[1] = &prop2; + props[2] = &prop3; + + SmcSetProperties (session_connection, 3, props); + + g_free (prop1.vals); + g_free (prop2.vals); + g_free (prop3.vals); +} + +/* The remaining code in this file actually loads/saves the session, + * while the code above this comment handles chatting with the + * session manager. + */ + +static const char* +window_type_to_string (MetaWindowType type) +{ + switch (type) + { + case META_WINDOW_NORMAL: + return "normal"; + case META_WINDOW_DESKTOP: + return "desktop"; + case META_WINDOW_DOCK: + return "dock"; + case META_WINDOW_DIALOG: + return "dialog"; + case META_WINDOW_MODAL_DIALOG: + return "modal_dialog"; + case META_WINDOW_TOOLBAR: + return "toolbar"; + case META_WINDOW_MENU: + return "menu"; + case META_WINDOW_SPLASHSCREEN: + return "splashscreen"; + case META_WINDOW_UTILITY: + return "utility"; + } + + return ""; +} + +static MetaWindowType +window_type_from_string (const char *str) +{ + if (strcmp (str, "normal") == 0) + return META_WINDOW_NORMAL; + else if (strcmp (str, "desktop") == 0) + return META_WINDOW_DESKTOP; + else if (strcmp (str, "dock") == 0) + return META_WINDOW_DOCK; + else if (strcmp (str, "dialog") == 0) + return META_WINDOW_DIALOG; + else if (strcmp (str, "modal_dialog") == 0) + return META_WINDOW_MODAL_DIALOG; + else if (strcmp (str, "toolbar") == 0) + return META_WINDOW_TOOLBAR; + else if (strcmp (str, "menu") == 0) + return META_WINDOW_MENU; + else if (strcmp (str, "utility") == 0) + return META_WINDOW_UTILITY; + else if (strcmp (str, "splashscreen") == 0) + return META_WINDOW_SPLASHSCREEN; + else + return META_WINDOW_NORMAL; +} + +static int +window_gravity_from_string (const char *str) +{ + if (strcmp (str, "NorthWestGravity") == 0) + return NorthWestGravity; + else if (strcmp (str, "NorthGravity") == 0) + return NorthGravity; + else if (strcmp (str, "NorthEastGravity") == 0) + return NorthEastGravity; + else if (strcmp (str, "WestGravity") == 0) + return WestGravity; + else if (strcmp (str, "CenterGravity") == 0) + return CenterGravity; + else if (strcmp (str, "EastGravity") == 0) + return EastGravity; + else if (strcmp (str, "SouthWestGravity") == 0) + return SouthWestGravity; + else if (strcmp (str, "SouthGravity") == 0) + return SouthGravity; + else if (strcmp (str, "SouthEastGravity") == 0) + return SouthEastGravity; + else if (strcmp (str, "StaticGravity") == 0) + return StaticGravity; + else + return NorthWestGravity; +} + +static char* +encode_text_as_utf8_markup (const char *text) +{ + /* text can be any encoding, and is nul-terminated. + * we pretend it's Latin-1 and encode as UTF-8 + */ + GString *str; + const char *p; + char *escaped; + + str = g_string_new (""); + + p = text; + while (*p) + { + g_string_append_unichar (str, *p); + ++p; + } + + escaped = g_markup_escape_text (str->str, str->len); + g_string_free (str, TRUE); + + return escaped; +} + +static char* +decode_text_from_utf8 (const char *text) +{ + /* Convert back from the encoded (but not escaped) UTF-8 */ + GString *str; + const char *p; + + str = g_string_new (""); + + p = text; + while (*p) + { + /* obviously this barfs if the UTF-8 contains chars > 255 */ + g_string_append_c (str, g_utf8_get_char (p)); + + p = g_utf8_next_char (p); + } + + return g_string_free (str, FALSE); +} + +static void +save_state (void) +{ + char *marco_dir; + char *session_dir; + FILE *outfile; + GSList *windows; + GSList *tmp; + int stack_position; + + g_assert (client_id); + + outfile = NULL; + + /* + * g_get_user_config_dir() is guaranteed to return an existing directory. + * Eventually, if SM stays with the WM, I'd like to make this + * something like <config>/window_placement in a standard format. + * Future optimisers should note also that by the time we get here + * we probably already have full_save_path figured out and therefore + * can just use the directory name from that. + */ + marco_dir = g_strconcat (g_get_user_config_dir (), + G_DIR_SEPARATOR_S "marco", + NULL); + + session_dir = g_strconcat (marco_dir, + G_DIR_SEPARATOR_S "sessions", + NULL); + + if (mkdir (marco_dir, 0700) < 0 && + errno != EEXIST) + { + meta_warning (_("Could not create directory '%s': %s\n"), + marco_dir, g_strerror (errno)); + } + + if (mkdir (session_dir, 0700) < 0 && + errno != EEXIST) + { + meta_warning (_("Could not create directory '%s': %s\n"), + session_dir, g_strerror (errno)); + } + + meta_topic (META_DEBUG_SM, "Saving session to '%s'\n", full_save_file ()); + + outfile = fopen (full_save_file (), "w"); + + if (outfile == NULL) + { + meta_warning (_("Could not open session file '%s' for writing: %s\n"), + full_save_file (), g_strerror (errno)); + goto out; + } + + /* The file format is: + * <marco_session id="foo"> + * <window id="bar" class="XTerm" name="xterm" title="/foo/bar" role="blah" type="normal" stacking="5"> + * <workspace index="2"/> + * <workspace index="4"/> + * <sticky/> <minimized/> <maximized/> + * <geometry x="100" y="100" width="200" height="200" gravity="northwest"/> + * </window> + * </marco_session> + * + * Note that attributes on <window> are the match info we use to + * see if the saved state applies to a restored window, and + * child elements are the saved state to be applied. + * + */ + + fprintf (outfile, "<marco_session id=\"%s\">\n", + client_id); + + windows = meta_display_list_windows (meta_get_display ()); + stack_position = 0; + + windows = g_slist_sort (windows, meta_display_stack_cmp); + tmp = windows; + stack_position = 0; + + while (tmp != NULL) + { + MetaWindow *window; + + window = tmp->data; + + if (window->sm_client_id) + { + char *sm_client_id; + char *res_class; + char *res_name; + char *role; + char *title; + + /* client id, class, name, role are not expected to be + * in UTF-8 (I think they are in XPCS which is Latin-1? + * in practice they are always ascii though.) + */ + + sm_client_id = encode_text_as_utf8_markup (window->sm_client_id); + res_class = window->res_class ? + encode_text_as_utf8_markup (window->res_class) : NULL; + res_name = window->res_name ? + encode_text_as_utf8_markup (window->res_name) : NULL; + role = window->role ? + encode_text_as_utf8_markup (window->role) : NULL; + if (window->title) + title = g_markup_escape_text (window->title, -1); + else + title = NULL; + + meta_topic (META_DEBUG_SM, "Saving session managed window %s, client ID '%s'\n", + window->desc, window->sm_client_id); + + fprintf (outfile, + " <window id=\"%s\" class=\"%s\" name=\"%s\" title=\"%s\" role=\"%s\" type=\"%s\" stacking=\"%d\">\n", + sm_client_id, + res_class ? res_class : "", + res_name ? res_name : "", + title ? title : "", + role ? role : "", + window_type_to_string (window->type), + stack_position); + + g_free (sm_client_id); + g_free (res_class); + g_free (res_name); + g_free (role); + g_free (title); + + /* Sticky */ + if (window->on_all_workspaces) + fputs (" <sticky/>\n", outfile); + + /* Minimized */ + if (window->minimized) + fputs (" <minimized/>\n", outfile); + + /* Maximized */ + if (META_WINDOW_MAXIMIZED (window)) + { + fprintf (outfile, + " <maximized saved_x=\"%d\" saved_y=\"%d\" saved_width=\"%d\" saved_height=\"%d\"/>\n", + window->saved_rect.x, + window->saved_rect.y, + window->saved_rect.width, + window->saved_rect.height); + } + + /* Workspaces we're on */ + { + int n; + n = meta_workspace_index (window->workspace); + fprintf (outfile, + " <workspace index=\"%d\"/>\n", n); + } + + /* Gravity */ + { + int x, y, w, h; + meta_window_get_geometry (window, &x, &y, &w, &h); + + fprintf (outfile, + " <geometry x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" gravity=\"%s\"/>\n", + x, y, w, h, + meta_gravity_to_string (window->size_hints.win_gravity)); + } + + fputs (" </window>\n", outfile); + } + else + { + meta_topic (META_DEBUG_SM, "Not saving window '%s', not session managed\n", + window->desc); + } + + tmp = tmp->next; + ++stack_position; + } + + g_slist_free (windows); + + fputs ("</marco_session>\n", outfile); + + out: + if (outfile) + { + /* FIXME need a dialog for this */ + if (ferror (outfile)) + { + meta_warning (_("Error writing session file '%s': %s\n"), + full_save_file (), g_strerror (errno)); + } + if (fclose (outfile)) + { + meta_warning (_("Error closing session file '%s': %s\n"), + full_save_file (), g_strerror (errno)); + } + } + + g_free (marco_dir); + g_free (session_dir); +} + +typedef enum +{ + WINDOW_TAG_NONE, + WINDOW_TAG_DESKTOP, + WINDOW_TAG_STICKY, + WINDOW_TAG_MINIMIZED, + WINDOW_TAG_MAXIMIZED, + WINDOW_TAG_GEOMETRY +} WindowTag; + +typedef struct +{ + MetaWindowSessionInfo *info; + char *previous_id; +} ParseData; + +static void session_info_free (MetaWindowSessionInfo *info); +static MetaWindowSessionInfo* session_info_new (void); + +static void start_element_handler (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error); +static void end_element_handler (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error); +static void text_handler (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error); + +static GMarkupParser marco_session_parser = { + start_element_handler, + end_element_handler, + text_handler, + NULL, + NULL +}; + +static GSList *window_info_list = NULL; + +static char* +load_state (const char *previous_save_file) +{ + GMarkupParseContext *context; + GError *error; + ParseData parse_data; + char *text; + gsize length; + char *session_file; + + session_file = g_strconcat (g_get_user_config_dir (), + G_DIR_SEPARATOR_S "marco" + G_DIR_SEPARATOR_S "sessions" G_DIR_SEPARATOR_S, + previous_save_file, + NULL); + + error = NULL; + if (!g_file_get_contents (session_file, + &text, + &length, + &error)) + { + char *canonical_session_file = session_file; + + /* Maybe they were doing it the old way, with ~/.marco */ + session_file = g_strconcat (g_get_home_dir (), + G_DIR_SEPARATOR_S ".marco" + G_DIR_SEPARATOR_S "sessions" + G_DIR_SEPARATOR_S, + previous_save_file, + NULL); + + if (!g_file_get_contents (session_file, + &text, + &length, + NULL)) + { + /* oh, just give up */ + + g_error_free (error); + g_free (session_file); + g_free (canonical_session_file); + return NULL; + } + + g_free (canonical_session_file); + } + + meta_topic (META_DEBUG_SM, "Parsing saved session file %s\n", session_file); + g_free (session_file); + session_file = NULL; + + parse_data.info = NULL; + parse_data.previous_id = NULL; + + context = g_markup_parse_context_new (&marco_session_parser, + 0, &parse_data, NULL); + + error = NULL; + if (!g_markup_parse_context_parse (context, + text, + length, + &error)) + goto error; + + + error = NULL; + if (!g_markup_parse_context_end_parse (context, &error)) + goto error; + + g_markup_parse_context_free (context); + + goto out; + + error: + + meta_warning (_("Failed to parse saved session file: %s\n"), + error->message); + g_error_free (error); + + if (parse_data.info) + session_info_free (parse_data.info); + + g_free (parse_data.previous_id); + parse_data.previous_id = NULL; + + out: + + g_free (text); + + return parse_data.previous_id; +} + +/* FIXME this isn't very robust against bogus session files */ +static void +start_element_handler (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ParseData *pd; + + pd = user_data; + + if (strcmp (element_name, "marco_session") == 0) + { + /* Get previous ID */ + int i; + + i = 0; + while (attribute_names[i]) + { + const char *name; + const char *val; + + name = attribute_names[i]; + val = attribute_values[i]; + + if (pd->previous_id) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("<marco_session> attribute seen but we already have the session ID")); + return; + } + + if (strcmp (name, "id") == 0) + { + pd->previous_id = decode_text_from_utf8 (val); + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + _("Unknown attribute %s on <%s> element"), + name, "marco_session"); + return; + } + + ++i; + } + } + else if (strcmp (element_name, "window") == 0) + { + int i; + + if (pd->info) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + _("nested <window> tag")); + return; + } + + pd->info = session_info_new (); + + i = 0; + while (attribute_names[i]) + { + const char *name; + const char *val; + + name = attribute_names[i]; + val = attribute_values[i]; + + if (strcmp (name, "id") == 0) + { + if (*val) + pd->info->id = decode_text_from_utf8 (val); + } + else if (strcmp (name, "class") == 0) + { + if (*val) + pd->info->res_class = decode_text_from_utf8 (val); + } + else if (strcmp (name, "name") == 0) + { + if (*val) + pd->info->res_name = decode_text_from_utf8 (val); + } + else if (strcmp (name, "title") == 0) + { + if (*val) + pd->info->title = g_strdup (val); + } + else if (strcmp (name, "role") == 0) + { + if (*val) + pd->info->role = decode_text_from_utf8 (val); + } + else if (strcmp (name, "type") == 0) + { + if (*val) + pd->info->type = window_type_from_string (val); + } + else if (strcmp (name, "stacking") == 0) + { + if (*val) + { + pd->info->stack_position = atoi (val); + pd->info->stack_position_set = TRUE; + } + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + _("Unknown attribute %s on <%s> element"), + name, "window"); + session_info_free (pd->info); + pd->info = NULL; + return; + } + + ++i; + } + } + else if (strcmp (element_name, "workspace") == 0) + { + int i; + + i = 0; + while (attribute_names[i]) + { + const char *name; + + name = attribute_names[i]; + + if (strcmp (name, "index") == 0) + { + pd->info->workspace_indices = + g_slist_prepend (pd->info->workspace_indices, + GINT_TO_POINTER (atoi (attribute_values[i]))); + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + _("Unknown attribute %s on <%s> element"), + name, "window"); + session_info_free (pd->info); + pd->info = NULL; + return; + } + + ++i; + } + } + else if (strcmp (element_name, "sticky") == 0) + { + pd->info->on_all_workspaces = TRUE; + pd->info->on_all_workspaces_set = TRUE; + } + else if (strcmp (element_name, "minimized") == 0) + { + pd->info->minimized = TRUE; + pd->info->minimized_set = TRUE; + } + else if (strcmp (element_name, "maximized") == 0) + { + int i; + + i = 0; + pd->info->maximized = TRUE; + pd->info->maximized_set = TRUE; + while (attribute_names[i]) + { + const char *name; + const char *val; + + name = attribute_names[i]; + val = attribute_values[i]; + + if (strcmp (name, "saved_x") == 0) + { + if (*val) + { + pd->info->saved_rect.x = atoi (val); + pd->info->saved_rect_set = TRUE; + } + } + else if (strcmp (name, "saved_y") == 0) + { + if (*val) + { + pd->info->saved_rect.y = atoi (val); + pd->info->saved_rect_set = TRUE; + } + } + else if (strcmp (name, "saved_width") == 0) + { + if (*val) + { + pd->info->saved_rect.width = atoi (val); + pd->info->saved_rect_set = TRUE; + } + } + else if (strcmp (name, "saved_height") == 0) + { + if (*val) + { + pd->info->saved_rect.height = atoi (val); + pd->info->saved_rect_set = TRUE; + } + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + _("Unknown attribute %s on <%s> element"), + name, "maximized"); + return; + } + + ++i; + } + + if (pd->info->saved_rect_set) + meta_topic (META_DEBUG_SM, "Saved unmaximized size %d,%d %dx%d \n", + pd->info->saved_rect.x, + pd->info->saved_rect.y, + pd->info->saved_rect.width, + pd->info->saved_rect.height); + } + else if (strcmp (element_name, "geometry") == 0) + { + int i; + + pd->info->geometry_set = TRUE; + + i = 0; + while (attribute_names[i]) + { + const char *name; + const char *val; + + name = attribute_names[i]; + val = attribute_values[i]; + + if (strcmp (name, "x") == 0) + { + if (*val) + pd->info->rect.x = atoi (val); + } + else if (strcmp (name, "y") == 0) + { + if (*val) + pd->info->rect.y = atoi (val); + } + else if (strcmp (name, "width") == 0) + { + if (*val) + pd->info->rect.width = atoi (val); + } + else if (strcmp (name, "height") == 0) + { + if (*val) + pd->info->rect.height = atoi (val); + } + else if (strcmp (name, "gravity") == 0) + { + if (*val) + pd->info->gravity = window_gravity_from_string (val); + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + _("Unknown attribute %s on <%s> element"), + name, "geometry"); + return; + } + + ++i; + } + + meta_topic (META_DEBUG_SM, "Loaded geometry %d,%d %dx%d gravity %s\n", + pd->info->rect.x, + pd->info->rect.y, + pd->info->rect.width, + pd->info->rect.height, + meta_gravity_to_string (pd->info->gravity)); + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + _("Unknown element %s"), + element_name); + return; + } +} + +static void +end_element_handler (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseData *pd; + + pd = user_data; + + if (strcmp (element_name, "window") == 0) + { + g_assert (pd->info); + + window_info_list = g_slist_prepend (window_info_list, + pd->info); + + meta_topic (META_DEBUG_SM, "Loaded window info from session with class: %s name: %s role: %s\n", + pd->info->res_class ? pd->info->res_class : "(none)", + pd->info->res_name ? pd->info->res_name : "(none)", + pd->info->role ? pd->info->role : "(none)"); + + pd->info = NULL; + } +} + +static void +text_handler (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + /* Right now we don't have any elements where we care about their + * content + */ +} + +static gboolean +both_null_or_matching (const char *a, + const char *b) +{ + if (a == NULL && b == NULL) + return TRUE; + else if (a && b && strcmp (a, b) == 0) + return TRUE; + else + return FALSE; +} + +static GSList* +get_possible_matches (MetaWindow *window) +{ + /* Get all windows with this client ID */ + GSList *retval; + GSList *tmp; + gboolean ignore_client_id; + + retval = NULL; + + ignore_client_id = g_getenv ("MARCO_DEBUG_SM") != NULL; + + tmp = window_info_list; + while (tmp != NULL) + { + MetaWindowSessionInfo *info; + + info = tmp->data; + + if ((ignore_client_id || + both_null_or_matching (info->id, window->sm_client_id)) && + both_null_or_matching (info->res_class, window->res_class) && + both_null_or_matching (info->res_name, window->res_name) && + both_null_or_matching (info->role, window->role)) + { + meta_topic (META_DEBUG_SM, "Window %s may match saved window with class: %s name: %s role: %s\n", + window->desc, + info->res_class ? info->res_class : "(none)", + info->res_name ? info->res_name : "(none)", + info->role ? info->role : "(none)"); + + retval = g_slist_prepend (retval, info); + } + else + { + if (meta_is_verbose ()) + { + if (!both_null_or_matching (info->id, window->sm_client_id)) + meta_topic (META_DEBUG_SM, "Window %s has SM client ID %s, saved state has %s, no match\n", + window->desc, + window->sm_client_id ? window->sm_client_id : "(none)", + info->id ? info->id : "(none)"); + else if (!both_null_or_matching (info->res_class, window->res_class)) + meta_topic (META_DEBUG_SM, "Window %s has class %s doesn't match saved class %s, no match\n", + window->desc, + window->res_class ? window->res_class : "(none)", + info->res_class ? info->res_class : "(none)"); + + else if (!both_null_or_matching (info->res_name, window->res_name)) + meta_topic (META_DEBUG_SM, "Window %s has name %s doesn't match saved name %s, no match\n", + window->desc, + window->res_name ? window->res_name : "(none)", + info->res_name ? info->res_name : "(none)"); + else if (!both_null_or_matching (info->role, window->role)) + meta_topic (META_DEBUG_SM, "Window %s has role %s doesn't match saved role %s, no match\n", + window->desc, + window->role ? window->role : "(none)", + info->role ? info->role : "(none)"); + else + meta_topic (META_DEBUG_SM, "???? should not happen - window %s doesn't match saved state %s for no good reason\n", + window->desc, info->id); + } + } + + tmp = tmp->next; + } + + return retval; +} + +static const MetaWindowSessionInfo* +find_best_match (GSList *infos, + MetaWindow *window) +{ + GSList *tmp; + const MetaWindowSessionInfo *matching_title; + const MetaWindowSessionInfo *matching_type; + + matching_title = NULL; + matching_type = NULL; + + tmp = infos; + while (tmp != NULL) + { + MetaWindowSessionInfo *info; + + info = tmp->data; + + if (matching_title == NULL && + both_null_or_matching (info->title, window->title)) + matching_title = info; + + if (matching_type == NULL && + info->type == window->type) + matching_type = info; + + tmp = tmp->next; + } + + /* Prefer same title, then same type of window, then + * just pick something. Eventually we could enhance this + * to e.g. break ties by geometry hint similarity, + * or other window features. + */ + + if (matching_title) + return matching_title; + else if (matching_type) + return matching_type; + else + return infos->data; +} + +const MetaWindowSessionInfo* +meta_window_lookup_saved_state (MetaWindow *window) +{ + GSList *possibles; + const MetaWindowSessionInfo *info; + + /* Window is not session managed. + * I haven't yet figured out how to deal with these + * in a way that doesn't cause broken side effects in + * situations other than on session restore. + */ + if (window->sm_client_id == NULL) + { + meta_topic (META_DEBUG_SM, + "Window %s is not session managed, not checking for saved state\n", + window->desc); + return NULL; + } + + possibles = get_possible_matches (window); + + if (possibles == NULL) + { + meta_topic (META_DEBUG_SM, "Window %s has no possible matches in the list of saved window states\n", + window->desc); + return NULL; + } + + info = find_best_match (possibles, window); + + g_slist_free (possibles); + + return info; +} + +void +meta_window_release_saved_state (const MetaWindowSessionInfo *info) +{ + /* We don't want to use the same saved state again for another + * window. + */ + window_info_list = g_slist_remove (window_info_list, info); + + session_info_free ((MetaWindowSessionInfo*) info); +} + +static void +session_info_free (MetaWindowSessionInfo *info) +{ + g_free (info->id); + g_free (info->res_class); + g_free (info->res_name); + g_free (info->title); + g_free (info->role); + + g_slist_free (info->workspace_indices); + + g_free (info); +} + +static MetaWindowSessionInfo* +session_info_new (void) +{ + MetaWindowSessionInfo *info; + + info = g_new0 (MetaWindowSessionInfo, 1); + + info->type = META_WINDOW_NORMAL; + info->gravity = NorthWestGravity; + + return info; +} + +static char* full_save_path = NULL; + +static void +regenerate_save_file (void) +{ + g_free (full_save_path); + + if (client_id) + full_save_path = g_strconcat (g_get_user_config_dir (), + G_DIR_SEPARATOR_S "marco" + G_DIR_SEPARATOR_S "sessions" G_DIR_SEPARATOR_S, + client_id, + ".ms", + NULL); + else + full_save_path = NULL; +} + +static const char* +full_save_file (void) +{ + return full_save_path; +} + +static int +windows_cmp_by_title (MetaWindow *a, + MetaWindow *b) +{ + return g_utf8_collate (a->title, b->title); +} + +static void +finish_interact (gboolean shutdown) +{ + if (current_state == STATE_DONE_WITH_INTERACT) /* paranoia */ + { + SmcInteractDone (session_connection, False /* don't cancel logout */); + + save_yourself_possibly_done (shutdown, TRUE); + } +} + +static void +dialog_closed (GPid pid, int status, gpointer user_data) +{ + gboolean shutdown = GPOINTER_TO_INT (user_data); + + if (WIFEXITED (status) && WEXITSTATUS (status) == 0) /* pressed "OK" */ + { + finish_interact (shutdown); + } +} + +static void +warn_about_lame_clients_and_finish_interact (gboolean shutdown) +{ + GSList *lame = NULL; + GSList *windows; + GSList *lame_details = NULL; + GSList *tmp; + GSList *columns = NULL; + GPid pid; + + windows = meta_display_list_windows (meta_get_display ()); + tmp = windows; + while (tmp != NULL) + { + MetaWindow *window; + + window = tmp->data; + + /* only complain about normal windows, the others + * are kind of dumb to worry about + */ + if (window->sm_client_id == NULL && + window->type == META_WINDOW_NORMAL) + lame = g_slist_prepend (lame, window); + + tmp = tmp->next; + } + + g_slist_free (windows); + + if (lame == NULL) + { + /* No lame apps. */ + finish_interact (shutdown); + return; + } + + columns = g_slist_prepend (columns, "Window"); + columns = g_slist_prepend (columns, "Class"); + + lame = g_slist_sort (lame, (GCompareFunc) windows_cmp_by_title); + + tmp = lame; + while (tmp != NULL) + { + MetaWindow *w = tmp->data; + + lame_details = g_slist_prepend (lame_details, + w->res_class ? w->res_class : ""); + lame_details = g_slist_prepend (lame_details, + w->title); + + tmp = tmp->next; + } + g_slist_free (lame); + + pid = meta_show_dialog("--list", + _("These windows do not support "save current setup" " + "and will have to be restarted manually next time " + "you log in."), + "240", + meta_screen_get_screen_number (meta_get_display()->active_screen), + NULL, NULL, + None, + columns, + lame_details); + + g_slist_free (lame_details); + + g_child_watch_add (pid, dialog_closed, GINT_TO_POINTER (shutdown)); +} + +#endif /* HAVE_SM */ |