gnome-shell/src/gnome-shell.in

789 lines
27 KiB
Plaintext
Raw Normal View History

#!@PYTHON@
# -*- mode: Python; indent-tabs-mode: nil; -*-
import atexit
import datetime
import dbus
from dbus.mainloop.glib import DBusGMainLoop
import gobject
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
json = None
import optparse
import os
import random
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import termios
import time
import errno
def show_version(option, opt_str, value, parser):
print "GNOME Shell @VERSION@"
sys.exit()
def get_running_session_environs():
wanted_environment = ['DBUS_SESSION_BUS_ADDRESS', 'DISPLAY', 'XDG_DATA_DIRS',
'XAUTHORITY', 'XDG_SESSION_COOKIE', 'ORBIT_SOCKETDIR',
'SESSION_MANAGER']
num_re = re.compile('^[0-9]+$')
myuid = os.getuid()
if not os.path.isdir('/proc'):
return {}
for filename in os.listdir('/proc'):
if not num_re.match(filename):
continue
piddir = '/proc/' + filename
try:
stat = os.stat(piddir)
except OSError, e:
continue
if not stat.st_uid == myuid:
continue
try:
f = open(piddir + "/cmdline")
command = f.read()
f.close()
except IOError, e:
continue
# /proc/cmdline is separated and terminated by NULs
command = command.split("\x00")[0]
command = os.path.basename(command)
if command != 'gnome-session':
continue
try:
f = open(os.path.join(piddir, 'environ'))
except OSError, e:
continue
environ_data = f.read()
f.close()
# There's a trailing null at the last one, so remove the
# empty string
environs = environ_data.split('\0')[:-1]
# Rumor has it the presence of just FOO (instead of FOO=bar)
# represents a deleted environment variable
environs = filter(lambda x: '=' in x, environs)
# Turn it into a dictionary
environs = dict(map(lambda x: x.split('=', 1), environs))
result = {}
for key in wanted_environment:
if key in environs:
result[key] = environs[key]
return result
def start_xephyr():
tmpdir = tempfile.mkdtemp("", "gnome-shell.")
atexit.register(shutil.rmtree, tmpdir)
display = ":" + str(random.randint(10, 99))
xauth_file = os.path.join(tmpdir, "database")
# Create a random 128-bit key and format it as hex
f = open("/dev/urandom", "r")
key = f.read(16)
f.close()
hexkey = "".join(("%02x" % ord(byte) for byte in key))
# Store that in an xauthority file as the key for connecting to our Xephyr
retcode = subprocess.call(["xauth",
"-f", xauth_file,
"add", display, "MIT-MAGIC-COOKIE-1", hexkey])
if retcode != 0:
raise RuntimeError("xauth failed")
# Launch Xephyr
try:
xephyr = subprocess.Popen(["Xephyr", display,
"-auth", xauth_file,
"-screen", options.geometry,
"-host-cursor"])
except OSError, e:
if e.errno == errno.ENOENT:
print "Could not find Xephyr."
sys.exit(1)
else:
raise
os.environ['DISPLAY'] = display
os.environ['XAUTHORITY'] = xauth_file
# Wait for server to get going: LAME
time.sleep(1)
# Start some windows in our session.
subprocess.Popen(["gnome-terminal"])
return xephyr;
def start_dconf_await_service():
DCONF_NAME = 'ca.desrt.dconf'
dbus_loop = DBusGMainLoop()
bus = dbus.SessionBus(mainloop=dbus_loop)
# See if the service is already running or normal D-Bus activation works
need_manual_activate = False
try:
dconf_proxy = bus.get_object(DCONF_NAME, '/')
dconf_proxy.Ping(dbus_interface='org.freedesktop.DBus.Peer')
except dbus.exceptions.DBusException, e:
if e.get_dbus_name() == 'org.freedesktop.DBus.Error.ServiceUnknown':
need_manual_activate = True
else:
raise e
if not need_manual_activate:
return
# At this point, it looks like we just have a jhbuild install
# of dconf, not known to the session dbus-daemon, so we start
# it manually and wait for it to join the bus
print "Starting dconf-service... ",
sys.stdout.flush()
# dconf is linked without libtool, so unlike other GNOME modules,
# won't have an embedded rpath for its library directory.
env = dict(os.environ)
if 'LD_LIBRARY_PATH' in env and env['LD_LIBRARY_PATH']:
ld_library_path = '@libdir@:' + env['LD_LIBRARY_PATH']
else:
ld_library_path = '@libdir@'
env['LD_LIBRARY_PATH'] = ld_library_path
dconf_path = os.path.join('@libexecdir@', 'dconf-service')
try:
subprocess.Popen([dconf_path, '--keep-alive'], env=env)
except OSError, e:
print "\nFailed to start %s: %s" % (dconf_path, e)
sys.exit(1)
bus_proxy = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
bus_iface = dbus.Interface(bus_proxy, 'org.freedesktop.DBus')
loop = gobject.MainLoop()
def on_name_owner_changed(name, prev_owner, new_owner):
if not (name == DCONF_NAME and new_owner != ''):
return
print "started"
loop.quit()
return
bus_iface.connect_to_signal('NameOwnerChanged', on_name_owner_changed)
def on_timeout():
print "\nFailed to start %s: timed out" % (dconf_path,)
sys.exit(1)
gobject.timeout_add_seconds(7, on_timeout)
loop.run()
GLXINFO_RE = re.compile(r"^(\S.*):\s*\n((?:^\s+.*\n)*)", re.MULTILINE)
def _get_glx_extensions():
"""Return a tuple of server, client, and effective GLX extensions"""
glxinfo = subprocess.Popen(["glxinfo"], stdout=subprocess.PIPE)
glxinfo_output = glxinfo.communicate()[0]
glxinfo.wait()
glxinfo_map = {}
for m in GLXINFO_RE.finditer(glxinfo_output):
glxinfo_map[m.group(1)] = m.group(2)
server_glx_extensions = set(re.split("\s*,\s*", glxinfo_map['server glx extensions'].strip()))
client_glx_extensions = set(re.split("\s*,\s*", glxinfo_map['client glx extensions'].strip()))
glx_extensions = set(re.split("\s*,\s*", glxinfo_map['GLX extensions'].strip()))
return (server_glx_extensions, client_glx_extensions, glx_extensions)
def start_shell(perf_output=None):
bin_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
if os.path.exists(os.path.join(bin_dir, 'gnome-shell.in')):
running_from_source_tree = True
top_dir = os.path.dirname(bin_dir)
plugin = os.path.join(top_dir, 'src', 'libgnome-shell.la')
typelib_dir = os.path.join(top_dir, "src")
js_dir = os.path.join(top_dir, "js")
data_dir = os.path.join(top_dir, "data")
else:
running_from_source_tree = False
plugin = 'libgnome-shell'
js_dir = os.path.join('@pkgdatadir@', 'js')
# Set up environment
# About the value of NO_GAIL and NO_AT_BRIDGE: If a11y is enabled,
# gtk_init() will normally load gail and at-bridge. But we don't
# want at-bridge to be loaded until after clutter is initialized
# (which mutter does after initializing gtk) and we don't want
# gail to be loaded at all. So set these flags. shell_a11y_init()
# will clear them so they don't get passed to gnome-shell's
# children.
env = dict(os.environ)
env.update({'GNOME_SHELL_JS' : js_dir,
'PATH' : '@MUTTER_BIN_DIR@:' + os.environ.get('PATH', ''),
'XDG_CONFIG_DIRS' : '@sysconfdir@/xdg:' + (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg'),
'XDG_DATA_DIRS' : '@datadir@:' + (os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share'),
'GNOME_DISABLE_CRASH_DIALOG' : '1',
'NO_GAIL' : '1',
'NO_AT_BRIDGE' : '1'})
if running_from_source_tree:
env.update({'GNOME_SHELL_DATADIR' : data_dir,
'GI_TYPELIB_PATH' : typelib_dir,
'GSETTINGS_SCHEMA_DIR' : data_dir })
else:
env.update({'GSETTINGS_SCHEMA_DIR' : os.path.join('@datadir@', 'glib-2.0', 'schemas')})
jhbuild_gconf_source = os.path.join('@sysconfdir@', 'gconf/2/path.jhbuild')
if os.path.exists(jhbuild_gconf_source):
env['GCONF_DEFAULT_SOURCE_PATH'] = jhbuild_gconf_source
# Work around Ubuntu xulrunner bug,
# http://bugzilla.gnome.org/show_bug.cgi?id=573413
pkgconfig = subprocess.Popen(['pkg-config', '--variable=sdkdir', 'mozilla-js'],
stdout=subprocess.PIPE)
mozjs_sdkdir = pkgconfig.communicate()[0].strip()
pkgconfig.wait()
if pkgconfig.returncode == 0:
mozjs_libdir = re.sub('-(sdk|devel)', '', mozjs_sdkdir)
if os.path.exists(mozjs_libdir + '/libmozjs.so'):
if 'LD_LIBRARY_PATH' in env and env['LD_LIBRARY_PATH']:
ld_library_path = env['LD_LIBRARY_PATH'] + ':' + mozjs_libdir
else:
ld_library_path = mozjs_libdir
env['LD_LIBRARY_PATH'] = ld_library_path
# Log everything to stderr (make stderr our "log file")
env['GJS_DEBUG_OUTPUT'] = 'stderr'
if not options.verbose:
# Unless verbose() is specified, only let gjs show errors and
# things that are explicitly logged via log() from javascript
env['GJS_DEBUG_TOPICS'] = 'JS ERROR;JS LOG'
if use_tfp:
# Decide if we need to set LIBGL_ALWAYS_INDIRECT=1 to get the
# texture_from_pixmap extension; we take having the extension
# be supported on both the client and server but not in the
# list of effective extensions as a signal of needing to force
# indirect rendering.
#
# Note that this check would give the wrong answer for Xephyr,
# but since we force !use_tfp there anyway, it doesn't matter.
(server_glx_extensions, client_glx_extensions, glx_extensions) = _get_glx_extensions()
if ("GLX_EXT_texture_from_pixmap" in server_glx_extensions and
"GLX_EXT_texture_from_pixmap" in client_glx_extensions and
(not "GLX_EXT_texture_from_pixmap" in glx_extensions)):
if options.verbose:
print "Forcing indirect GL"
# This is Mesa specific; the NVIDIA proprietary drivers
# drivers use __GL_FORCE_INDIRECT=1 instead. But we don't
# need to force indirect rendering for NVIDIA.
env['LIBGL_ALWAYS_INDIRECT'] = '1'
if options.perf is not None:
env['SHELL_PERF_MODULE'] = options.perf
if perf_output is not None:
env['SHELL_PERF_OUTPUT'] = perf_output
if options.debug:
debug_command = options.debug_command.split()
args = list(debug_command)
else:
args = []
args.extend(['mutter', '--mutter-plugins=' + plugin])
if options.replace:
args.append('--replace')
if options.sync:
args.append('--sync')
return subprocess.Popen(args, env=env)
def run_shell(perf_output=None):
if options.debug:
# Record initial terminal state so we can reset it to that
# later, in case we kill gdb at a bad time
termattrs = termios.tcgetattr(0);
normal_exit = False
xephyr = None
if options.verbose:
print "Starting shell"
try:
shell = None
if options.xephyr:
xephyr = start_xephyr()
# This makes us not grab the org.gnome.Panel or
# org.freedesktop.Notifications D-Bus names
os.environ['GNOME_SHELL_NO_REPLACE'] = '1'
shell = start_shell()
else:
shell = start_shell(perf_output=perf_output)
# Wait for shell to exit
if options.verbose:
print "Waiting for shell to exit"
shell.wait()
except KeyboardInterrupt, e:
try:
os.kill(shell.pid, signal.SIGKILL)
except:
pass
shell.wait()
finally:
# Clean up Xephyr if it outlived the shell
if xephyr:
try:
os.kill(xephyr.pid, signal.SIGKILL)
except OSError:
pass
if shell is None:
print "Failed to start shell"
elif shell.returncode == 0:
normal_exit = True
if options.verbose:
print "Shell exited normally"
elif shell.returncode < 0:
# Python has no mapping for strsignal; not worth using
# ctypes for this.
print "Shell killed with signal %d" % - shell.returncode
else:
# Normal reason here would be losing connection the X server
if options.verbose:
print "Shell exited with return code %d" % shell.returncode
if options.debug:
termios.tcsetattr(0, termios.TCSANOW, termattrs);
return normal_exit
def upload_performance_report(report_text):
# Local imports to avoid impacting gnome-shell startup time
import base64
from ConfigParser import RawConfigParser
import hashlib
import hmac
import httplib
import urlparse
import urllib
try:
config_home = os.environ['XDG_CONFIG_HOME']
except KeyError:
config_home = None
if not config_home:
config_home = os.path.expanduser("~/.config")
config_file = os.path.join(config_home, "gnome-shell/perf.ini")
try:
config = RawConfigParser()
f = open(config_file)
config.readfp(f)
f.close()
base_url = config.get('upload', 'url')
system_name = config.get('upload', 'name')
secret_key = config.get('upload', 'key')
except Exception, e:
print "Can't read upload configuration from %s: %s" % (config_file, str(e))
sys.exit(1)
# Determine host, port and upload URL from provided data, we're
# a bit extra-careful about normalization since the URL is part
# of the signature.
split = urlparse.urlsplit(base_url)
scheme = split[0].lower()
netloc = split[1]
base_path = split[2]
m = re.match(r'^(.*?)(?::(\d+))?$', netloc)
if m.group(2):
host, port = m.group(1), int(m.group(2))
else:
host, port = m.group(1), None
if scheme != "http":
print "'%s' is not a HTTP URL" % base_url
sys.exit(1)
if port is None:
port = 80
if base_path.endswith('/'):
base_path = base_path[:-1]
if port == 80:
normalized_base = "%s://%s%s" % (scheme, host, base_path)
else:
normalized_base = "%s://%s:%d%s" % (scheme, host, port, base_path)
upload_url = normalized_base + '/system/%s/upload' % system_name
upload_path = urlparse.urlsplit(upload_url)[2] # path portion
# Create signature based on upload URL and the report data
signature_data = 'POST&' + upload_url + "&&"
h = hmac.new(secret_key, digestmod=hashlib.sha1)
h.update(signature_data)
h.update(report_text)
signature = urllib.quote(base64.b64encode(h.digest()), "~")
headers = {
'User-Agent': 'gnome-shell',
'Content-Type': 'application/json',
'X-Shell-Signature': 'HMAC-SHA1 ' + signature
};
connection = httplib.HTTPConnection(host, port)
connection.request('POST', upload_path, report_text, headers)
response = connection.getresponse()
if response.status == 200:
print "Performance report upload succeeded"
else:
print "Performance report upload failed with status %d" % response.status
print response.read()
def run_performance_test():
iters = options.perf_iters
if options.perf_warmup:
iters += 1
logs = []
metric_summaries = {}
for i in xrange(0, iters):
# We create an empty temporary file that the shell will overwrite
# with the contents.
handle, output_file = tempfile.mkstemp(".json", "gnome-shell-perf.")
os.close(handle)
# Run the performance test and collect the output as JSON
normal_exit = False
try:
normal_exit = run_shell(perf_output=output_file)
finally:
if not normal_exit:
os.remove(output_file)
if not normal_exit:
return False
try:
f = open(output_file)
output = json.load(f)
f.close()
finally:
os.remove(output_file)
# Grab the event definitions and monitor layout the first time around
if i == 0:
events = output['events']
monitors = output['monitors']
if options.perf_warmup and i == 0:
continue
for metric in output['metrics']:
name = metric['name']
if not name in metric_summaries:
summary = {}
summary['description'] = metric['description']
summary['units'] = metric['units']
summary['values'] = []
metric_summaries[name] = summary
else:
summary = metric_summaries[name]
summary['values'].append(metric['value'])
logs.append(output['log'])
if options.perf_output or options.perf_upload:
# Write a complete report, formatted as JSON. The Javascript/C code that
# generates the individual reports we are summarizing here is very careful
# to format them nicely, but we just dump out a compressed no-whitespace
# version here for simplicity. Using json.dump(indent=0) doesn't real
# improve the readability of the output much.
report = {
'date': datetime.datetime.utcnow().isoformat() + 'Z',
'events': events,
'monitors': monitors,
'metrics': metric_summaries,
'logs': logs
}
# Add the Git revision if available
bin_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
if os.path.exists(os.path.join(bin_dir, 'gnome-shell.in')):
top_dir = os.path.dirname(bin_dir)
git_dir = os.path.join(top_dir, '.git')
if os.path.exists(git_dir):
env = dict(os.environ)
env['GIT_DIR'] = git_dir
revision = subprocess.Popen(['git', 'rev-parse', 'HEAD'],
env=env,
stdout=subprocess.PIPE).communicate()[0].strip()
report['revision'] = revision
if options.perf_output:
f = open(options.perf_output, 'w')
json.dump(report, f)
f.close()
if options.perf_upload:
upload_performance_report(json.dumps(report))
else:
# Write a human readable summary
print '------------------------------------------------------------';
for metric in sorted(metric_summaries.keys()):
summary = metric_summaries[metric]
print "#", summary['description']
print metric, ", ".join((str(x) for x in summary['values']))
print '------------------------------------------------------------';
return True
def restore_gnome():
# Do imports lazily to save time and memory
import gio
import gconf
# We don't want to start the new gnome-panel in the current
# directory; $HOME is better for stuff launched from it
os.chdir(os.path.expanduser("~"))
def launch_component(gconf_path):
client = gconf.client_get_default()
component = client.get_string(gconf_path)
if component == None or component == "":
return
# See gnome-session/gsm-util.c:gsm_util_find_desktop_file_for_app_name()
# The one difference is that we don't search the autostart directories,
# and just search normal application search path. (Gio doesnt' know
# how to search the autostart dirs, so we'd have to do that ourselves.)
appinfo = None
try:
appinfo = gio.unix.DesktopAppInfo(component + ".desktop")
except:
try:
appinfo = gio.unix.DesktopAppInfo("gnome-" + component + ".desktop")
except:
pass
if appinfo:
appinfo.launch()
launch_component("/desktop/gnome/session/required_components/windowmanager")
launch_component("/desktop/gnome/session/required_components/panel")
# Main program
parser = optparse.OptionParser()
parser.add_option("-r", "--replace", action="store_true",
help="Replace the running metacity/gnome-panel")
parser.add_option("-g", "--debug", action="store_true",
help="Run under a debugger")
parser.add_option("", "--debug-command", metavar="COMMAND",
help="Command to use for debugging (defaults to 'gdb --args')")
parser.add_option("-v", "--verbose", action="store_true")
parser.add_option("", "--sync", action="store_true")
parser.add_option("", "--perf", metavar="PERF_MODULE",
help="Specify the name of a performance module to run")
parser.add_option("", "--perf-iters", type="int", metavar="ITERS",
help="Numbers of iterations of performance module to run",
default=1)
parser.add_option("", "--perf-warmup", action="store_true",
help="Run a dry run before performance tests")
parser.add_option("", "--perf-output", metavar="OUTPUT_FILE",
help="Output file to write performance report")
parser.add_option("", "--perf-upload", action="store_true",
help="Upload performance report to server")
parser.add_option("", "--xephyr", action="store_true",
help="Run a debugging instance inside Xephyr")
parser.add_option("", "--geometry", metavar="GEOMETRY",
help="Specify Xephyr screen geometry",
default="1024x768");
parser.add_option("-w", "--wide", action="store_true",
help="Use widescreen (1280x800) with Xephyr")
parser.add_option("", "--eval-file", metavar="EVAL_FILE",
help="Evaluate the contents of the given JavaScript file")
parser.add_option("", "--create-extension", action="store_true",
help="Create a new GNOME Shell extension")
parser.add_option("", "--version", action="callback", callback=show_version,
help="Display version and exit")
options, args = parser.parse_args()
if args:
parser.print_usage()
sys.exit(1)
if options.create_extension and json is None:
print 'The Python simplejson module is required to create a new GNOME Shell extension'
sys.exit(1)
if options.perf and json is None:
print 'The Python simplejson module is required for performance tests'
sys.exit(1)
if options.create_extension:
print
print '''Name should be a very short (ideally descriptive) string.
Examples are: "Click To Focus", "Adblock", "Shell Window Shrinker".
'''
name = raw_input('Name: ').strip()
print
print '''Description is a single-sentence explanation of what your extension does.
Examples are: "Make windows visible on click", "Block advertisement popups"
"Animate windows shrinking on minimize"
'''
description = raw_input('Description: ').strip()
underifier = re.compile('[^A-Za-z]')
sample_uuid = underifier.sub('_', name)
# TODO use evolution data server
hostname = subprocess.Popen(['hostname'], stdout=subprocess.PIPE).communicate()[0].strip()
sample_uuid = sample_uuid + '@' + hostname
print
print '''Uuid is a globally-unique identifier for your extension.
This should be in the format of an email address (foo.bar@extensions.example.com), but
need not be an actual email address, though it's a good idea to base the uuid on your
email address. For example, if your email address is janedoe@example.com, you might
use an extension title clicktofocus@janedoe.example.com.'''
uuid = raw_input('Uuid [%s]: ' % (sample_uuid, )).strip()
if uuid == '':
uuid = sample_uuid
extension_path = os.path.join(os.path.expanduser('~/.local'), 'share', 'gnome-shell', 'extensions', uuid)
if os.path.exists(extension_path):
print "Extension path %r already exists" % (extension_path, )
sys.exit(0)
os.makedirs(extension_path)
meta = { 'name': name,
'description': description,
'uuid': uuid,
'shell-version': ['@VERSION@'] }
f = open(os.path.join(extension_path, 'metadata.json'), 'w')
try:
json.dump(meta, f)
except AttributeError:
# For Python versions older than 2.6, try using the json-py module
f.write(json.write(meta) + '\n')
f.close()
extensionjs_path = os.path.join(extension_path, 'extension.js')
f = open(extensionjs_path, 'w')
f.write('''// Sample extension code, makes clicking on the panel show a message
const St = imports.gi.St;
const Mainloop = imports.mainloop;
const Main = imports.ui.main;
function _showHello() {
let text = new St.Label({ style_class: 'helloworld-label', text: "Hello, world!" });
let monitor = global.get_primary_monitor();
global.stage.add_actor(text);
text.set_position(Math.floor (monitor.width / 2 - text.width / 2), Math.floor(monitor.height / 2 - text.height / 2));
Mainloop.timeout_add(3000, function () { text.destroy(); });
}
// Put your extension initialization code here
function main() {
Main.panel.actor.reactive = true;
Main.panel.actor.connect('button-release-event', _showHello);
}
''')
f.close()
f = open(os.path.join(extension_path, 'stylesheet.css'), 'w')
f.write('''/* Example stylesheet */
.helloworld-label {
font-size: 36px;
font-weight: bold;
color: #ffffff;
background-color: rgba(10,10,10,0.7);
border-radius: 5px;
}
''')
f.close()
print "Created extension in %r" % (extension_path, )
subprocess.Popen(['gnome-open', extensionjs_path])
sys.exit(0)
# Handle ssh logins
if 'DISPLAY' not in os.environ:
running_env = get_running_session_environs()
os.environ.update(running_env)
if options.eval_file:
f = open(options.eval_file)
contents = f.read()
f.close()
session = dbus.SessionBus()
shell = session.get_object('org.gnome.Shell', '/org/gnome/Shell')
shell = dbus.Interface(shell, 'org.gnome.Shell')
result = shell.Eval(contents)
print result
sys.exit(0)
if options.debug_command:
options.debug = True
elif options.debug:
options.debug_command = "gdb --args"
if options.wide:
options.geometry = "1280x800"
# Figure out whether or not to use GL_EXT_texture_from_pixmap. By default
# we use it iff we aren't running Xephyr, but we allow the user to
# explicitly disable it.
# FIXME: Move this to ClutterGlxPixmap like
# CLUTTER_PIXMAP_TEXTURE_RECTANGLE=disable.
if 'GNOME_SHELL_DISABLE_TFP' in os.environ and \
os.environ['GNOME_SHELL_DISABLE_TFP'] != '':
use_tfp = False
else:
# tfp does not work correctly in Xephyr
use_tfp = not options.xephyr
# We only respawn the previous environment on abnormal exit;
# for a clean exit, we assume that gnome-shell was replaced with
# something else.
normal_exit = False
# Make sure dconf daemon is running
start_dconf_await_service()
try:
if options.perf:
normal_exit = run_performance_test()
else:
normal_exit = run_shell()
finally:
if not options.xephyr and options.replace and (options.perf or not normal_exit):
restore_gnome()