# This file is part of Parti.
# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
# Copyright (C) 2010-2013 Antoine Martin <antoine@devloop.org.uk>
# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
# Parti is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.

import sys
import os
import socket
import time
from optparse import OptionParser, OptionGroup
import logging
from subprocess import Popen, PIPE
import signal
import shlex

#this is here so we can expose the "platform" module
#before we import xpra.platform
import platform as python_platform
assert python_platform

import xpra
from xpra.dotxpra import DotXpra
from xpra.platform import (XPRA_LOCAL_SERVERS_SUPPORTED,
                           DEFAULT_SSH_CMD,
                           GOT_PASSWORD_PROMPT_SUGGESTION,
                           add_client_options)
from xpra.bytestreams import TwoFileConnection, SocketConnection

from wimpiggy.gobject_compat import import_gobject, is_gtk3
gobject = import_gobject()
try:
    import Image
    assert Image
    _has_PIL = True
except:
    _has_PIL = False
ENCODINGS = []
if is_gtk3():
    """ with gtk3, we get png via cairo out of the box
        but we need PIL for the others:
    """
    ENCODINGS.append("png")
    if _has_PIL:
        ENCODINGS.append("jpeg")
        ENCODINGS.append("rgb24")
else:
    """ with gtk2, we get rgb24 via gdk pixbuf out of the box
        but we need PIL for the others:
    """
    if _has_PIL:
        ENCODINGS.append("png")
        ENCODINGS.append("jpeg")
    ENCODINGS.append("rgb24")
#we need rgb24 for x264 and vpx (as well as the cython bindings and libraries):
if "rgb24" in ENCODINGS:
    try:
        from xpra import vpx            #@UnusedImport
        try:
            from xpra.vpx import codec      #@UnusedImport @UnresolvedImport @Reimport
            ENCODINGS.insert(0, "vpx")
        except Exception, e:
            print("cannot load vpx codec: %s" % e)
    except ImportError, e:
        #the vpx module does not exist
        #xpra was probably built with --without-vpx
        pass
    try:
        from xpra import x264           #@UnusedImport
        try:
            from xpra.x264 import codec     #@UnusedImport @UnresolvedImport
            ENCODINGS.insert(0, "x264")
        except Exception, e:
            print("cannot load x264 codec: %s" % e)
    except ImportError, e:
        #the x264 module does not exist
        #xpra was probably built with --without-x264
        pass
    try:
        from xpra.webm.decode import DecodeRGB      #@UnusedImport
        from xpra.webm.encode import EncodeRGB      #@UnusedImport
        ENCODINGS.append("webp")
    except ImportError, e:
        #the webm module does not exist
        #xpra was probably built with --without-webp
        pass
    except Exception, e:
        print("cannot load webp: %s" % e)
ENCRYPTION_CIPHERS = []
try:
    from Crypto.Cipher import AES
    assert AES
    ENCRYPTION_CIPHERS.append("AES")
except:
    pass


def get_build_info():
    info = []
    try:
        from xpra.build_info import BUILT_BY, BUILT_ON, BUILD_DATE, REVISION, LOCAL_MODIFICATIONS
        info.append("Built on %s by %s" % (BUILT_ON, BUILT_BY))
        if BUILD_DATE:
            info.append(BUILD_DATE)
        if int(LOCAL_MODIFICATIONS)==0:
            info.append("revision %s" % REVISION)
        else:
            info.append("revision %s with %s local changes" % (REVISION, LOCAL_MODIFICATIONS))
    except Exception, e:
        print("Error: could not find the build information: %s", e)
    return info


def read_config(conf_file):
    d = {}
    f = open(conf_file, "rU")
    lines = []
    for line in f:
        sline = line.strip().rstrip('\r\n').strip()
        if len(sline) == 0:
            continue
        if sline[0] in ( '!', '#' ):
            continue
        lines.append(sline)
    f.close()
    #aggregate any lines with trailing bacakslash
    agg_lines = []
    l = ""
    for line in lines:
        if line.endswith("\\"):
            l += line[:-1]
        else:
            l += line
            agg_lines.append(l)
            l = ""
    if len(l)>0:
        #last line had a trailing backslash... meh
        agg_lines.append(l)
    #parse name=value pairs:
    for sline in agg_lines:
        if sline.find("=")<=0:
            continue
        props = sline.split("=", 1)
        assert len(props)==2
        name = props[0].strip()
        value = props[1].strip()
        current_value = d.get(name)
        if current_value:
            if type(current_value)==list:
                d[name] = current_value + [value]
            else:
                d[name] = [current_value, value]
        else:
            d[name] = value
    return  d

def read_xpra_conf(conf_dir):
    d = {}
    if not os.path.exists(conf_dir) or not os.path.isdir(conf_dir):
        return  d
    conf_file = os.path.join(conf_dir, 'xpra.conf')
    if not os.path.exists(conf_file) or not os.path.isfile(conf_file):
        return  d
    return read_config(conf_file)

