mutter/src/x11/session.c
2016-01-10 15:16:09 +01:00

1831 lines
50 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/* Mutter Session Management */
/*
* Copyright (C) 2001 Havoc Pennington (some code in here from
* libgnomeui, (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, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include "util-private.h"
#include <meta/main.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");
}
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 <meta/main.h>
#include <meta/util.h>
#include "display-private.h"
#include <meta/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) gnome_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];
/* Historically, this was SmRestartImmediately, which made sense
* for a stateless window manager, but we don't really control
* what embedders do, and it's all around better if gnome-session
* handles this.
*/
char hint = SmRestartIfRunning;
char priority = 20; /* low to run before other apps */
const char *prgname;
prgname = g_get_prgname ();
prop1.name = (char *)SmProgram;
prop1.type = (char *)SmARRAY8;
prop1.num_vals = 1;
prop1.vals = &prop1val;
prop1val.value = (char *)prgname;
prop1val.length = strlen (prgname);
/* twm sets getuid() for this, but the SM spec plainly
* says pw_name, twm is on crack
*/
prop2.name = (char *)SmUserID;
prop2.type = (char *)SmARRAY8;
prop2.num_vals = 1;
prop2.vals = &prop2val;
prop2val.value = (char*) g_get_user_name ();
prop2val.length = strlen (prop2val.value);
prop3.name = (char *)SmRestartStyleHint;
prop3.type = (char *)SmCARD8;
prop3.num_vals = 1;
prop3.vals = &prop3val;
prop3val.value = &hint;
prop3val.length = 1;
sprintf (pid, "%d", getpid ());
prop4.name = (char *)SmProcessID;
prop4.type = (char *)SmARRAY8;
prop4.num_vals = 1;
prop4.vals = &prop4val;
prop4val.value = pid;
prop4val.length = strlen (prop4val.value);
/* Always start in home directory */
prop5.name = (char *)SmCurrentDirectory;
prop5.type = (char *)SmARRAY8;
prop5.num_vals = 1;
prop5.vals = &prop5val;
prop5val.value = (char*) g_get_home_dir ();
prop5val.length = strlen (prop5val.value);
prop6.name = (char *)"_GSM_Priority";
prop6.type = (char *)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);
}
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 GnomeClient 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, "Disconnecting from session manager");
disconnect ();
/* We don't actually exit here - we will simply go away with the X
* server on logout, when we lose the X connection and libx11 kills
* us. It looks like *crap* on logout if the user sees their
* windows lose the decorations, etc.
*
* Anything that wants us to go away outside of session management
* can use kill().
*/
/* All of that is true - unless we're a wayland compositor. In which
* case the X server won't go down until we do, so we must die first.
*/
if (meta_is_wayland_compositor ())
meta_quit (0);
}
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];
const char *prgname;
prgname = g_get_prgname ();
/* Restart (use same client ID) */
prop1.name = (char *)SmRestartCommand;
prop1.type = (char *)SmLISTofARRAY8;
g_return_if_fail (client_id);
i = 0;
restartv[i] = (char *)prgname;
++i;
restartv[i] = (char *)"--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] = (char *)prgname;
++i;
clonev[i] = NULL;
prop2.name = (char *)SmCloneCommand;
prop2.type = (char *)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] = (char *)"rm";
++i;
discardv[i] = (char *)"-f";
++i;
discardv[i] = (char*) full_save_file ();
++i;
discardv[i] = NULL;
prop3.name = (char *)SmDiscardCommand;
prop3.type = (char *)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";
case META_WINDOW_DROPDOWN_MENU:
return "dropdown_menu";
case META_WINDOW_POPUP_MENU:
return "popup_menu";
case META_WINDOW_TOOLTIP:
return "tooltip";
case META_WINDOW_NOTIFICATION:
return "notification";
case META_WINDOW_COMBO:
return "combo";
case META_WINDOW_DND:
return "dnd";
case META_WINDOW_OVERRIDE_OTHER:
return "override_redirect";
}
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 *mutter_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.
*/
mutter_dir = g_strconcat (g_get_user_config_dir (),
G_DIR_SEPARATOR_S "mutter",
NULL);
session_dir = g_strconcat (mutter_dir,
G_DIR_SEPARATOR_S "sessions",
NULL);
if (mkdir (mutter_dir, 0700) < 0 &&
errno != EEXIST)
{
meta_warning ("Could not create directory '%s': %s\n",
mutter_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:
* <mutter_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>
* </mutter_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, "<mutter_session id=\"%s\">\n",
client_id);
windows = meta_display_list_windows (meta_get_display (), META_LIST_DEFAULT);
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_requested)
{
fputs (" <sticky/>\n", outfile);
} else {
int n;
n = meta_workspace_index (window->workspace);
fprintf (outfile,
" <workspace index=\"%d\"/>\n", n);
}
/* 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);
}
/* Gravity */
{
int x, y, w, h;
meta_window_get_session_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 ("</mutter_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 (mutter_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 mutter_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 "mutter"
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 ~/.mutter */
session_file = g_strconcat (g_get_home_dir (),
G_DIR_SEPARATOR_S ".mutter"
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 (&mutter_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, "mutter_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,
"<mutter_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, "mutter_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 ("MUTTER_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 "mutter"
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 (), META_LIST_DEFAULT);
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, (gpointer)"Window");
columns = g_slist_prepend (columns, (gpointer)"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 : (gpointer)"");
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 &quot;save current setup&quot; "
"and will have to be restarted manually next time "
"you log in."),
"240",
meta_get_display()->screen->screen_name,
NULL, 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 */