#!/usr/bin/env python

# This file is part of Window-Switch.
# Copyright (c) 2009-2013 Antoine Martin <antoine@nagafix.co.uk>
# Window-Switch is released under the terms of the GNU GPL v3

import signal
import os
import sys
import traceback
import time
import thread

from winswitch.util.simple_logger import Logger, set_loggers_debug, msig, set_log_filename, set_log_dir
logger = Logger("controller")
debug_import = logger.get_debug_import()

debug_import("main_loop")
from winswitch.util.main_loop import loop_init, loop_run, loop_exit, callFromThread, callLater, listenUNIX
reactor = loop_init(False)


debug_import("consts")
from winswitch.consts import DEFAULT_SERVER_PORT, LOCALHOST, WINSWITCH_VERSION, NOTIFY_ERROR, NOTIFY_INFO, TYPE_NAMES
from winswitch.consts import TYPE_LAPTOP, NX_TYPE, XPRA_TYPE, VNC_TYPE, GSTVIDEO_TYPE, X11_TYPE, WINDOWS_TYPE, OSX_TYPE, SCREEN_TYPE, LIBVIRT_TYPE, VIRTUALBOX_TYPE, X_PORT_BASE
debug_import("globals")
from winswitch.globals import USER_ID, USERNAME, HOSTNAME, LINUX, OSX, WIN32
debug_import("commands_util")
from winswitch.util.commands_util import XDG_MIME_COMMAND
debug_import("which")
from winswitch.util.which import which
debug_import("common")
from winswitch.util.common import visible_command, get_machine_id, is_laptop, csv_list, is_valid_dir, is_valid_file, delete_if_exists, CAN_USE_GIO, generate_UUID, no_newlines
debug_import("selinux_util")
from winswitch.util.selinux_util import check_selinux, is_selinux_checked
debug_import("process_util")
from winswitch.util.process_util import twisted_exec, register_sigusr_debugging, get_output
debug_import("config")
from winswitch.util.config import get_local_server_config, save_local_server_config, load_session_from_file, load_object_from_properties
debug_import("file_io")
from winswitch.util.file_io import get_port_filename, get_menu_dir, get_session_filename, get_actions_dir, get_local_server_socket, get_sessions_dir
debug_import("paths")
from winswitch.util.paths import SERVER_DIR, SERVER_LOG, XSESSION_DIRS, ETC_DIR, SESSION_CONFIG_FILENAME, SESSION_CONFIG_EXT, APP_DIR, WINSWITCH_LIBEXEC_DIR
debug_import("server_portinfo")
from winswitch.util.server_portinfo import get_local_server_portinfo
debug_import("net_util")
from winswitch.net.net_util import get_port_mapper, tcp_listen_on, wait_for_socket, get_bind_IPs, has_netifaces
debug_import("icon_cache")
from winswitch.util.icon_cache import populate_pixmap_lookup
debug_import("objects.common")
from winswitch.objects.common import add_argv_overrides
debug_import("server_settings")
from winswitch.objects.server_settings import ServerSettings
debug_import("session")
from winswitch.objects.session import Session, session_status_has_actor
debug_import("server_session")
from winswitch.objects.server_session import ServerSession
debug_import("server_command")
from winswitch.objects.server_command import ServerCommand
debug_import("mount_point")
from winswitch.objects.mount_point import MountPoint
debug_import("client_channel")
from winswitch.server.client_channel import ClientChannelFactory
debug_import("all done!")

"""
the minimum client bandwidth required to send them screenshots
(prevents low bandwidth clients from being swamped with pixel data)
"""
SCREENSHOT_MIN_SPEED = 500*1000					#
SCREENSHORT_DEBUG = False

# even if VNC and NX are not installed, export the local X11 displays
ALWAYS_EXPORT_LOCALX11 = True