def read_xpra_defaults():
    #first, read the global defaults:
    if sys.platform.startswith("win"):
        conf_dir = os.path.dirname(sys.executable)
    elif sys.prefix == '/usr':
        conf_dir = '/etc/xpra'
    else:
        conf_dir = sys.prefix + '/etc/xpra/'
    defaults = read_xpra_conf(conf_dir)
    #now load the per-user config over it:
    if sys.platform.startswith("win"):
        conf_dir = os.path.join(os.environ.get("APPDATA"), "Xpra")
    else:
        conf_dir = os.path.expanduser("~/.xpra")
    user_defaults = read_xpra_conf(conf_dir)
    for k,v in user_defaults.items():
        defaults[k] = v
    return defaults


def nox():
    if "DISPLAY" in os.environ:
        del os.environ["DISPLAY"]
    # This is an error on Fedora/RH, so make it an error everywhere so it will
    # be noticed:
    import warnings
    warnings.filterwarnings("error", "could not open display")

def main(script_file, cmdline):
    if os.name=="posix" and os.getuid()==0:
        print("\nWarning: running as root")
    #################################################################
    ## NOTE NOTE NOTE
    ##
    ## If you modify anything here, then remember to update the man page
    ## (xpra.1) as well!
    ##
    ## NOTE NOTE NOTE
    #################################################################
    defaults = read_xpra_defaults()
    def parse_or_use_default(varname, default_value, parse_function):
        """ Utility method for parsing defaults """
        v = defaults.get(varname)
        if not v:
            return default_value
        try:
            pv = parse_function(v)
            if pv is not None:
                return pv
        except Exception, e:
            print("invalid value '%s' for %s: %s, using default value %s instead" % (v, varname, e, default_value))
        return default_value
    def bool_default(varname, default_value):
        def bool_parse(v):
            if type(v)==str:
                v = v.lower()
            if v in ["yes", "true", "1"]:
                return  True
            if v in ["no", "false", "0"]:
                return  False
        return parse_or_use_default(varname, default_value, bool_parse)
    def int_default(varname, default_value):
        return parse_or_use_default(varname, default_value, int)
    def int_auto_default(varname, default_value):
        def int_or_auto(val):
            if val=="auto":
                return default_value
            return int(val)
        return parse_or_use_default(varname, default_value, int_or_auto)
    def float_default(varname, default_value):
        return parse_or_use_default(varname, default_value, float)
    def string_list(varname, default_value):
        v = defaults.get(varname)
        if v is None:
            return default_value
        if type(v)==list:
            return v
        return [str(v)]

    supports_server = XPRA_LOCAL_SERVERS_SUPPORTED
    if supports_server:
        try:
            from xpra.wait_for_x_server import wait_for_x_server    #@UnresolvedImport @UnusedImport
        except:
            supports_server = False

    if supports_server:
        start_str = "\t%prog start DISPLAY\n"
        list_str = "\t%prog list\n"
        upgrade_str = "\t%prog upgrade DISPLAY\n"
        shadow_str = "\t%prog shadow DISPLAY\n"
        note_str = ""
        stop_str = "\t%prog stop [DISPLAY]\n"
    else:
        start_str, list_str, upgrade_str, shadow_str, stop_str = "", "", "", "", ""
        note_str = "(This xpra installation does not support starting local servers.)"
    parser = OptionParser(version="xpra v%s" % xpra.__version__,
                          usage="".join(["\n",
                                         start_str,
                                         "\t%prog attach [DISPLAY]\n",
                                         "\t%prog detach [DISPLAY]\n",
                                         "\t%prog screenshot filename [DISPLAY]\n",
                                         "\t%prog info [DISPLAY]\n",
                                         "\t%prog version [DISPLAY]\n",
                                         stop_str,
                                         list_str,
                                         upgrade_str,
                                         shadow_str,
                                         note_str]))
    if supports_server:
        group = OptionGroup(parser, "Server Options",
                    "These options are only relevant on the server when using the 'start' or 'upgrade' mode.")
        group.add_option("--start-child", action="append",
                          dest="children", metavar="CMD", default=string_list("start-child", [""]),
                          help="program to spawn in new server (may be repeated) (default: '%default')")
        group.add_option("--exit-with-children", action="store_true",
                          dest="exit_with_children", default=False,
                          help="Terminate server when --start-child command(s) exit")
        group.add_option("--no-daemon", action="store_false",
                          dest="daemon", default=True,
                          help="Don't daemonize when running as a server")
        group.add_option("--use-display", action="store_true",
                          dest="use_display", default=False,
                          help="Use an existing display rather than starting one with xvfb")
        DEFAULT_XVFB = "Xvfb -dpi 96 +extension Composite -screen 0 3840x2560x24+32 -nolisten tcp -noreset -auth $XAUTHORITY"
        group.add_option("--xvfb", action="store",
                          dest="xvfb",
                          default=defaults.get("xvfb", DEFAULT_XVFB),
                          metavar="CMD",
                          help="How to run the headless X server (default: '%default')")
        group.add_option("--bind-tcp", action="store",
                          dest="bind_tcp", default=None,
                          metavar="[HOST]:PORT",
                          help="Listen for connections over TCP (insecure)")
        group.add_option("--no-pulseaudio", action="store_false",
                      dest="pulseaudio", default=bool_default("pulseaudio", True),
                      help="Disable starting of a pulseaudio server for the session")
        group.add_option("--pulseaudio-command", action="store",
                      dest="pulseaudio_command", default=defaults.get("pulseaudio-command", ""),
                      help="The command used to start the pulseaudio server (default: '%default')")
        parser.add_option_group(group)

    group = OptionGroup(parser, "Server Controlled Features",
                "These options can be used to turn certain features on or off, "
                "they can be specified on the client or on the server, "
                "but the client cannot enable them if they are disabled on the server.")
    group.add_option("--no-clipboard", action="store_false",
                      dest="clipboard", default=bool_default("clipboard", True),
                      help="Disable clipboard support")
    group.add_option("--no-notifications", action="store_false",
                      dest="notifications", default=bool_default("notifications", True),
                      help="Disable forwarding of system notifications")
    group.add_option("--no-system-tray", action="store_false",
                      dest="system_tray", default=bool_default("system-tray", True),
                      help="Disable forwarding of system tray icons")
    group.add_option("--no-cursors", action="store_false",
                      dest="cursors", default=bool_default("cursors", True),
                      help="Disable forwarding of custom application mouse cursors")
    group.add_option("--no-bell", action="store_false",
                      dest="bell", default=bool_default("bell", True),
                      help="Disable forwarding of the system bell")
    group.add_option("--no-mmap", action="store_false",
                      dest="mmap", default=bool_default("mmap", True),
                      help="Disable memory mapped transfers for local connections")
    group.add_option("--readonly", action="store_true",
                      dest="readonly", default=bool_default("readonly", False),
                      help="Ignore all keyboard input and mouse events from the clients")
    group.add_option("--enable-sharing", action="store_true",
                      dest="sharing", default=bool_default("sharing", False),
                      help="Allow more than one client to connect to the same session")
    group.add_option("--no-speaker", action="store_false",
                      dest="speaker", default=bool_default("speaker", True),
                      help="Disable forwarding of sound output to the client(s)")
    group.add_option("--speaker-codec", action="append",
                      dest="speaker_codec", default=string_list("speaker-codec", []),
                      help="The audio codec to use for forwarding the speaker sound output "
                      "(you may specify more than one to define the preferred order, use 'help' to get a list of options, "
                      "when unspecified all available codecs are allowed and the first one is used)")
    group.add_option("--no-microphone", action="store_false",
                      dest="microphone", default=bool_default("microphone", True),
                      help="Disable forwarding of sound input to the server")
    group.add_option("--microphone-codec", action="append",
                      dest="microphone_codec", default=string_list("microphone-codec", []),
                      help="The audio codec to use for forwaring the microphone sound input "
                      "(you may specify more than one to define the preferred order, use 'help' to get a list of options, "
                      "when unspecified all available codecs are allowed and the first one is used)")
    parser.add_option_group(group)

    group = OptionGroup(parser, "Client Picture Encoding and Compression Options",
                "These options are used by the client to specify the desired picture and network data compression."
                "They may also be specified on the server as default values for those clients that do not set them.")
    default_encoding = defaults.get("encoding", ENCODINGS[0])
    group.add_option("--encoding", action="store",
                      metavar="ENCODING", default=default_encoding,
                      dest="encoding", type="str",
                      help="What image compression algorithm to use: %s. Default: %s" % (", ".join(ENCODINGS), default_encoding))
    if "jpeg" in ENCODINGS:
        group.add_option("-b", "--max-bandwidth", action="store",
                          dest="max_bandwidth", type="float", default=0.0, metavar="BANDWIDTH (kB/s)",
                          help="Specify the link's maximal receive speed to auto-adjust JPEG quality, 0.0 disables. (default: disabled)")
    if len(set(("jpeg", "webp", "x264")).intersection(set(ENCODINGS)))>0:
        group.add_option("--min-quality", action="store",
                          metavar="MIN-LEVEL",
                          dest="min_quality", type="int", default=int_default("min-quality", 0),
                          help="Sets the minimum x264 encoding quality allowed in automatic quality setting (from 1 to 100, 0 to leave unset). Default: %default.")
        group.add_option("--quality", action="store",
                          metavar="LEVEL",
                          dest="quality", type="int", default=int_auto_default("quality", 0),
                          help="Use a fixed image compression quality - only relevant to lossy encodings (1-100, 0 to use automatic setting). Default: %default.")
    if "x264" in ENCODINGS:
        group.add_option("--min-speed", action="store",
                          metavar="SPEED",
                          dest="min_speed", type="int", default=int_default("min-speed", 0),
                          help="Sets the minimum x264 encoding speed allowed in automatic speed setting (1-100, 0 to leave unset). Default: %default.")
        group.add_option("--speed", action="store",
                          metavar="SPEED",
                          dest="speed", type="int", default=int_auto_default("speed", 0),
                          help="Use x264 image compression with the given encoding speed (1-100, 0 to use automatic setting). Default: %default.")
    group.add_option("--auto-refresh-delay", action="store",
                      dest="auto_refresh_delay", type="float", default=float_default("auto-refresh-delay", 0.25),
                      metavar="DELAY",
                      help="Idle delay in seconds before doing an automatic lossless refresh."
                      + " 0.0 to disable."
                      + " Default: %default.")
    DEFAULT_COMPRESS = 1
    compress_str = defaults.get("compress", str(DEFAULT_COMPRESS))
    try:
        compress = int(compress_str)
    except:
        print("WARNING: invalid default value for 'compress' option, using default: %s" % DEFAULT_COMPRESS)
        compress = DEFAULT_COMPRESS
    group.add_option("-z", "--compress", action="store",
                      dest="compression_level", type="int", default=compress,
                      metavar="LEVEL",
                      help="How hard to work on compressing data."
                      + " You generally do not need to use this option,"
                      + " the default value should be adequate."
                      + " 0 to disable compression,"
                      + " 9 for maximal (slowest) compression. Default: %default.")
    parser.add_option_group(group)

    group = OptionGroup(parser, "Client Features Options",
                "These options control client features that affect the appearance or the keyboard.")
    group.add_option("--no-windows", action="store_false",
                      dest="windows_enabled", default=True,
                      help="Tells the server not to send any window data, only notifications and bell events will be forwarded - if enabled.")
    group.add_option("--session-name", action="store",
                      dest="session_name", default=None,
                      help="The name of this session, which may be used in notifications, menus, etc. Default: Xpra")
    group.add_option("--title", action="store",
                      dest="title", default="@title@ on @client-machine@",
                      help="Text which is shown as window title, may use remote metadata variables (default: '@title@ on @client-machine@')")
    group.add_option("--window-icon", action="store",
                          dest="window_icon", default=None,
                          help="Path to the default image which will be used for all windows (the application may override this)")
    # let the platform specific code add its own options:
    # adds "--no-tray" for platforms that support it
    add_client_options(group)
    group.add_option("--tray-icon", action="store",
                          dest="tray_icon", default=None,
                          help="Path to the image which will be used as icon for the system-tray or dock")
    group.add_option("--key-shortcut", action="append",
                      dest="key_shortcuts", type="str", default=[],
                      help="Define key shortcuts that will trigger specific actions."
                      + " Defaults to 'Meta+Shift+F4:quit' if no shortcuts are defined.")
    group.add_option("--no-keyboard-sync", action="store_false",
                      dest="keyboard_sync", default=bool_default("keyboard-sync", True),
                      help="Disable keyboard state synchronization, prevents keys from repeating on high latency links but also may disrupt applications which access the keyboard directly")
    parser.add_option_group(group)

    group = OptionGroup(parser, "Advanced Options",
                "These options apply to both client and server. Please refer to the man page for details.")
    group.add_option("--password-file", action="store",
                      dest="password_file", default=None,
                      help="The file containing the password required to connect (useful to secure TCP mode)")
    group.add_option("--dpi", action="store",
                      dest="dpi", default=int_default("dpi", 96),
                      help="The 'dots per inch' value that client applications should try to honour (default: %default)")
    default_socket_dir = defaults.get("socket-dir")
    default_socket_dir_str = default_socket_dir or "$XPRA_SOCKET_DIR or '~/.xpra'"
    group.add_option("--socket-dir", action="store",
                      dest="sockdir", default=default_socket_dir,
                      help="Directory to place/look for the socket files in (default: %s)" % default_socket_dir_str)
    debug_default = None
    if bool_default("debug", False):
        debug_default = "all"
    group.add_option("-d", "--debug", action="store",
                      dest="debug", default=debug_default, metavar="FILTER1,FILTER2,...",
                      help="List of categories to enable debugging for (or \"all\")")
    parser.add_option_group(group)

    group = OptionGroup(parser, "Advanced Client Options",
                "Please refer to the man page for details.")
    group.add_option("--ssh", action="store",
                      dest="ssh", default=defaults.get("ssh", DEFAULT_SSH_CMD), metavar="CMD",
                      help="How to run ssh (default: '%default')")
    group.add_option("--mmap-group", action="store_true",
                      dest="mmap_group", default=bool_default("mmap-group", False),
                      help="When creating the mmap file with the client, set the group permission on the mmap file to the same value as the owner of the server socket file we connect to (default: '%default')")
    group.add_option("--enable-pings", action="store_true",
                      dest="send_pings", default=bool_default("pings", False),
                      help="Send ping packets every second to gather latency statistics")
    group.add_option("--remote-xpra", action="store",
                      dest="remote_xpra", default=".xpra/run-xpra",
                      metavar="CMD",
                      help="How to run xpra on the remote host (default: '%default')")
    if len(ENCRYPTION_CIPHERS)>0:
        group.add_option("--encryption", action="store",
                          dest="encryption", default=None,
                          metavar="CMD",
                          help="Specifies the encryption cipher to use, only %s is currently supported. (default: None)" % (", ".join(ENCRYPTION_CIPHERS)))
    parser.add_option_group(group)

    options, args = parser.parse_args(cmdline[1:])
    if not args:
        parser.error("need a mode")
    #ensure the default values are set even though
    #the option is not shown to the user as it is not available
    if "jpeg" not in ENCODINGS:
        options.max_bandwidth = 0
    if len(set(("jpeg", "webp", "x264")).intersection(set(ENCODINGS)))==0:
        options.min_quality = int_default("min-quality", 0)
        options.quality = int_auto_default("quality", 0)
    if "x264" not in ENCODINGS:
        options.min_speed = int_default("min-speed", 0)
        options.speed = int_auto_default("speed", 0)
    try:
        int(options.dpi)
    except Exception, e:
        parser.error("invalid dpi: %s" % e)
    if len(ENCRYPTION_CIPHERS)==0:
        #if no encryption options are available the option is not shown
        #but we still need to set a value to avoid errors later on:
        options.encryption = ""
    if options.encoding and options.encoding not in ENCODINGS:
        parser.error("encoding %s is not supported, try: %s" % (options.encoding, ", ".join(ENCODINGS)))
    if options.encryption:
        assert len(ENCRYPTION_CIPHERS)>0
        if options.encryption not in ENCRYPTION_CIPHERS:
            parser.error("encryption %s is not supported, try: %s" % (options.encryption, ", ".join(ENCRYPTION_CIPHERS)))
        if not options.password_file:
            parser.error("encryption %s cannot be used without a password (see --password-file option)" % options.encryption)

    def toggle_logging(level):
        if not options.debug:
            logging.root.setLevel(level)
            return
        categories = options.debug.split(",")
        for cat in categories:
            if cat.startswith("-"):
                logging.getLogger(cat[1:]).setLevel(logging.INFO)
            if cat == "all":
                logger = logging.root
            else:
                logger = logging.getLogger(cat)
            logger.setLevel(level)

    def dump_frames(*arsg):
        import traceback
        frames = sys._current_frames()
        print("")
        print("found %s frames:" % len(frames))
        for fid,frame in frames.items():
            print("%s - %s:" % (fid, frame))
            traceback.print_stack(frame)
        print("")

    #configure default logging handler:
    mode = args.pop(0)
    if mode in ("start", "upgrade", "attach", "shadow"):
        if show_codec_help("attach" not in cmdline[1:],
                           options.speaker_codec, options.microphone_codec):
            return 0
        logging.basicConfig(format="%(asctime)s %(message)s")
    else:
        logging.root.addHandler(logging.StreamHandler(sys.stdout))

    #set debug log on if required:
    if options.debug is not None:
        toggle_logging(logging.DEBUG)
    else:
        toggle_logging(logging.INFO)

    #register posix signals for debugging:
    if os.name=="posix":
        def sigusr1(*args):
            dump_frames()
            toggle_logging(logging.DEBUG)
        def sigusr2(*args):
            toggle_logging(logging.INFO)
        signal.signal(signal.SIGUSR1, sigusr1)
        signal.signal(signal.SIGUSR2, sigusr2)

    if mode in ("start", "upgrade", "shadow") and supports_server:
        nox()
        from xpra.scripts.server import run_server
        return run_server(parser, options, mode, script_file, args)
    elif mode in ("attach", "detach", "screenshot", "version", "info"):
        return run_client(parser, options, args, mode)
    elif mode == "stop" and supports_server:
        nox()
        return run_stop(parser, options, args)
    elif mode == "list" and supports_server:
        return run_list(parser, options, args)
    elif mode == "_proxy" and supports_server:
        nox()
        return run_proxy(parser, options, args)
    else:
        parser.error("invalid mode '%s'" % mode)
        return 1

