#!/usr/bin/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 2.31.5"
    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_path = os.path.join('/usr/lib/d-conf/', 'dconf-service')
    try:
        subprocess.Popen([dconf_path, '--keep-alive'])
    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('/usr/share/gnome-shell', 'js')

    # Set up environment
    env = dict(os.environ)
    env.update({'GNOME_SHELL_JS'      : '/usr/share/gjs-1.0:/usr/lib/gjs-1.0:' + js_dir,
                'PATH'                : '/usr/bin:' + os.environ.get('PATH', ''),
                'XDG_CONFIG_DIRS'     : '/etc/xdg:' + (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg'),
                'GNOME_DISABLE_CRASH_DIALOG' : '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('/usr/share', 'glib-2.0', 'schemas')})

    jhbuild_gconf_source = os.path.join('/etc', '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
    xul_ver = subprocess.Popen(['xulrunner-1.9.2', '--gre-version'],
                                 stdout=subprocess.PIPE)
    xul_ver.wait()
    if xul_ver.returncode == 0:
        mozjs_libdir = '/usr/lib/xulrunner-' + xul_ver.communicate()[0].strip() 
        if os.path.exists(mozjs_libdir + '/libmozjs.so'):
            env['LD_LIBRARY_PATH'] = os.environ.get('LD_LIBRARY_PATH', '') + ':' + mozjs_libdir

    # 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

    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:
            xephyr = None
            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 }
    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()

    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()
