#!/usr/bin/python

import atexit
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 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;

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():
    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', ''),
                'GNOME_DISABLE_CRASH_DIALOG' : '1'})

    if running_from_source_tree:
        env.update({'GNOME_SHELL_DATADIR' : data_dir,
                    'GI_TYPELIB_PATH'     : typelib_dir})

    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
    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'):
            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.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 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("", "--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")

options, args = parser.parse_args()

if args:
    parser.print_usage()
    sys.exit(1)

if options.eval_file:
    import dbus

    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

if options.verbose:
    print "Starting shell"

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

# 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

try:
    shell = None
    if options.xephyr:
        xephyr = start_xephyr()
        # This makes us not grab the org.gnome.Panel name
        os.environ['GNOME_SHELL_NO_REPLACE_PANEL'] = '1'
        shell = start_shell()
    else:
        xephyr = None
        shell = start_shell()

    # 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);

    if not options.xephyr and options.replace and not normal_exit:
        restore_gnome()