# we end up initializing gstreamer here and it does things
# we don't want with sys.argv, so hack around it:
saved_args = sys.argv
sys.argv = sys.argv[:1]
try:
    from xpra.sound import gstreamer_util   #@UnusedImport
    HAS_SOUND = True
except:
    HAS_SOUND = False
sys.argv = saved_args

def get_codecs(is_speaker, is_server):
    if not HAS_SOUND:
        return []
    try:
        from xpra.sound.gstreamer_util import can_encode, can_decode
        if (is_server and is_speaker) or (not is_server and not is_speaker):
            return can_encode()
        else:
            return can_decode()
    except Exception, e:
        print("failed to get list of codecs: %s" % e)
        return []

def show_codec_help(is_server, speaker_codecs, microphone_codecs):
    all_speaker_codecs = get_codecs(True, is_server)
    invalid_sc = [x for x in speaker_codecs if x not in all_speaker_codecs]
    hs = "help" in speaker_codecs
    if hs:
        print("speaker codecs available: %s" % (", ".join(all_speaker_codecs)))
    elif len(invalid_sc):
        print("WARNING: some of the specified speaker codecs are not available: %s" % (", ".join(invalid_sc)))
        for x in invalid_sc:
            speaker_codecs.remove(x)
    elif len(speaker_codecs)==0:
        speaker_codecs += all_speaker_codecs

    all_microphone_codecs = get_codecs(True, is_server)
    invalid_mc = [x for x in microphone_codecs if x not in all_microphone_codecs]
    hm = "help" in microphone_codecs
    if hm:
        print("microphone codecs available: %s" % (", ".join(all_microphone_codecs)))
    elif len(invalid_mc):
        print("WARNING: some of the specified microphone codecs are not available: %s" % (", ".join(invalid_mc)))
        for x in invalid_mc:
            microphone_codecs.remove(x)
    elif len(microphone_codecs)==0:
        microphone_codecs += all_microphone_codecs
    return hm or hs