class WinSwitchServer:

	def __init__(self, embedded=False, ready_callback=None):
		Logger(self)
		self.embedded = embedded
		self.slog("sys.argv=%s" % csv_list(sys.argv), embedded)
		self.port_mapper = get_port_mapper()
		self.config = None
		self.ready_end_callbacks = []		#fire these callbacks when the server has finished starting up
		if ready_callback:
			self.ready_end_callbacks.append(ready_callback)
		self.cleanup_done = False
		self.clients = []					#connections to client (applet)
		self.reload_menu_scheduled = False
		self.mdns_initialized = False
		self.mdns_publisher = None
		self.menu_dir_listener = None
		self.socket_filename = None
		self.menu_monitors = {}
		self.fs_client_helpers = {}

		self.virt_utils = {}

		if not embedded:
			signal.signal(signal.SIGTERM, self.sigtermhandler)
			signal.signal(signal.SIGINT, self.siginthandler)
			register_sigusr_debugging()

	def __str__(self):
		if not self.config:
			return	"WinSwitchServer(<init>)"
		return	"WinSwitchServer(%s)" % self.config.ID

	def notify(self, title, message, delay=None, callback=None, notification_type=None, from_server=None, from_uuid=None):
		self.slog("server is not running embedded, so this message has not been shown to the user!", title, message, delay, callback, notification_type, from_server, from_uuid)

	def ask(self, title, text, nok_callback, ok_callback, password=False, ask_save_password=False, buttons=None, icon=None, UUID=None):
		self.slog("server is not running embedded, so this message has not been shown to the user!", title, text, nok_callback, ok_callback, password, ask_save_password, buttons, icon, UUID)


	def check(self):
		if USER_ID==0:
			self.serror("running as root (uid=0) is currently broken")
			return	False
		if not self.embedded and "DISPLAY" in os.environ:
			self.serror("Warning: the DISPLAY environment variable is set!")

		self.socket_filename = get_local_server_socket(USER_ID==0)
		if os.path.exists(self.socket_filename):
			import socket
			self.slog("socket %s is already present, testing it" % self.socket_filename)
			try:
				sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
				sock.settimeout(10)
				sock.connect(self.socket_filename)
				sock.close()
				self.serror("another server is already listening on socket %s - exiting" % self.socket_filename)
				return	False
			except socket.error, e:
				self.serror("socket %s is dead: '%s', removing it so we can proceed" % (self.socket_filename, e))
				os.unlink(self.socket_filename)

		self.config = get_local_server_config(resave_newerversion=True)
		self.slog("found existing config=%s" % self.config)
		if (self.config):
			add_argv_overrides(self.config)

		existing_port = get_local_server_portinfo(USER_ID==0, server_config=self.config)
		if existing_port:
			msg = "A server is already running on port %s" % str(existing_port)
			self.serror("%s" % msg)
			return	False
		return	True

	def check_for_firewall(self):
		if LINUX:
			if is_valid_file(which("iptables")) and is_valid_file(which("sshd")):
				#there is no API for testing firewall and testing ports from the system itself may bypass rules,
				#so just assume that the firewall is enabled when iptables is installed
				self.notify("Firewall detected", "'iptables' is installed so we assume that a firewall is active,\n"
						"this server will advertise that SSH tunnelling is required.\n"
						"If that is not the case, please change 'ssh_tunnel' in your server configuration.",
						notification_type=NOTIFY_INFO)
				self.config.ssh_tunnel = True
			self.save_new_server_config()
			return
		self.slog("going to test all host:ports from: %s" % csv_list(self.listen_on))
		callLater(5, self.check_firewall_final_state)
		for (host, port) in self.listen_on:
			if host=="0.0.0.0":
				if not has_netifaces:
					self.serror("'netifaces' not present, failed to detect the presence of a firewall")
					continue
				ips = get_bind_IPs()
				for ip in ips:
					self.check_firewall_on(ip, port)
			else:
				self.check_firewall_on(host, port)

	def check_firewall_on(self, host, port):
		self.sdebug(None, host, port)
		def is_firewall_already_failed():
			return self.config.firewall_test_failed
		wait_for_socket(host, port, max_wait=3,
						success_callback=lambda : self.firewall_success(host,port),
						error_callback=lambda : self.firewall_failure(host,port),
						abort_test=is_firewall_already_failed)

	def check_firewall_final_state(self):
		self.sdebug("ssh_tunnel=%s, firewall_test_failed=%s, saving new config" % (self.config.ssh_tunnel, self.config.firewall_test_failed))
		self.save_new_server_config()

	def firewall_success(self, host, port):
		self.slog(None, host, port)
		if self.config.firewall_test_failed is None:
			self.config.firewall_test_failed = False

	def firewall_failure(self, host, port):
		if self.config.firewall_test_failed is True:
			self.sdebug("previous failure already recorded and handled", host, port)
			return
		self.config.firewall_test_failed = True
		#notify the user:
		if WIN32:
			msg = "You must open up port %s on the firewall to allow clients to connect to this server" % port
		else:
			msg = "An ssh tunnel will probably be required to bypass the local firewall since test access to %s:%s has failed,\n"
			"alternatively, you can use a fixed port and allow it through the firewall" % (host,port)
		self.serror(msg, host, port)
		self.notify("Firewall block", msg, delay=20, notification_type=NOTIFY_ERROR)
		if not WIN32:			#cant use ssh_tunnel on win32
			self.serror("since this a new configuration, setting ssh_tunnel to True", host, port)
			self.config.ssh_tunnel = True

	def prepare_server_config(self):
		if not self.config:
			self.slog("creating a new server config")
			# try to load the global defaults:
			if not WIN32 and is_valid_dir(ETC_DIR):
				server_defaults = os.path.join(ETC_DIR, "server_defaults.conf")
				if is_valid_file(server_defaults):
					#these keys should never be re-used and are re-created below anyway
					ignored_keys=["ID", "crypto_modulus", "crypto_private_exponent", "crypto_public_exponent", "key_fingerprint"]
					self.config = load_object_from_properties(server_defaults, ServerSettings, ignored_keys=ignored_keys, warn_on_missing_keys=False)
					self.slog("loaded defaults from %s" % server_defaults)
			if not self.config:
				self.config = ServerSettings()
			self.config.name = HOSTNAME
			self.config.ID = "%s" % get_machine_id()
			self.config.assign_keys()
			if is_laptop():
				self.config.type = TYPE_LAPTOP
			if USER_ID==0:
				self.config.listen_on = "*:%d" % DEFAULT_SERVER_PORT	#only root uses fixed port by default
			self.config.is_new = True
			if OSX and not self.config.supports_ssh:
				""" Warn OSX users about X11Forwarding being turned off """
				def show_enable_forwarding_info(*args):
					self.sdebug(None, *args)
					import webbrowser
					webbrowser.open_new_tab("http://mactips.dwhoard.com/mactips/x11-and-terminal/x11-forwarding")
				self.notify("SSH X11 Forwarding is Disabled",
							"You may want to enable X11 display forwarding "
							"to allow remote users to start graphical X11 applications via SSH", delay=20,
							callback=("Howto enable forwarding", show_enable_forwarding_info),
							notification_type=NOTIFY_INFO)
			add_argv_overrides(self.config)
		else:
			#test for protocol support but preserve supports_ssh which is saved to the config file
			ssh = self.config.supports_ssh
			self.config.test_support_protocols(False)
			if not ssh:
				self.config.supports_ssh = False
				self.config.supports_ssh_desktop = False

			for username in self.config.prelaunch_users:
				self.add_local_user(username)
			if OSX and self.config.application_directory!=APP_DIR:
				self.serror("Application directory was changed (from %s to %s), trying to update path configuration to match" % (self.config.application_directory, APP_DIR))
				defaults = ServerSettings(skip_detection=False)
				for var_reset in ["application_directory", "nxagent_command", "xpra_command", "xnest_command", "xvnc_command", "vncshadow_command", "vncpasswd_command",
								"screen_command", "default_open_command"]:
					newvalue = getattr(defaults, var_reset)
					setattr(self.config, var_reset, newvalue)
					self.sdebug("%s set to %s" % (var_reset, newvalue))
				#re-run protocol support detection:
				self.config.test_support_protocols()
				if not APP_DIR.strip("/").startswith("Volumes"):
					self.slog("new location '%s' is not on a volume, assuming this is our final location and saving the new configuration")
					#assume that if we are not running from a mounted volume (ie: DMG just downloaded), then it is the correct new location and save it:
					save_local_server_config(self.config)
			if len(self.config.locales)==0 and not self.config.default_locale and LINUX:
				#old versions didn't have those... and we don't want to do them everytime
				#which means you can't have both 'locales' and 'default_locale' empty
				self.config.locales = self.config.detect_locales()
				self.config.default_locale = self.config.detect_default_locale()

		self.config.start_time = int(time.time())
		if self.embedded:
			""" server cannot be stopped by clients when running embedded (as it would also stop the client!) """
			self.config.clients_can_stop = False
		else:
			self.sdebug("debug_mode=%s" % self.config.debug_mode)
			set_loggers_debug(self.config.debug_mode)

		self.config.detect_xpra_version()
		#detect local config's uuid:
		try:
			from winswitch.util.config import load_uuid_from_settings
			uuid = load_uuid_from_settings()
			if uuid:
				self.config.local_session_owner = uuid
		except:
			pass
		self.slog("configuration ready: %s" % self.config)


	def start(self):
		self.slog()
		thread.start_new(self.threaded_setup, ())

		#when not running embedded: start the main loop and call cleanup when it finishes
		if not self.embedded:
			loop_run()
			self.cleanup(False)

	def threaded_setup(self):
		try:
			self.prepare_server_config()
			populate_pixmap_lookup()
			self.load_server_start_menu()
			self.detect_X_locks()
			self.assign_fs_utils()
			callFromThread(self.ready)
		except Exception, e:
			self.serr(None, e)

	def ready(self):
		try:
			self.start_menu_monitor()
			self.assign_virt_utils()
			self.selinux_check()
			self.setup_local_listener()
			self.setup_tcp_listeners()
			thread.start_new_thread(self.threaded_ready, ())
		except Exception, e:
			self.serr(None, e)

	def threaded_ready(self):
		try:
			if WIN32:
				#check for existing vnc server blocking port, etc
				callLater(30, self.win32_checks)

			#Save files first (in new threads)
			if not self.config.is_new:
				self.save_port_file()
				self.start_publish(1)
			else:
				if not self.config.ssh_tunnel:		#not tunneled? check it
					self.check_for_firewall()		#this will fire save_new_server_config() once we know the state of the firewall
				else:
					self.save_new_server_config()

			#we're done - fire callbacks
			if self.ready_end_callbacks:
				for cb in self.ready_end_callbacks:
					try:
						callFromThread(cb)
					except Exception, e:
						self.serr("error on %s" % cb, e)
		except Exception, e:
			self.serr(None, e)

	def save_new_server_config(self):
		save_local_server_config(self.config)
		self.save_port_file()
		self.start_publish(1)

	def start_publish(self, delay):
		callLater(delay, self.publish)

	def publish(self):
		self.sdebug()
		if self.mdns_initialized:
			return
		self.mdns_initialized = True
		self.slog("mDNS_publish=%s" % (self.config.mDNS_publish))
		if self.config.mDNS_publish:
			self.start_mdns_publisher()

	def load_existing_session_files(self, enabled_session_types):
		#only works for non-root...
		sessions_dir = get_sessions_dir(USERNAME, USER_ID==0)			#directory containing all the session directories
		all_files = os.listdir(sessions_dir)
		sessions = []
		for f in all_files:
			session_dir = os.path.join(sessions_dir, f)
			if not is_valid_dir(session_dir):
				continue
			session_filename = os.path.join(session_dir, SESSION_CONFIG_FILENAME+SESSION_CONFIG_EXT)
			if not is_valid_file(session_filename):
				continue
			session = load_session_from_file(session_filename, True, ServerSession)
			if session is None:
				self.slog("failed to load session from file '%s'" % session_filename)
			if session.session_type not in enabled_session_types:
				self.sdebug("session type %s is not enabled, discarded session file '%s'" % (session.session_type, f))
				continue
			if session is None or not session.display:
				self.sdebug("cowardly ignoring session file %s which does not have a display - please remove manually" % session_filename)
				continue
			self.sdebug("found %s session on display %s in state %s" % (session.session_type, session.display, session.status))
			sessions.append(session)
		return	sessions


	def selinux_check(self):
		#SELinux sanity checks:
		if not LINUX or is_selinux_checked():
			return
		def selinux_callback(selinux_enabled, selinux_enforcing, warning_bool, info):
			self.sdebug(selinux_enabled, selinux_enforcing, warning_bool, info)
			if warning_bool:
				self.notify("SELinux configuration problem", info, 20,
						notification_type=NOTIFY_ERROR)
			for util in set(self.virt_utils.values()):
				util.selinux_enforcing = selinux_enforcing
		check_selinux(selinux_callback)

	def assign_virt_utils(self):
		from winswitch.virt.vnc_server_util import VNCServerUtil

		virt_args = (self.config, self.add_new_session, self.send_remove_session, self.update_session_status, self.session_failed)
		if self.config.supports_xpra:
			try:
				from winswitch.virt.xpra_server_util import XpraServerUtil
				self.virt_utils[XPRA_TYPE] = XpraServerUtil(*virt_args)
			except Exception, e:
				self.serror("failed to load xpra, make sure it is installed: %s" % e)
		if self.config.supports_nx:
			from winswitch.virt.nx_server_util import NXServerUtil
			nx_util = NXServerUtil(*virt_args)
			self.virt_utils[NX_TYPE] = nx_util
		if self.config.supports_vnc:
			self.virt_utils[VNC_TYPE] = VNCServerUtil(*virt_args)
		if self.config.supports_gstvideo:
			from winswitch.virt.gstvideo_server_util import GSTVideoServerUtil
			self.virt_utils[GSTVIDEO_TYPE] = GSTVideoServerUtil(*virt_args)
		if self.config.supports_libvirt:
			from winswitch.virt.libvirt_server_util import LibvirtServerUtil
			self.virt_utils[LIBVIRT_TYPE] = LibvirtServerUtil(*virt_args)
		if self.config.supports_virtualbox:
			from winswitch.virt.virtualbox_server_util import VirtualboxServerUtil
			self.virt_utils[VIRTUALBOX_TYPE] = VirtualboxServerUtil(*virt_args)
		if self.config.supports_screen:
			from winswitch.virt.screen_server_util import ScreenServerUtil
			self.virt_utils[SCREEN_TYPE] = ScreenServerUtil(*virt_args)
		if self.config.export_local_displays:
			""" Note: exporting X11 displays is pointless on OSX """
			if not OSX and (ALWAYS_EXPORT_LOCALX11 or self.config.supports_nx or self.config.supports_vnc):
				from winswitch.virt.localX11_server_util import LocalX11ServerUtil
				self.virt_utils[X11_TYPE] = LocalX11ServerUtil(*virt_args)
			def needs_VNC():
				#we need vnc util to export win/osx via vnc:
				if VNC_TYPE not in self.virt_utils:
					self.virt_utils[VNC_TYPE] = VNCServerUtil(*virt_args)
			if WIN32:
				from winswitch.virt.localWindows_server_util import LocalWindowsServerUtil
				self.virt_utils[WINDOWS_TYPE] = LocalWindowsServerUtil(*virt_args)
				needs_VNC()
			if OSX:
				from winswitch.virt.localOSX_server_util import LocalOSXServerUtil
				self.virt_utils[OSX_TYPE] = LocalOSXServerUtil(*virt_args)
				needs_VNC()
		#initialize them
		for util in set(self.virt_utils.values()):
			if util.enabled:
				util.load_config()
			if util.enabled:
				util.init()
			else:
				self.slog("%s is disabled" % util.session_type)
		#remove the ones that are disabled:
		new_utils = {}
		for session_type, virt in self.virt_utils.items():
			if virt.enabled:
				new_utils[session_type] = virt
		self.virt_utils = new_utils

		#now we can delete the dead sessions:
		reloaded_sessions = self.load_existing_session_files(self.virt_utils.keys())
		for session in reloaded_sessions:
			if session.status==Session.STATUS_CLOSED:
				self.session_cleanup(session)
		#detect existing sessions:
		for util in set(self.virt_utils.values()):
			if not util.enabled:
				continue
			disk_sessions = self.live_filtered_sessions(reloaded_sessions, util.session_type)
			try:
				util.detect_existing_sessions(disk_sessions)
			except Exception, e:
				self.serr("error detecting sessions with %s, disk_sessions=%s" % (util, disk_sessions), e)
		for util in set(self.virt_utils.values()):
			if util.enabled:
				util.setup_directory_watcher()
		self.slog("=%s" % self.virt_utils)


	def session_cleanup(self, session):
		virt = self.get_virt(session)
		if virt:
			virt.session_cleanup(session)
		else:
			self.serror("ignoring session type %s" % session.session_type, session)

	def live_filtered_sessions(self, sessions, session_type):
		filtered = []
		for session in sessions:
			if session.session_type!=session_type:
				continue
			if session.status!=Session.STATUS_CLOSED:
				filtered.append(session)
		return filtered


	def win32_checks(self):
		"""
		Ensures that the VNC port is free so we can enable it, warns the user otherwise
		and gives the option to disable VNC or try to kill the process.
		"""
		#First, check that we haven't started a shadow already (dont want to kill our own process):
		windows_sessions = self.config.get_sessions_by_type(WINDOWS_TYPE)
		self.sdebug("windows_sessions=%s" % csv_list(windows_sessions))
		if windows_sessions:
			for ws in windows_sessions:
				shadows = self.config.get_all_sessions_for_display(ws.display, True)
				active_vnc_shadows = [x for x in shadows if (x.status!=Session.STATUS_CLOSED and x.session_type==VNC_TYPE)]
				if len(active_vnc_shadows)>0:
					self.slog("not checking - found active VNC shadow(s) we started: %s" % csv_list(active_vnc_shadows))
					return
		from winswitch.net.net_util import win32_netstat_parse
		netstat = win32_netstat_parse("TCP")
		self.slog("netstat=%s" % csv_list(netstat))
		for (command, host, port, pid) in netstat:
			if port==self.config.winvnc_port:
				try :
					self.win32_vnc_warn(command, host, port, pid)
				except Exception, e:
					self.serr("command=%s pid=%s: %s:%s" %(command, pid, host, port), e)

	def win32_vnc_warn(self, command, host, port, pid):
		self.slog(None, command, host, port, pid)
		def disable_vnc(*args):
			self.sdebug(None, *args)
			self.config.supports_vnc = False
			self.config.touch()
		def stop_vnc_server(*args):
			self.slog("pid=%s" % pid, *args)
			from winswitch.util.process_util import send_kill_signal
			killed = send_kill_signal(pid)
			self.debug("killed(%s)=%s" % (pid,killed), *args)
			if not killed:
				args_list = ['%s' % self.config.winvnc_command, '-stop']
				self.slog("failed to stop vnc server with kill, trying with: %s" % str(args_list))
				def winvnc_stopped(*args):
					self.slog(None, *args)
				def winvnc_not_stopped(*args):
					self.serror(None, *args)
				twisted_exec(args_list, winvnc_stopped, winvnc_not_stopped)

		self.ask('VNC port conflict',
						"The application '%s' is already using the VNC port\n"
						"VNC support cannot be enabled!\n"
						"Do you want to try to stop it?" % command,
						nok_callback=disable_vnc, ok_callback=stop_vnc_server, UUID="KILL-%s" % pid)


	def detect_X_locks(self):
		"""
		Xvnc and Xpra would refuse to start if there is a lockfile in /tmp for the given display.
		So we mark the port as taken in PortMapper which will prevent us from choosing the conflicting port.
		Also call display_callback (if it is defined) for each display found
		"""
		if WIN32:
			return
		detected = []
		for tmp_file in os.listdir("/tmp"):
			if not tmp_file.startswith(".X"):
				continue
			pos = tmp_file.find("-unix")
			if pos<0:
				pos = tmp_file.find("-lock")
			if pos<0:
				pos = len(tmp_file)
			display = tmp_file[2:pos]
			if display.startswith("IM"):
				continue
			try:
				display_no = int(display)
			except Exception, e:
				self.serror("%s is not an X lockfile: %s" % (display, e))
				continue
			port = X_PORT_BASE+display_no
			detected.append(port)
			self.port_mapper.add_taken_port(port)
			self.sdebug("found lockfile %s, marking port %d as taken" % (tmp_file, port))
		self.sdebug("detected ports: %s" % str(detected))


	def setup_tcp_listeners(self):
		# the network server:
		factory = ClientChannelFactory(self)
		listen_on = self.config.listen_on
		if len(listen_on)==0:
			listen_on = "*:"
			self.slog("'listen_on' not specified, using %s" % listen_on)

		self.listen_on = tcp_listen_on(factory, listen_on)
		if len(self.listen_on)==0:
			self.serror("failed to start, not listening on any interfaces using %s" % listen_on)
			sys.exit(1)
		else:
			self.slog("started listening on: %s" % csv_list(self.listen_on))

	def setup_local_listener(self):
		# local socket for local clients / monitoring tool
		if "--no-local-socket" in sys.argv:
			return
		if WIN32:
			self.local_listener = None			#should use named pipes but unsupported with twisted...
		else:
			factory = ClientChannelFactory(self, local=True)
			self.local_listener = listenUNIX(self.socket_filename, factory, )
			self.slog("listening on local socket '%s'" % self.socket_filename)

	def start_menu_monitor(self):
		# menu entries changed by user
		if not CAN_USE_GIO:
			self.serror("gio support is disabled! you will need to restart the server when adding or removing applications.. sorry!")
			return
		menu_home_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(":")
		menu_dirs = [os.path.join(x, "applications") for x in menu_home_dirs]
		dirs = menu_dirs+[get_menu_dir(), get_actions_dir()]+XSESSION_DIRS
		for d in dirs:
			if is_valid_dir(d):
				import gio
				gfile = gio.File(d)
				monitor = gfile.monitor_directory()
				monitor.connect("changed", self.menu_dir_changed)
				self.menu_monitors[d] = monitor

	def menu_dir_changed(self, *args):
		self.slog(None, args)
		self.schedule_reload_server_start_menu()

	def add_local_user(self, username, uuid=None, pref_session=None):
		self.sdebug(None, username, uuid, pref_session)
		if not username:
			return
		for util in set(self.virt_utils.values()):
			util.add_local_user(username, uuid)

	def start_mdns_publisher(self):
		"""
		Prepares the mDNS publisher service to announce the availability of a Window-Switch instance on the LAN.
		The actual broadcast will happen when the reactor is running.
		"""
		from winswitch.consts import MDNS_TYPE
		from winswitch.consts import APPLICATION_NAME
		if USER_ID==0 or not self.config.mDNS_publish_username:
			service_name = "%s Service" % APPLICATION_NAME
		else:
			from winswitch.globals import NAME
			if NAME:
				service_name = "%s for %s" % (APPLICATION_NAME, NAME)
			else:
				service_name = "%s for %s" % (APPLICATION_NAME, USERNAME)
		if HOSTNAME and HOSTNAME not in [LOCALHOST, "localhost", "localhost.localdomain"]:
			service_name += " on %s" % HOSTNAME
		try:
			txt = self.get_mdns_text_record()
			if not WIN32 and not OSX:
				try:
					from winswitch.net.avahi_publisher import AvahiPublishers
					self.mdns_publisher = AvahiPublishers(self.listen_on, service_name, MDNS_TYPE, txt)
				except ImportError, e:
					self.serror("failed to load avahi publisher: %s" % e)
			if self.mdns_publisher is None:
				try:
					from winswitch.net.twisted_bonjour_publisher import TwistedBonjourPublishers
					self.mdns_publisher = TwistedBonjourPublishers(reactor, self.listen_on, service_name, MDNS_TYPE, txt)
				except ImportError, e:
					self.serror("failed to load pybonjour publisher: %s" % e)
			if self.mdns_publisher:
				callFromThread(self.mdns_publisher.start)
			else:
				self.serror("if you do not wish to use mDNS, you may want to disable it in the server's config file (set mDNS_publish to False)")
		except Exception, e:
			self.serr("mDNS error: %s, mDNS is now disabled! Automatic discovery is OFF!" % e, e)

	def get_mdns_text_record(self):
		text_record = {"ID":self.config.ID}
		if self.config.mDNS_publish_username and USER_ID>0:
			text_record["username"] = USERNAME
			text_record["version"] = WINSWITCH_VERSION
		text_record["ssh_tunnel"] = "%s" % (self.config.ssh_tunnel)
		self.sdebug("=%s" % text_record)
		return	text_record

	def shutdown(self, message):
		self.slog(None, message)
		self.cleanup(True, message)
		sys.exit(0)

	def stop(self):
		""" This is used by client_base when the server is embedded in the client
			DO NOT use this method to stop the server in non-embedded mode, use shutdown() instead.
		"""
		self.slog()
		self.cleanup(False, "Client requested stop")

	def cleanup(self, stop_reactor=True, message=""):
		self.slog(None, stop_reactor, message)
		if self.cleanup_done:
			self.sdebug("already clean")
			return
		self.cleanup_done = True
		if self.config.close_sessions_on_exit:
			for session in self.config.get_live_sessions(True):
				self.kill_session(session)
		for client in self.clients:
			client.send_plain_message("%s is shutting down" % self.config.name, message)
			client.stop()
		if self.mdns_publisher:
			self.mdns_publisher.stop()
		for util in set(self.virt_utils.values()):
			util.stop()
		self.delete_port_file()
		self.disconnect_clients()
		if stop_reactor and not self.embedded:
			loop_exit()

	def sigtermhandler(self, *args):
		self.slog(None, args)
		self.cleanup(False)
		sys.exit(0)

	def siginthandler(self, *args):
		self.slog(None, args)
		self.cleanup(False)
		sys.exit(0)


	def assign_fs_utils(self):
		from winswitch.fs.smb_client_helper import Samba_Client_Helper
		self.fs_client_helpers[MountPoint.SMB] = Samba_Client_Helper(self.config)
		from winswitch.fs.nfs_client_helper import NFS_Client_Helper
		self.fs_client_helpers[MountPoint.NFS] = NFS_Client_Helper(self.config)
		self.slog("fs_client_helpers=%s" % self.fs_client_helpers)

	def add_mount_point(self, user, mount_point, close_callbacks):
		if not self.config.mount_client:
			return
		client_helper = self.fs_client_helpers.get(mount_point.protocol)
		if not client_helper:
			self.serror("unknown file protocol: %s" % mount_point.protocol, user, mount_point, close_callbacks)
			return
		self.sdebug("client_helper(%s)=%s" % (mount_point.protocol, client_helper), user, mount_point, close_callbacks)
		if not client_helper.mount(user, mount_point, close_callbacks):
			self.slog("failed to mount via %s" % client_helper, user, mount_point, close_callbacks)


	def kill_session(self, session):
		self.slog(None, session)
		remote_util = self.get_virt(session)
		if not remote_util:
			self.serror("cannot find valid utility class for session type: %s" % session.session_type, session)
			return
		user = None
		for uuid in [session.actor, session.owner]:
			if not uuid:
				continue
			user = self.config.get_user_by_uuid(uuid)
			if user:
				break
		remote_util.stop_display(session, user, session.display)

	def get_client(self, UUID, not_client=None):
		assert UUID
		not_connected = []
		for client in self.clients:
			if not_client and not_client==client:
				continue
			if client.user and client.user.uuid == UUID:
				if client.is_connected():
					return	client
				else:
					not_connected.append(client)
		if len(not_connected)>0:
			return	not_connected[0]
		return	None

	def send_message_to_user(self, UUID, title, message):
		client = self.get_client(UUID)
		if client:
			client.send_plain_message(title, message)
		else:
			self.sdebug("client not found!", UUID, title, message)

	def disconnect_session(self, session, uuid):
		self.sdebug(None, session, uuid)
		display_util = self.get_virt(session)
		display_util.disconnect(session)

	def send_session_to_user(self, session, requested_by_uuid, UUID):
		sig = "%s status=%s" % (msig(session, requested_by_uuid, UUID), session.status)
		self.log(sig)
		target_user = self.config.get_user_by_uuid(UUID)
		if not target_user:
			self.error("%s target user is unknown!" % sig)
			return
		display_util = self.get_virt(session)
		if not display_util.can_send_session(session, target_user):
			self.error("%s, session cannot be sent!" % sig)
			return

		def send_prepared_session():
			self.sdebug()
			self.do_send_session_to_user(session, UUID, deadline=time.time()+30)

		disconnect = False
		if session.status in [Session.STATUS_CONNECTED, Session.STATUS_IDLE]:
			if not session.actor:
				self.error(sig+" actor is missing! forcing disconnection")
				disconnect = True
			elif session.actor==requested_by_uuid:
				#request may come from the current actor,
				#in which case we don't need to send the request to disconnect back to him!
				#it is assumed that he will be disconnecting shortly (the send session code on the client does this)
				self.debug(sig+" request comes from actor, not asking it to disconnect")
			else:
				#Ask the current actor to disconnect
				client = self.get_client(session.actor)
				if client:
					self.debug(sig+" asking actor %s to send it" % session.actor)
					def force_send(last_count):
						self.sdebug("status_update_count=%s" % session.status_update_count, last_count)
						if session.status_update_count==last_count:
							""" update count has not changed, so the other client has failed to disconnect, force it """
							display_util.prepare_session_for_attach(session, target_user, True, send_prepared_session)
					callLater(30, force_send, session.status_update_count)
					client.handler.send_session_to_user(session.ID, UUID, session.password)
				else:
					disconnect = True
					self.log(sig+" not connected to client... session may get forcibly disconnected")
		else:
			self.debug(sig+" session is not connected")
		display_util.prepare_session_for_attach(session, target_user, disconnect, send_prepared_session)

	def prepare_and_send_session_to_user(self, session, user):
		if session.actor==user.uuid:
			self.slog("user is already connected", session, user)
			return
		display_util = self.get_virt(session)
		deadline = time.time()+30
		self.sdebug("display_util=%s, deadline=%s" % (display_util, deadline), session, user)
		def session_prepared():
			self.sdebug()
			self.do_send_session_to_user(session, user.uuid, deadline=deadline)
		display_util.prepare_session_for_attach(session, user, False, session_prepared)

	def do_send_session_to_user(self, session, UUID, deadline=0):
		"""
		This method is called after display_util has successfully prepared the session
		and the user can attach to it.
		"""
		sig = msig(session, UUID, deadline)
		if deadline!=0 and deadline<time.time():
			self.error(sig+" event triggered past deadline!")
			return
		client = self.get_client(UUID)
		if not client:
			self.error(sig+" client channel not found!")
			return
		self.log(sig+" found matching channel %s" % client)
		session.actor = UUID
		self.update_session_status(session, Session.STATUS_CONNECTING)
		self.send_session_status_to_all(session)
		client.handler.send_session_to_user(session.ID, UUID, session.password),


	def call_list_with_embargo(self, call_list, max_time=0):
		if max_time>0 and time.time() > max_time:
			self.serror("too late!", call_list, max_time)
			return
		self.slog(None, call_list, max_time)
		for call in call_list:
			call()


	def session_failed(self, session, message=None, new_session_status=Session.STATUS_CLOSED):
		self.slog("owner=%s" % session.owner, session, message, new_session_status)
		if session.status!=new_session_status:
			self.update_session_status(session, new_session_status, None)
		if session.owner:
			self.send_message_to_user(session.owner, "Session '%s' Failed" % session.name, message)

	#also sends session info to all users that can access it
	#ensures that the session data is there by the time we get the connect() request
	def send_new_session_to_user(self, session, UUID):
		self.sdebug(None, session, UUID)
		client = self.get_client(UUID)
		if client and client.can_access_session(session):
			self.slog("sending to %s" % client.user, session, UUID)
			client.send_session(session)

	def schedule_capture(self, session):
		if SCREENSHORT_DEBUG:
			self.sdebug("capture_pending=%s" % session.capture_pending, session)
		if session.capture_pending or session.capture_delay<=0 or session.status in [Session.STATUS_CLOSED, Session.STATUS_UNAVAILABLE]:
			return	False		#no need to run again
		util = self.get_virt(session)
		if not util.can_capture(session):
			self.sdebug("cannot capture this session", session)
			return
		if not util.can_capture_now(session):
			#wait until the session can be captured,
			#check whenever the session changes until we can:
			def check_can_capture_now(*args):
				if util.can_capture(session):
					self.do_schedule_capture(session)
					return	False
				return True
			session.add_modified_callback(check_can_capture_now)
			return
		self.do_schedule_capture(session)

	def do_schedule_capture(self, session):
		if not session.capture_pending:
			session.capture_pending = True
			callLater(2, self.capture_display, session)
			callLater(session.capture_delay, self.schedule_capture, session)
		return	True			#may schedule again


	def capture_display(self, session):
		"""
		Delegates the capture to the remote_util for this session type
		and sends the updated screenshot to all the suitable clients.
		"""
		if SCREENSHORT_DEBUG:
			self.sdebug("preload=%s" % session.preload, session)
		if session.preload or session.status==Session.STATUS_CLOSED:
			return
		util = self.get_virt(session)
		def send_new_capture_data(*args):
			self.sdebug("session=%s, util=%s, clients=%s" % (session, util, self.clients), *args)
			#go back to normal speed:
			session.capture_delay = self.config.screen_capture_delay
			#send new screenshot to suitable clients
			for client in self.clients:
				try:
					if SCREENSHORT_DEBUG:
						self.sdebug("client.user=%s, client.closed=%s, client.handler=%s" % (client.user, client.closed, client.handler), session)
					if not client.user or client.closed or not client.handler:
						continue
					do_send = client.user.line_speed>=SCREENSHOT_MIN_SPEED		#only send screen capture if bandwidth is sufficient
					if SCREENSHORT_DEBUG:
						self.sdebug("sending to %s, version=%s : %s" % (client, client.user.app_version, do_send), session)
					if do_send:
						client.handler.send_session_icon(session, True)
				except Exception, e:
					self.serr("failed to send new screenshot to %s - %s" % (client, client.user), e, session)
			session.capture_pending = False
		def capture_err(*args):
			self.sdebug(None, *args)
			#run less often as it failed
			session.capture_delay = min(session.capture_delay*2, self.config.screen_capture_delay*10)
			session.capture_pending = False
		util.capture_display(session, send_new_capture_data, capture_err)

	def set_new_user_keymap(self, user):
		""" find all the sessions in use by this user and apply the xmodmap """
		sessions = self.config.get_connected_sessions(user.uuid, or_connecting=True)
		self.log("(...) will now update these connected/connecting sessions with the new xmodmap: %s" % csv_list(sessions))
		for session in sessions:
			virt = self.get_virt(session)
			virt.set_keyboard_mappings(session, user)

	def add_new_session(self, session):
		"""
		This is called when a ServerUtilBase instance detects or creates a new session.
		We use this opportunity to register the hook to do the screen capture (if the session supports it)
		"""
		util = self.get_virt(session)
		can_capture = util and util.can_capture(session)
		self.sdebug("can_capture=%s, screen_capture_command=%s" % (can_capture, self.config.screen_capture_command), session)
		if can_capture:
			self.schedule_capture(session)
			def status_change_schedule_capture():
				return self.schedule_capture(session)
			session.add_status_update_callback(None, None, status_change_schedule_capture, clear_it=False, timeout=None)
		self.send_session_to_all(session)

	def send_session_to_all(self, session):
		self.slog("clients=%s" % csv_list(self.clients), session)
		for client in self.clients:
			if client.can_access_session(session):
				client.send_session(session)

	def send_session_status_to_all(self, session, status_override=None):
		self.slog("clients=%s" % csv_list(self.clients), session, status_override)
		if status_override is None:
			status = session.status
		else:
			status = status_override
		for client in self.clients:
			if client.can_access_session(session):
				client.send_session_status(session, status)

	def send_remove_session(self, session):
		for client in self.clients:
			client.send_remove_session(session)

	def disconnect_clients(self):
		for client in self.clients:
			client.disconnect()

	def schedule_reload_server_start_menu(self):
		callFromThread(self.do_schedule_reload_server_start_menu)

	def do_schedule_reload_server_start_menu(self):
		if self.reload_menu_scheduled and self.reload_menu_scheduled.active():
			action = "reset"
			self.reload_menu_scheduled.reset(5)
		else:
			action = "scheduled"
			self.reload_menu_scheduled = callLater(5, self.reload_server_start_menu)
		self.slog("%s: %s, due at %s" % (action, self.reload_menu_scheduled, self.reload_menu_scheduled.getTime()))

	def reload_server_start_menu(self):
		self.debug()
		self.reload_menu_scheduled = None
		thread.start_new_thread(self.do_reload_server_start_menu, ())

	def do_reload_server_start_menu(self):
		self.debug()
		try:
			self.load_server_start_menu()
			self.send_updated_menu_to_clients()
		except Exception, e:
			self.exc(e)
		self.slog("done")

	def send_updated_menu_to_clients(self):
		self.log()
		for client in self.clients:
			if client.is_connected() and client.user:			#commands are only visible to authenticated users
				client.send_sync()	#send a full sync so the client can timeout the old commands with on_syncend

	def load_server_start_menu(self):
		self.debug()
		from winswitch.util.load_desktop_menus import load_start_menu, load_desktop_commands, load_actions
		(commands, dirs) = load_start_menu(self.config.ignored_commands, self.config.whitelist_commands)
		self.config.server_commands = commands
		self.config.menu_directories = dirs
		self.config.desktop_commands = load_desktop_commands(self.config.ignored_xsessions, self.config.whitelist_xsessions)
		self.config.action_commands = load_actions()
		self.slog("found %s server commands, %s menu directories, %s desktop commands, %s shortcuts" % (len(commands), len(dirs), len(self.config.desktop_commands), len(self.config.action_commands)))

	def delete_port_file(self):
		port_filename = get_port_filename()
		delete_if_exists(port_filename)

	def save_port_file(self):
		self.debug()
		port_filename = get_port_filename()
		f = file(port_filename, "w")
		for host, port in self.listen_on:
			f.write("%s:%d\n" % (host,port))
		f.close()

	def add_client(self, channel):
		self.sdebug("user=%s" % channel.user, channel)
		assert channel.user
		for client in self.clients:
			assert channel.handler
			client.handler.send_user(channel.user)
		self.clients.append(channel)

	def remove_client(self, channel):
		self.sdebug(None, channel)
		if channel in self.clients:
			self.clients.remove(channel)
		#mark sessions as idle:
		if not channel.user or not channel.user.uuid:
			return
		uuid = channel.user.uuid
		for session in self.config.sessions.values():
			if session.actor == uuid and session.status==Session.STATUS_CONNECTED:
				session.set_status(Session.STATUS_IDLE)
				self.send_session_status_to_all(session)
		#remove user
		if channel.user and channel.user.uuid:
			uuid = channel.user.uuid
			for client in self.clients:
				client.handler.send_remove_user(uuid)
		#notify virt implementations:
		for util in set(self.virt_utils.values()):
			self.sdebug("removing %s from %s" % (uuid, util))
			util.remove_user(uuid)



	def send_all(self, msg, except_channel=None):
		self.slog("clients=%s" % csv_list(self.clients), visible_command(msg), except_channel)
		for channel in self.clients:
			if channel != except_channel:
				channel.send_message(msg)

	def open_file(self, user, filename, mode):
		thread.start_new_thread(self.do_open_file, (user, filename, mode))

	def do_open_file(self, user, filename, mode):
		self.slog(None, user, filename, mode)
		name = filename
		pos = name.rfind("/")
		if pos>0:
			name = ": %s" % name[pos+1:]
		server_command = None
		if is_valid_file(XDG_MIME_COMMAND):
			def xdg_mime_exec(args):
				cmd = [XDG_MIME_COMMAND]+args
				try:
					code, out, _ = get_output(cmd)
					return code, no_newlines(out)
				except Exception, e:
					self.serr("failed to run %s" % XDG_MIME_COMMAND, e, args)
					return -1, None

			""" Try to find what command we should be using """
			status, mimetype = xdg_mime_exec(["query", "filetype", "filename"])
			self.slog("mimetype=%s" % mimetype, user, filename, mode)
			if status==0 and mimetype:
				status, desktop_filename = xdg_mime_exec(["query", "default", mimetype])
				self.slog("desktop filename=%s" % desktop_filename, user, filename, mode)
				if status==0 and desktop_filename:
					""" Look for the desktop file in the launchers """
					noext = desktop_filename
					if desktop_filename.endswith(".desktop"):
						noext = desktop_filename[:len(desktop_filename)-len(".desktop")]
					#self.sdebug("looking for %s in %s" % (noext, ",".join(x.filename for x in self.config.server_commands)), user, filename, mode)
					for cmd in self.config.server_commands:
						if cmd.filename==noext:
							server_command = cmd
							self.slog("found matching command entry: %s" % cmd, user, filename, mode)
							break
					if not server_command:
						self.slog("could not find a matching command entry for: %s" % noext, user, filename, mode)
		if not server_command:
			""" Open using our 'mime_open' wrapper script - hoping it will manage to figure out what to do.. """
			server_command = ServerCommand(generate_UUID(), name,
								self.config.default_open_command,	#not used as-is. just here as a placeholder
								"nc", None)
			#override with dynamic command:
			def get_open_command(display, fn):
				session_file = get_session_filename(display, username=USERNAME, as_root=USER_ID==0)
				open_cmd = self.config.default_open_command
				if not is_valid_file(open_cmd.split(" ")[0]):
					if WINSWITCH_LIBEXEC_DIR:
						open_cmd = os.path.join(WINSWITCH_LIBEXEC_DIR, "mime_open")
					else:
						open_cmd = "mime_open"
					self.serror("invalid default_open_command='%s', using default '%s' instead" % (self.config.default_open_command, open_cmd), display, filename)

				cmd = "%s -f '%s' '%s'" % (open_cmd, session_file, filename)
				self.sdebug("=%s" % cmd, display, filename)
				return	cmd
			server_command.get_command_for_display = get_open_command
		callFromThread(self.real_start_session, user, user.preferred_session_type, server_command, None, None, [filename])

	def start_session(self, user, session_type, server_command, screen_size, opts, filenames=None):
		self.slog(None, user, session_type, server_command, screen_size, opts, filenames)
		self.real_start_session(user, session_type, server_command, screen_size, opts, filenames)

	def real_start_session(self, user, session_type, server_command, screen_size, opts, filenames=None):
		try:
			assert user
			self.sdebug(None, user, session_type, server_command, screen_size, opts, filenames)
			if server_command and server_command.run_once_only:
				#chech that there isn't a session with this run_once_only session
				existing = None
				for session in self.config.get_live_sessions(allow_shadow=False):
					if session.command_uuid==server_command.uuid:
						existing = session
						break
				if existing:
					#tell user this wouldn't work!
					self.send_message_to_user(user.uuid, "Cannot start session", "The application %s can only be started once and there is already a copy running" % server_command.name)
					return

			display_util = self.get_remote_util(session_type)
			if not display_util:
				self.send_message_to_user(user.uuid, "Cannot start session", "You have requested an '%s' session but this server does not support it" % session_type)
				return	None
			session = display_util.start_session(user, server_command, screen_size, opts, filenames)
			#fire "when_ready" if the session starts ok, "on_error" otherwise:
			#TODO: we could mark sessions as "ready" to avoid testing pre-launch sessions twice
			def session_started(*args):
				display_util.prepare_display(session, user)
				if session.status not in [Session.STATUS_CONNECTED, Session.STATUS_CONNECTING]:
					self.prepare_and_send_session_to_user(session, user)
			def session_error(*args):
				self.session_not_ready(session, user)
			if session is None:
				self.serror("something went wrong, session is None..", user, session_type, server_command, screen_size, opts, filenames)
				session_error()
			elif session.status in [Session.STATUS_AVAILABLE, Session.STATUS_CONNECTED, Session.STATUS_CONNECTING, Session.STATUS_IDLE]:
				self.sdebug("session is already running: %s" % session, user, session_type, server_command, screen_size, opts, filenames)
				session_started()
			else:
				display_util.wait_for_session_readyness(session, user, success_callback=session_started, error_callback=session_error)
		except Exception, e:
			self.serr(None, e, user, session_type, server_command, screen_size, opts, filenames)

	def session_not_ready(self, session, user):
		self.send_message_to_user(user.uuid, "Session failed", "The session has started but it is not responding")

	def shadow_session(self, session, user, read_only, shadow_type, screen_size, options):
		self.sdebug(None, session, user, read_only, shadow_type, screen_size, options)
		try:
			assert session
			assert session.can_be_shadowed(shadow_type) is True
			assert shadow_type in [NX_TYPE, VNC_TYPE, XPRA_TYPE, GSTVIDEO_TYPE]
			shadow_util = self.get_remote_util(shadow_type)
			self.sdebug("shadow_util=%s" % shadow_util, session, user, read_only, shadow_type, screen_size, options)
			if not shadow_util:
				self.send_message_to_user(user.uuid, "Cannot shadow display", "The session shadow type %s is not supported by this server" % TYPE_NAMES.get(shadow_type))
				self.serror("no remote util found for type", session, user, read_only, shadow_type, screen_size, options)
				return
			shadow = shadow_util.get_unique_shadow(session, user, read_only)
			if shadow:
				self.sdebug("found unique shadow=%s, sending to user immediately", session, user, read_only, shadow_type, screen_size, options)
				self.do_send_session_to_user(shadow, user.uuid)
				return
			shadow = shadow_util.create_shadow_session(session, user, read_only, screen_size, options)
			if not shadow:
				self.send_message_to_user(user.uuid, "Cannot shadow display", "The system failed to create the %s shadow" % TYPE_NAMES.get(shadow_type))
				self.serror("%s failed to create shadow" % shadow_util, session, user, read_only, shadow_type, screen_size, options)
				return
			shadow_util.shadow_start(shadow, user,
					success_callback=lambda : self.do_send_session_to_user(shadow, user.uuid),
					error_callback=lambda : self.session_not_ready(shadow, user))
		except Exception, e:
			self.serr(None, e, session, user, read_only, shadow_type, screen_size, options)
			self.send_message_to_user(user.uuid, "Cannot shadow display", "The system encountered an error creating the %s shadow: %s" % (TYPE_NAMES.get(shadow_type), str(e)))


	def get_virt(self, session):
		return	self.get_remote_util(session.session_type)

	def get_remote_util(self, session_type):
		return	self.virt_utils.get(session_type)

	def update_session_status(self, session, status, preload=None, actor=None):
		"""
		This is used by xxx_server_util to tell us about status changes detected in the server process.
		We clear the actor if the session is no longer connected.
		Set the status if the new value is allowed, and propagate the change to all the clients.
		If closed we remove the session.
		If connecting: set a timer to ensure we go back to available after a configurable timeout (not used for NX which does its own timeout in nxagent).
		"""
		assert session
		if session.status==Session.STATUS_CLOSED:
			self.sdebug("session already removed", session, status, preload)
			return
		if session.ID not in self.config.sessions:
			self.serror("cannot find session", session, status, preload)
			return

		if session.status==status and (preload is None):
			return				#no change
		if status not in Session.ALL_STATUS:
			self.serror("illegal status", session, status, preload)
			return

		if preload is not None:
			session.preload = preload
		if not session_status_has_actor(status):
			self.remove_actor(session)		#actor is only meaningful when connected
		elif actor:
			session.actor = actor
		self.send_session_status_to_all(session, status)			#send the new status before setting it locally (gives clients a chance to close)
		session.set_status(status)									#finally set it locally
		display_util = self.get_virt(session)
		assert display_util
		if status == Session.STATUS_CLOSED:
			self.config.remove_session(session)
			self.send_remove_session(session)
		elif status == Session.STATUS_CONNECTING:
			connecting_timeout = display_util.get_connecting_timeout()
			self.sdebug("connecting_timeout=%s" % connecting_timeout, session, status, preload)
			if connecting_timeout>0:
				callLater(connecting_timeout, self.connecting_timeout, session, session.status_update_count)

	def connecting_timeout(self, session, status_update_count):
		if session.status_update_count == status_update_count and session.status == Session.STATUS_CONNECTING:
			self.slog("connection timed out", session, status_update_count)
			self.update_session_status(session, Session.STATUS_AVAILABLE)
		else:
			self.sdebug("not timing out, status=%s, current status_update_count=%s" % (session.status, session.status_update_count), session, status_update_count)

	def remove_actor(self, session):
		"""
		"""
		if not session.actor:
			return
		self.sdebug("actor=%s" % session.actor, session)
		display_util = self.get_virt(session)
		user = self.config.get_user_by_uuid(session.actor)
		if not user:
			self.serror("user not found for actor=%s" % session.actor, session)
		display_util.session_detached(session, user)
		session.actor = ""		#actor is only meaningful when connected




def main():
	set_log_dir(SERVER_DIR)
	set_log_filename(SERVER_LOG)
	server = WinSwitchServer()
	exit_code = 2
	if server.check():
		exit_code = 0
		try :
			server.start()
		except Exception, e:
			print("winswitch_server.py")
			print('*** Caught exception: %s' % e)
			traceback.print_exc()
			exit_code = 1
		server.cleanup()
	sys.exit(exit_code)

if __name__ == "__main__":
	main()