def get_default_socket_dir():
    return os.environ.get("XPRA_SOCKET_DIR", "~/.xpra")

def parse_display_name(error_cb, opts, display_name):
    desc = {"display_name" : display_name}
    if display_name.startswith("ssh:") or display_name.startswith("ssh/"):
        separator = display_name[3] # ":" or "/"
        desc["type"] = "ssh"
        desc["local"] = False
        parts = display_name.split(separator)
        if len(parts)>2:
            host = separator.join(parts[1:-1])
            desc["display"] = ":" + parts[-1]
            desc["display_as_args"] = [desc["display"]]
        else:
            host = parts[1]
            desc["display"] = None
            desc["display_as_args"] = []
        desc["ssh"] = shlex.split(opts.ssh)
        full_ssh = desc["ssh"]
        upos = host.find("@")
        if upos>=0:
            username = host[:upos]
            host = host[upos+1:]
            ppos = username.find(":")
            if ppos>=0:
                password = username[ppos+1:]
                username = username[:ppos]
                desc["password"] = password
            desc["username"] = username
            full_ssh += ["-l", username]
        desc["host"] = host
        full_ssh += ["-T", desc["host"]]
        desc["full_ssh"] = full_ssh
        remote_xpra = opts.remote_xpra.split()
        if opts.sockdir:
            #ie: XPRA_SOCKET_DIR=/tmp .xpra/run-xpra _proxy :10
            remote_xpra.append("--socket-dir=%s" % opts.sockdir)
        desc["remote_xpra"] = remote_xpra
        if desc.get("password") is None and opts.password_file and os.path.exists(opts.password_file):
            try:
                try:
                    passwordFile = open(opts.password_file, "rb")
                    desc["password"] = passwordFile.read()
                finally:
                    passwordFile.close()
            except Exception, e:
                print("failed to read password file %s: %s", opts.password_file, e)
        return desc
    elif display_name.startswith(":"):
        desc["type"] = "unix-domain"
        desc["local"] = True
        desc["display"] = display_name
        desc["sockdir"] = opts.sockdir or get_default_socket_dir()
        return desc
    elif display_name.startswith("tcp:") or display_name.startswith("tcp/"):
        separator = display_name[3] # ":" or "/"
        desc["type"] = "tcp"
        desc["local"] = False
        parts = display_name.split(separator)
        port = int(parts[-1])
        if port<=0 or port>=65536:
            error_cb("invalid port number: %s" % port)
        desc["port"] = port
        desc["host"] = separator.join(parts[1:-1])
        if desc["host"] == "":
            desc["host"] = "127.0.0.1"
        return desc
    else:
        error_cb("unknown format for display name: %s" % display_name)

def pick_display(parser, opts, extra_args):
    if len(extra_args) == 0:
        # Pick a default server
        sockdir = DotXpra(opts.sockdir or get_default_socket_dir())
        servers = sockdir.sockets()
        live_servers = [display
                        for (state, display) in servers
                        if state is DotXpra.LIVE]
        if len(live_servers) == 0:
            parser.error("cannot find a live server to connect to")
        elif len(live_servers) == 1:
            return parse_display_name(parser.error, opts, live_servers[0])
        else:
            parser.error("there are multiple servers running, please specify")
    elif len(extra_args) == 1:
        return parse_display_name(parser.error, opts, extra_args[0])
    else:
        parser.error("too many arguments")

def _socket_connect(sock, endpoint, description):
    sock.connect(endpoint)
    sock.settimeout(None)
    return SocketConnection(sock, sock.getsockname(), sock.getpeername(), description)

def connect_or_fail(display_desc):
    try:
        return connect_to(display_desc)
    except Exception, e:
        sys.exit("connection failed: %s" % str(e))

def connect_to(display_desc, debug_cb=None):
    display_name = display_desc["display_name"]
    if display_desc["type"] == "ssh":
        cmd = display_desc["full_ssh"]
        if sys.platform.startswith("win"):
            password = display_desc.get("password")
            if password:
                cmd += ["-pw", password]
            cmd.append("-ssh")
            cmd.append("-agent")
        cmd += display_desc["remote_xpra"] + ["_proxy"] + display_desc["display_as_args"]
        try:
            kwargs = {}
            if os.name=="posix" and not sys.platform.startswith("darwin"):
                def setsid():
                    #run in a new session
                    os.setsid()
                kwargs["preexec_fn"] = setsid
            elif sys.platform.startswith("win"):
                from subprocess import CREATE_NEW_PROCESS_GROUP, CREATE_NEW_CONSOLE
                flags = CREATE_NEW_PROCESS_GROUP | CREATE_NEW_CONSOLE
                kwargs["creationflags"] = flags
                kwargs["stderr"] = PIPE
            if debug_cb:
                debug_cb("starting %s tunnel" % str(cmd[0]))
                #debug_cb("starting ssh: %s with kwargs=%s" % (str(cmd), kwargs))
            child = Popen(cmd, stdin=PIPE, stdout=PIPE, **kwargs)
        except OSError, e:
            raise Exception("Error running ssh program '%s': %s" % (cmd, e))
        def abort_test(action):
            """ if ssh dies, we don't need to try to read/write from its sockets """
            e = child.poll()
            if e is not None:
                error_message = "cannot %s using %s: the SSH process has terminated with exit code=%s" % (action, display_desc["full_ssh"], e)
                print(error_message)
                from wimpiggy.util import gtk_main_quit_really
                gtk_main_quit_really()
                raise IOError(error_message)
        def stop_tunnel():
            try:
                if child.poll() is None:
                    #only supported on win32 since Python 2.7
                    if hasattr(child, "terminate"):
                        child.terminate()
                    elif hasattr(os, "kill"):
                        os.kill(child.pid, signal.SIGTERM)
                    else:
                        raise Exception("cannot find function to kill subprocess")
            except Exception, e:
                print("error trying to stop ssh tunnel process: %s" % e)
        return TwoFileConnection(child.stdin, child.stdout, abort_test, target=display_name, info="SSH", close_cb=stop_tunnel)

    elif display_desc["type"] == "unix-domain":
        sockdir = DotXpra(display_desc["sockdir"])
        sock = socket.socket(socket.AF_UNIX)
        sock.settimeout(5)
        sockfile = sockdir.socket_path(display_desc["display"])
        return _socket_connect(sock, sockfile, display_name)

    elif display_desc["type"] == "tcp":
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(10)
        tcp_endpoint = (display_desc["host"], display_desc["port"])
        return _socket_connect(sock, tcp_endpoint, display_name)

    else:
        assert False, "unsupported display type in connect"

def set_signal_handlers(app):
    def deadly_signal(signum, frame):
        print("got deadly signal %s, exiting" % {signal.SIGINT:"SIGINT", signal.SIGTERM:"SIGTERM"}.get(signum, signum))
        app.cleanup()
        os._exit(128 + signum)
    def app_signal(signum, frame):
        print("\ngot signal %s, exiting" % {signal.SIGINT:"SIGINT", signal.SIGTERM:"SIGTERM"}.get(signum, signum))
        signal.signal(signal.SIGINT, deadly_signal)
        signal.signal(signal.SIGTERM, deadly_signal)
        gobject.timeout_add(0, app.quit, 128 + signum, priority=gobject.PRIORITY_HIGH)
    signal.signal(signal.SIGINT, app_signal)
    signal.signal(signal.SIGTERM, app_signal)

def run_client(parser, opts, extra_args, mode):
    if mode=="screenshot":
        if len(extra_args)==0:
            parser.error("invalid number of arguments for screenshot mode")
        screenshot_filename = extra_args[0]
        extra_args = extra_args[1:]

    if mode in ("attach"):
        sys.stdout.write("xpra client version %s\n" % xpra.__version__)
        sys.stdout.flush()

    conn = connect_or_fail(pick_display(parser, opts, extra_args))
    if opts.compression_level < 0 or opts.compression_level > 9:
        parser.error("Compression level must be between 0 and 9 inclusive.")
    if opts.quality!=-1 and (opts.quality < 0 or opts.quality > 100):
        parser.error("Quality must be between 0 and 100 inclusive. (or -1 to disable)")

    if mode=="screenshot":
        from xpra.client_base import ScreenshotXpraClient
        app = ScreenshotXpraClient(conn, opts, screenshot_filename)
    elif mode=="info":
        from xpra.client_base import InfoXpraClient
        app = InfoXpraClient(conn, opts)
    elif mode=="version":
        from xpra.client_base import VersionXpraClient
        app = VersionXpraClient(conn, opts)
    else:
        from xpra.client import XpraClient
        app = XpraClient(conn, opts)
    def got_gibberish_msg(obj, data):
        if str(data).find("assword")>0:
            sys.stdout.write("Your ssh program appears to be asking for a password.\n"
                             + GOT_PASSWORD_PROMPT_SUGGESTION)
            sys.stdout.flush()
        if str(data).find("login")>=0:
            sys.stdout.write("Your ssh program appears to be asking for a username.\n"
                             "Perhaps try using something like 'ssh:USER@host:display'?\n")
            sys.stdout.flush()
    app.connect("received-gibberish", got_gibberish_msg)
    def handshake_complete(*args):
        from wimpiggy.log import Logger
        log = Logger()
        if mode=="detach":
            log.info("handshake-complete: detaching")
            app.quit(0)
        elif mode=="attach":
            log.info("Attached to %s (press Control-C to detach)\n" % conn.target)
    app.connect("handshake-complete", handshake_complete)
    set_signal_handlers(app)
    try:
        try:
            return app.run()
        except KeyboardInterrupt, e:
            return -signal.SIGINT
    finally:
        app.cleanup()

def run_proxy(parser, opts, extra_args):
    from xpra.proxy import XpraProxy
    assert "gtk" not in sys.modules
    server_conn = connect_or_fail(pick_display(parser, opts, extra_args))
    app = XpraProxy(TwoFileConnection(sys.stdout, sys.stdin, info="stdin/stdout"), server_conn)
    signal.signal(signal.SIGINT, app.quit)
    signal.signal(signal.SIGTERM, app.quit)
    app.run()
    return  0

def run_stop(parser, opts, extra_args):
    assert "gtk" not in sys.modules
    from xpra.client_base import StopXpraClient

    def show_final_state(display):
        sockdir = DotXpra(opts.sockdir)
        for _ in range(6):
            final_state = sockdir.server_state(display)
            if final_state is DotXpra.LIVE:
                time.sleep(0.5)
        if final_state is DotXpra.DEAD:
            print("xpra at %s has exited." % display)
            return 0
        elif final_state is DotXpra.UNKNOWN:
            print("How odd... I'm not sure what's going on with xpra at %s" % display)
            return 1
        elif final_state is DotXpra.LIVE:
            print("Failed to shutdown xpra at %s" % display)
            return 1
        else:
            assert False, "invalid state: %s" % final_state
            return 1

    display_desc = pick_display(parser, opts, extra_args)
    conn = connect_or_fail(display_desc)
    e = 1
    try:
        app = StopXpraClient(conn, opts)
        e = app.run()
    finally:
        app.cleanup()
    if display_desc["local"]:
        show_final_state(display_desc["display"])
    else:
        print("Sent shutdown command")
    return  e


def may_cleanup_socket(sockdir, state, display, clean_states=[DotXpra.DEAD]):
    sys.stdout.write("\t%s session at %s" % (state, display))
    if state in clean_states:
        try:
            os.unlink(sockdir.socket_path(display))
        except OSError:
            pass
        else:
            sys.stdout.write(" (cleaned up)")
    sys.stdout.write("\n")

def run_list(parser, opts, extra_args):
    assert "gtk" not in sys.modules
    if extra_args:
        parser.error("too many arguments for mode")
    sockdir = DotXpra(opts.sockdir)
    results = sockdir.sockets()
    if not results:
        sys.stdout.write("No xpra sessions found\n")
        return  1
    sys.stdout.write("Found the following xpra sessions:\n")
    for state, display in results:
        may_cleanup_socket(sockdir, state, display)
    #now, re-probe the "unknown" ones:
    unknown = [display for state, display in results if state==DotXpra.UNKNOWN]
    if len(unknown)>0:
        sys.stdout.write("Re-probing unknown sessions: %s\n" % (", ".join(unknown)))
    counter = 0
    while len(unknown)>0 and counter<5:
        time.sleep(1)
        counter += 1
        probe_list = list(unknown)
        unknown = []
        for display in probe_list:
            state = sockdir.server_state(display)
            if state is DotXpra.DEAD:
                may_cleanup_socket(sockdir, state, display)
            elif state is DotXpra.UNKNOWN:
                unknown.append(display)
            else:
                sys.stdout.write("\t%s session at %s\n" % (state, display))
    #now cleanup those still unknown:
    clean_states = [DotXpra.DEAD, DotXpra.UNKNOWN]
    for display in unknown:
        state = sockdir.server_state(display)
        may_cleanup_socket(sockdir, state, display, clean_states=clean_states)
    return 0

if __name__ == "__main__":
    code = main("xpra.exe", sys.argv)
    if not code:
        code = 0
    sys.exit(code)
