#!/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 sys
from winswitch.util.simple_logger import Logger
logger = Logger("client_base")
debug_import = logger.get_debug_import()

debug_import("os/traceback/commands/signal/thread/time")
import os
import signal
import thread
import time
import re


debug_import("globals")
# Our imports
from winswitch.globals import USER_ID, OSX, WIN32, call_exit_hooks
debug_import("consts")
from winswitch.consts import MDNS_TYPE, NOTIFY_ERROR, NOTIFY_INFO, MAX_SPEED, APPLICATION_NAME, MDNS_INFO_URL, LOCALHOST
debug_import("objects.common")
from winswitch.objects.common import add_argv_overrides
debug_import("server_config")
from winswitch.objects.server_config import ServerConfig
debug_import("global_settings")
from winswitch.objects.server_config import get_settings
debug_import("session")
from winswitch.objects.session import Session, session_status_has_actor
debug_import("config")
from winswitch.util.config import delete_server_config, load_settings, save_exports, save_avatar, save_settings, \
		load_server_configs, find_server_config, load_server_config, get_local_server_config, modify_server_config, load_server_config_from_file, save_server_config, \
		make_server_config_from_settings
debug_import("common")
from winswitch.util.common import get_elapsed_time_string, get_bool, get_int, is_valid_file, is_valid_dir, csv_list, alphanumfile, get_as_root_list, visible_command, CAN_USE_GIO
debug_import("selinux_util")
from winswitch.util.selinux_util import check_selinux
debug_import("process_util")
from winswitch.util.process_util import dump_threads, start_dump_threads, exec_daemonpid_wrapper, register_sigusr_debugging, get_output
debug_import("commands_util")
from winswitch.util.commands_util import XMODMAP_COMMAND, XPRA_COMMAND, SSH_COMMAND, NXPROXY_COMMAND, VNC_COMMAND, XMING_COMMAND, RDESKTOP_COMMAND, XFREERDP_COMMAND, XTERM_COMMAND, WINSWITCH_SERVER_COMMAND
debug_import("file_io")
from winswitch.util.file_io import get_lock_dir, get_port_filename, load_binary_file
debug_import("simple_logger")
from winswitch.util.simple_logger import set_loggers_debug, msig
debug_import("server_link")
from winswitch.client.server_link import ServerLink
debug_import("local_common")
from winswitch.net.local_common import COMMAND_WAKEUP, COMMAND_OPEN_SERVER_CONFIG, make_local_listener, make_local_client
debug_import("net_util")
from winswitch.net.net_util import is_localhost, get_interface_speed, if_indextoname, has_netifaces, get_bind_IPs
debug_import("format_util")
from winswitch.util.format_util import format_message
debug_import("main_loop")
from winswitch.util.main_loop import callLater, callFromThread
debug_import("all imports done")

#For testing only
FAKE_LINK_ERROR=False
FAKE_CLIENT_ERROR=False


""" Force to use file transfer code even for local servers - useful for testing """
FILE_TRANSFER_TO_LOCAL = "--transfer-to-local" in sys.argv

class ClientBase:
	"""
	This is the core client class.
	It will linkup to servers and respond to requests,
	starting/stopping sessions as required.

	You must subclass this to implement a real client, see:
	- client.py for a text client
	- applet.py for the pygtk application
	"""

	def open_url(self, url):
		self.slog(None, url)

	def show_server_config(self, server):
		self.slog("server=%r" % server, server)

	def show_settings(self):
		self.slog("settings=%r" % self.settings)

	def notify(self, title, message, delay=None, callback=None, notification_type=None, from_server=None, from_uuid=None):
		self.serror("notification_util not defined!", title, message, delay, callback, notification_type, from_server, from_uuid)

	def server_message(self, *args):
		self.serr(None, *args)
		raise Exception("This must be overridden")

	def ask(self, title, text, nok_callback, ok_callback, password=False, ask_save_password=False, buttons=None, icon=None, UUID=None):
		if self.dialog_util:
			self.dialog_util.ask(title, text, nok_callback, ok_callback, password=password, ask_save_password=ask_save_password, buttons=buttons, icon=icon, UUID=UUID)
		else:
			self.serror("dialog_util not defined!", title, text, nok_callback, ok_callback, password, ask_save_password, buttons, icon, UUID)

	def cancel_ask(self, UUID):
		if self.dialog_util:
			self.dialog_util.cancel_ask(UUID)
		else:
			self.serror("dialog_util not defined!", UUID)

	def attention(self, message=None, delay=5):
		self.sdebug(None, message, delay)


	def __init__(self, reactor, reactor_run, reactor_stop):
		Logger(self)
		self.sdebug(None, reactor_run, reactor_stop)
		self.initialize()
		self.reactor = reactor
		self.reactor_run = reactor_run
		self.reactor_stop = reactor_stop
		self.find_server_config_arg()

	def __str__(self):
		return	"WinSwitchClient"

	def initialize(self):
		self.sdebug()
		self.start_time = time.time()
		self.settings = None				# global settings
		self.servers = []					# the list of servers we know about
		self.mdns_add_count = 0				# The number of times mdns_add has been called
		self.mdns_listener = None			# mDNS listener (optional, recommended for LAN usage)
		self.local_detect_scheduled = False	# batch detection
		self.local_servers_watchers = {}	# list of watchers monitoring for changes to server lockfiles
		self.local_socket_listener = None	# listen for local commands using a socket or named pipe
		self.cleanup_called = False			# prevents calling cleanup twice
		self.client_utils = {}				# A subclass of ClientUtilBase for each type supported (nx,xpra,vnc,rdp,...)
		self.local_server = None			# the instance of the server if we start one
		self.server_config_filename_arg = None		# a server configuration filename passed as argument
		self.dialog_util = None
		self.notification_util = None

	def startup_progress(self, info, pct):
		self.sdebug(None, info, pct)

	def threaded_start(self):
		self.do_threaded_setup()

	def do_threaded_setup(self):
		self.startup_progress("loading settings", 20)
		self.load_settings()				# load settings from disk (or use defaults)
		self.startup_progress("detecting local configuration", 30)
		self.may_populate_xmodmap()			# used on *nix to tell server what keymap we want
		self.populate_xkeymap()				# used on *nix to tell server what keymap we want
		self.startup_progress("loading server configs", 70)
		self.open_server_config()			# load a server config if specified on the command line
		self.load_servers()					# load list of servers we know about from disk
		start_dump_threads()				# periodically dump the list of threads (if --dump-threads is set)

	def settings_assigned(self):
		""" called as soon as the settings variable is set - it may not be fully initialized though """
		pass

	def settings_ready(self):
		""" called when the settings are ready for use """
		self.settings.disable_flags_for_missing_commands()
		self.settings.detect_xpra_version()
		for x in [
					self.init_client_utils,				# client utils
					self.start_local_server,
					self.start_local_servers_watchers,	# watch for changes in local server lock directories
					self.start_mdns_listener,			# listen for mDNS multicast
					self.start_local_socket_listener,	# start a local command socket
					self.start_links,					# connect to enabled servers
					self.ready
				]:
			self.sdebug("calling %s" % x)
			callFromThread(x)

	def run(self):
		self.sdebug()
		self.register_signals()				# catch SIGINT / SIGTERM
		#self.threaded_start()
		thread.start_new(self.threaded_start, ())
		self.reactor_run()
		self.slog("ended")

	def ready(self):
		self.sdebug()

	def register_signals(self):
		self.do_register_signals(self.signal_exit)

	def do_register_signals(self, signal_callback):
		signal.signal(signal.SIGTERM, signal_callback)
		signal.signal(signal.SIGINT, signal_callback)
		register_sigusr_debugging()

	def signal_exit(self, signum, frame):
		""" Called when we catch a SIGTERM/SIGINT signal for the first time: try to exit cleanly """
		self.slog(None, signum, frame)
		def forced_exit(*args):
			self.slog(None, *args)
			sys.exit(1)
		self.do_register_signals(forced_exit)		#register the forced_exit handler: from now on we exit more forcefully
		callFromThread(self.exit)

	def exit(self, *args):
		self.slog(None, *args)
		try:
			self.cleanup()
		except Exception, e:
			self.serr(None, e, *args)

	def stop(self):
		self.slog()
		time_str = get_elapsed_time_string(self.start_time, suffix="")
		if not self.reactor_stop:
			self.slog("no reactor stop method defined!")
			return
		call_exit_hooks()
		dump_threads()
		self.slog("calling %s, was up for %s" % (self.reactor_stop, time_str))
		self.reactor_stop()
		self.reactor_stop = None
		self.slog("done")

	def cleanup(self):
		if self.cleanup_called:
			self.sdebug("cleanup already called! (skipping)")
			return
		self.cleanup_called = True
		try:
			self.do_cleanup()
		except Exception, e:
			self.exc(e)

	def do_cleanup(self):
		self.slog()
		self.stop_local_servers_watchers()
		self.do_detach_all_servers()			#on win32 we cannot exit until we disconnect which will free Xming
		self.close_virt_utils()					#free up resources (may close Xming if used)
		self.stop_all_links()
		""" The methods above may fire network packets,
		so give them a chance to run before closing all the sockets """
		callLater(0, self.stop_local_server)
		callLater(1, self.stop_mdns_listener)
		callLater(2, self.stop_local_socket_listener)
		callLater(3, self.stop_when_clean)
		self.sdebug("stop calls scheduled")

	def stop_when_clean(self, timeout=10):
		self.sdebug()
		if self.local_socket_listener is None or self.local_socket_listener.terminated or timeout<=0:
			callLater(0, self.stop)
			return
		self.sdebug("waiting for local socket listener=%s, terminated=" % (self.local_socket_listener, self.local_socket_listener.terminated), timeout)
		callLater(1, self.stop_when_clean, timeout-1)




	def load_settings(self):
		""" will call settings_assigned() once settings are mostly usable (..) """
		self.settings = load_settings()
		if self.settings:
			# apply log options
			add_argv_overrides(self.settings)
			set_loggers_debug(self.settings.debug_mode)
			# override command paths if we find some more valid ones (disabling commands should be done with the booleans - not by setting invalid paths):
			for var,newvalue in {"xpra_command" : XPRA_COMMAND, "ssh_command" : SSH_COMMAND, "nxproxy_command" : NXPROXY_COMMAND,
								"vnc_command" : VNC_COMMAND, "xming_command" : XMING_COMMAND, "rdesktop_command" : (RDESKTOP_COMMAND or XFREERDP_COMMAND),
								"xterm_command" : XTERM_COMMAND, "server_command" : WINSWITCH_SERVER_COMMAND}.items():
				curvalue = getattr(self.settings, var)
				if not is_valid_file(curvalue) and is_valid_file(newvalue):
					self.serror("%s set to invalid location %s, setting it to %s" % (var, curvalue, newvalue))
					setattr(self.settings, var, newvalue)
			self.settings.set_gst_attributes()
			self.settings_assigned()

		if not self.settings or not self.settings.uuid or self.settings.crypto_private_exponent==0 or not self.settings.get_key():
			self.startup_progress("creating new configuration", 40)
			self.create_new_settings()
			self.settings_assigned()
			self.notify("%s is starting up" % APPLICATION_NAME,
						"Creating a new system configuration,\nplease be patient as this may take a some time..\n(the application will not be accessible until this is completed)",
						notification_type=NOTIFY_INFO)
			self.startup_progress("detecting extra settings", 50)
			self.assign_settings_extras()
		else:
			self.slog("found existing settings, uuid=%s, key=%s, mount_points=%s" % (self.settings.uuid, self.settings.get_key(), csv_list(self.settings.mount_points)))
			self.settings_ready()

	def create_new_settings(self):
		# Generate default settings
		self.settings = get_settings(True)
		add_argv_overrides(self.settings)
		set_loggers_debug(self.settings.debug_mode)

	def assign_settings_extras(self):
		# Now the slow stuff:
		self.settings.assign_keys()
		from winswitch.fs.smb_parse import get_default_exports
		from winswitch.ui.icons import get_raw_icondata_for_platform
		self.settings.mount_points = get_default_exports()
		self.settings.set_avatar_icon_data(get_raw_icondata_for_platform())
		save_exports(self.settings)
		save_avatar(self.settings)
		save_settings(self.settings)
		def netspeed(*args):
			#must be called from main thread...
			self.settings.detect_lan_speed()
			self.settings_ready()
		callFromThread(netspeed)

	def init_client_utils(self):
		"""
		Ensures that the self.client_utils dict contains the client util instances
		for the types that are enabled in self.settings
		"""
		from winswitch.consts import XPRA_TYPE, NX_TYPE, VNC_TYPE, SSH_TYPE, SCREEN_TYPE, LIBVIRT_TYPE, WINDOWS_TYPE, OSX_TYPE, GSTVIDEO_TYPE, VIRTUALBOX_TYPE
		from winswitch.virt.xpra_client_util import XpraClientUtil
		from winswitch.virt.nx_client_util import NXClientUtil
		from winswitch.virt.vnc_client_util import VNCClientUtil
		from winswitch.virt.rdp_client_util import RDPClientUtil
		from winswitch.virt.ssh_client_util import SSHClientUtil
		from winswitch.virt.gstvideo_client_util import GSTVideoClientUtil
		from winswitch.virt.screen_client_util import ScreenClientUtil

		def vnc_common(*args):
			for x in [VNC_TYPE, OSX_TYPE, LIBVIRT_TYPE]:
				vnc = self.client_utils.get(x)
				if vnc:
					return	vnc
			return	VNCClientUtil(self.update_session_status, self.notify)
		def rdp_common(*args):
			for x in [WINDOWS_TYPE, VIRTUALBOX_TYPE]:
				vnc = self.client_utils.get(x)
				if vnc:
					return	vnc
			return	RDPClientUtil(self.update_session_status, self.notify)

		type_to_util = [(XPRA_TYPE,		self.settings.supports_xpra,	lambda : XpraClientUtil(self.update_session_status, self.notify)),
						(NX_TYPE,		self.settings.supports_nx,		lambda : NXClientUtil(self.update_session_status, self.notify)),
						(VNC_TYPE,		self.settings.supports_vnc,		vnc_common),
						(WINDOWS_TYPE,	self.settings.supports_vnc,		rdp_common),
						(OSX_TYPE,		self.settings.supports_vnc,		vnc_common),
						(LIBVIRT_TYPE,	self.settings.supports_vnc,		vnc_common),
						(VIRTUALBOX_TYPE,self.settings.supports_vnc,	rdp_common),
						(SSH_TYPE,		self.settings.supports_ssh,		lambda : SSHClientUtil(self.update_session_status, self.notify, self.ask)),
						(GSTVIDEO_TYPE,	self.settings.supports_gstvideo,lambda : GSTVideoClientUtil(self.update_session_status, self.notify)),
						(SCREEN_TYPE,	self.settings.supports_screen,	lambda : ScreenClientUtil(self.update_session_status, self.notify))
						]
		for (session_type, enabled, constructor) in type_to_util:
			if enabled:
				if session_type in self.client_utils:
					pass		#already exists
				else:
					self.client_utils[session_type] = constructor()
			else:
				if session_type in self.client_utils:
					del self.client_utils[session_type]
				else:
					pass		#not present anyway
		self.slog("client utils=%s" % self.client_utils)

	def close_virt_utils(self):
		done = []
		for virt in self.client_utils.values():
			if not virt in done:
				done.append(virt)
				virt.close()

	def get_remote_util(self, session_type):
		util = self.client_utils.get(session_type)
		if not util:
			self.serr("client util not found!", session_type)
		return	util

	def load_servers(self):
		"""
		Load all the server configs from disk for servers which are not dynamic (mDNS) and not local.
		(these will get loaded on demand when found)
		"""
		for server in load_server_configs():
			self.add_server(server)

	def add_server(self, server):
		self.sdebug(None, server)
		if server not in self.servers:
			self.servers.append(server)

	def remove_server(self, server):
		self.sdebug(None, server)
		self.servers.remove(server)
		if server.link:
			server.link.stop()
		delete_server_config(server)

	def populate_xkeymap(self):
		if WIN32 or OSX:
			return
		def getkeymap(setxkbmap_arg):
			cmd = ["setxkbmap", setxkbmap_arg]
			code, out, _ = get_output(cmd)
			if code==0:
				self.slog("=%s" % visible_command(out), setxkbmap_arg)
				return	out
			self.serror("failed with exit code %s\n" % code, setxkbmap_arg)
			return	""
		self.settings.xkbmap_print = getkeymap("-print")
		self.settings.xkbmap_query = getkeymap("-query")
		if WIN32:
			from winswitch.util.xkbmap_util import get_win32_layout
			self.settings.xkbmap_layout, self.settings.xkbmap_variant = get_win32_layout[:2]
		elif self.settings.xkbmap_query:
			#find layout from setxkbmap -query output:
			for x in self.settings.xkbmap_query.splitlines():
				if not x:
					continue
				if x.startswith("layout:"):
					self.settings.xkbmap_layout = x.split(":")[1].strip()
					self.slog("found layout=%s" % self.settings.xkbmap_layout)
					break

	def may_populate_xmodmap(self):
		if self.settings.custom_xmodmap_pm and self.settings.custom_xmodmap_pke:
			pm_file = os.path.expanduser(self.settings.custom_xmodmap_pm)
			pke_file = os.path.expanduser(self.settings.custom_xmodmap_pke)
			if is_valid_file(pm_file) and is_valid_file(pke_file):
				self.slog("using custom xmodmap files: %s and %s" % (pm_file, pke_file))
				self.xmodmap_keys = load_binary_file(pke_file)
				self.xmodmap_modifiers = load_binary_file(pm_file)
				return
		if not XMODMAP_COMMAND:
			return
		if OSX:
			""" don't start the xserver just to get the xmodmap on OSX """
			cmd = ["launchctl", "list"]
			try:
				code, out, _ = get_output(cmd)
			except Exception, e:
				self.serr("failed to run '%s'" % cmd, e)
				return
			self.sdebug("%s: %s" % (cmd, out))
			if code==0 and out:
				#parse output:
				started = False
				for line in out.splitlines():
					if line.endswith("org.x.startx"):
						self.slog("found launchctl status for startx: %s" % line)
						parts = line.split()
						started = parts[0].isdigit()
						break
				if not started:
					self.slog("X server is not started, not detecting xmodmap (which would cause it to start)")
					return
		def xmodmap_updated(*args):
			self.slog(None, *args)
			if self.settings.xmodmap_keys or self.settings.xmodmap_modifiers:
				for server in self.servers:
					try:
						if server.link and server.link.client:
							server.link.client.do_send_xmodmap()
					except Exception, e:
						self.serr(None, e)
		self.settings.on_populate_xmodmap.append(xmodmap_updated)
		self.settings.populate_xmodmap()

	def stop_local_socket_listener(self):
		self.sdebug()
		if self.local_socket_listener:
			try:
				self.local_socket_listener.stop()
			except Exception, e:
				self.serr("error trying to stop local listener: %s" % self.local_socket_listener, e)
		self.sdebug("done")

	def start_local_socket_listener(self):
		if "--no-local-socket" not in sys.argv:
			self.local_socket_listener = make_local_listener(self)
			self.local_socket_listener.start()

	def find_server_config_arg(self):
		OPEN_SERVER_CONFIG = "--open_server_config="
		for arg in sys.argv:
			if arg.startswith(OPEN_SERVER_CONFIG) and len(arg)>len(OPEN_SERVER_CONFIG):
				self.server_config_filename_arg = arg[len(OPEN_SERVER_CONFIG):]
				self.slog("found server config argument=%s" % self.server_config_filename_arg)

	def open_server_config(self):
		self.sdebug("server_config_filename_arg=%s" % self.server_config_filename_arg)
		if self.server_config_filename_arg:
			self.do_open_server_config(self.server_config_filename_arg)

	def do_open_server_config(self, filename):
		self.slog(None, filename)
		try:
			if not is_valid_file(filename):
				self.notify("File not found", "Cannot open the server configuration file '%s'" % filename,
						notification_type=NOTIFY_ERROR)
				return
			config = load_server_config_from_file(filename, try_plain_password=True)
			if config.ID:
				#find if we already have this server in our list
				#TODO: replace existing config with this one?
				for server in self.servers:
					if server.ID==config.ID:
						self.slog("server %s already known as '%s', enabling/connecting to it" % (server.ID, server.get_display_name()))
						server.enabled = True
						server.touch()
						self.add_server(server)			#doesn't add it, just ensures that we fire schedule_menu_refresh()
						if server.is_connected():
							self.notify("Already connected", "Already connected to server %s" % config.get_display_name(),
										notification_type=NOTIFY_INFO, from_server=server)
						else:
							self.kick_server_link(server)
						return
			#server not found, add it:
			self.add_server(config)
			self.kick_server_link(config)
			save_server_config(config, delete_old=False)
		except Exception, e:
			self.serr(None, e, filename)

	def contact_existing_client(self):
		"""
		If an existing applet is running, just wake it up with a simple message.
		Then close this instance as we don't want to have 2 copies running.
		"""
		if "--no-local-socket" in sys.argv:
			return	False
		client = make_local_client()
		self.sdebug("client=%s" % client)
		if self.server_config_filename_arg:
			cmd = format_message(COMMAND_OPEN_SERVER_CONFIG, [self.server_config_filename_arg])
		else:
			cmd = COMMAND_WAKEUP
		if client.test_socket(cmd):
			self.sdebug("successfully sent '%s' to existing instance" % cmd)
			return True
		return	False

	def start_local_server(self):
		"""
		Start a server instance.
		"""
		self.slog("start_local_server=%s, command=%s, current local_server=%s" % (self.settings.start_local_server, self.settings.server_command, self.local_server))
		if not self.settings.start_local_server:
			return
		if self.local_server is not None:
			self.slog("server already started: %s" % self.local_server)
			return
		try:
			if self.settings.embedded_server:
				self.start_local_server_embedded()
			else:
				self.start_local_server_daemon()
			self.slog("local_server=%s" % self.local_server)
		except Exception, e:
			self.serr(None, e)

	def start_local_server_daemon(self):
		#daemonize it:
		#but ensure we know the selinux status before we do:
		def do_start_local_server_daemon(selinux_enabled, selinux_enforcing, warning_bool, info):
			self.sdebug(selinux_enabled, selinux_enforcing, warning_bool, info)
			ENV_ALLOWED = ["HOME", "HOSTNAME", "LANG", "PATH", "PWD", "TERM", "PATH", "USER", "USERNAME"]
			if selinux_enabled and selinux_enforcing:
				#server will need access to this if SELinux is enforcing, see: http://winswitch.org/trac/ticket/107
				ENV_ALLOWED.append("XAUTHORITY")
			env = {}
			for k,v in os.environ.items():
				if k in ENV_ALLOWED:
					env[k] = v
			args = [self.settings.server_command, "--daemon", "--print-pid"]
			self.local_server = exec_daemonpid_wrapper(args, env)
		check_selinux(do_start_local_server_daemon)


	def connect_to_embedded_server(self):
		""" will try to connect directly using a fake connection """
		self.slog(None)
		try:
			server = load_server_config(self.local_server.config.ID)
			if server is None:
				server = make_server_config_from_settings(self.local_server.config)
			server.embedded_server = self.local_server
			server.local = True
			server.enabled = True
			server.line_speed = MAX_SPEED
			self.add_server(server)
			if not server.is_connected() and not server.is_connecting():
				self.kick_server(server, server.command_host, server.command_port)
		except Exception, e:
			self.serr("embedded client failed for local_server=%s" % self.local_server, e)

	def start_local_server_embedded(self):
		from winswitch.server.controller import WinSwitchServer
		def embedded_server_ready():
			self.slog()
			self.connect_to_embedded_server()
			self.schedule_detect_local_servers("embedded server is now ready")
		server = WinSwitchServer(True, embedded_server_ready)
		self.sdebug("server=%s" % server)
		if server.check():
			self.local_server = server
			server.start()

	def stop_local_server(self):
		self.slog("stopping %s" % self.local_server)
		if self.local_server is not None:
			try:
				self.local_server.stop()
			except Exception, e:
				self.serr(None, e)
			self.local_server = None

	def check_local_server(self, add, reason):
		"""
		This callback is triggered when a server is removed from mDNS or the portfile disappears.
		"""
		if add:
			self.schedule_detect_local_servers(reason)
		else:
			self.slog("should get removed automatically..", add)

	def schedule_detect_local_servers(self, reason=None):
		"""
		Schedule to run in 1 second, allows us to batch local server detection.
		"""
		if self.cleanup_called:
			return
		if self.local_detect_scheduled:
			self.sdebug("already scheduled", reason)
			return
		self.slog("scheduling", reason)
		self.local_detect_scheduled = True
		callLater(1, self.detect_local_servers, True)

	def detect_local_servers(self, kick=False, required_ID=None):
		"""
		Find local servers using local_server_portinfo
		and add a reference to them in the local config.
		"""
		self.local_detect_scheduled = False
		try:
			from winswitch.util.server_portinfo import get_local_servers_portinfo
		except Exception, e:
			self.serr("server component not installed locally: %s" % e, kick, required_ID)
			return	0
		portinfo = get_local_servers_portinfo(False)		#False: cant do connect test from within twisted's main loop...
		self.slog("found %s" % csv_list(portinfo), kick, required_ID)
		if not portinfo or len(portinfo)==0:
			return	0
		for (host, port, ID) in portinfo:
			if required_ID and ID!=required_ID:
				continue
			if host=="0.0.0.0":
				host = "127.0.0.1"
			self.sdebug("testing %s:%s / %s" % (host, port, ID), kick, required_ID)
			server = find_server_config(self.servers, ID)
			if not server:
				#not in active list, try on disk:
				server = load_server_config(ID)
				if server:
					self.add_server(server)
			if not server:
				for as_root in get_as_root_list():
					conf = get_local_server_config(as_root)
					if conf and conf.ID == ID:
						#remove private key (should never be needed/used on client)
						conf.crypto_private_exponent = 0l
						conf.key = None
						if conf.get_key():
							#This is a ServerSettings not a ServerConfig... copy common fields:
							server = make_server_config_from_settings(conf)
							break
			if server:
				# found a config (in memory or on disk)
				server.local = True
				server.command_host = host
				server.command_port = port
				if server not in self.servers:
					self.add_server(server)
				if kick and server.enabled and server.auto_connect and not server.is_connecting():
					self.kick_server(server, host, port)
				continue	#job done for this one


			# Create a new config
			self.slog("local server config not found for %s - creating one, target %s:%s" % (ID, host, port), kick)
			server = ServerConfig()
			server.line_speed = MAX_SPEED			#loopback is almost limitless..
			server.timeout = 5						#fast timeout for local servers
			server.ID = ID
			server.ssh_tunnel = False				#no need to tunnel
			server.name = "Local Server"
			server.local = True
			if port > 0:
				server.command_port = port
				server.default_command_port = 0
			if host:
				server.command_host = host
			self.add_server(server)
		return	len(portinfo)

	def stop_local_servers_watchers(self):
		self.sdebug("stopping: %s" % str(self.local_servers_watchers))
		for file_monitor in self.local_servers_watchers.values():
			try:
				file_monitor.cancel()
			except Exception, e:
				self.serr("error stopping %s" % file_monitor, e)

	def start_local_servers_watchers(self):
		"""
		Watch for a local server that may start after this client.
		It may not show in mDNS, for example if there is no network - or if mDNS is not enabled.
		"""
		if self.local_server is not None and self.settings.embedded_server and (OSX or WIN32):
			self.sdebug("no need to monitor for server file changes since the server is embedded")
			return

		if not CAN_USE_GIO:
			self.slog("gio disabled, will use polling fallback...")
			callLater(20, self.detect_local_servers, True)
			callLater(60, self.detect_local_servers, True)
			return
		self.sdebug()
		import gio
		dirs = [get_lock_dir()]
		files = [get_port_filename()]
		#watch for run-as-root server too:
		if USER_ID!=0:
			dirs.append(get_lock_dir(True))
			files.append(get_port_filename(True))
		for d in dirs:
			if is_valid_dir(d):
				gfile = gio.File(d)
				monitor = gfile.monitor_directory()
				monitor.connect("changed", self.server_dir_changed)
				self.local_servers_watchers[d] = monitor
		for f in files:
			if is_valid_file(f):
				gfile = gio.File(f)
				monitor = gfile.monitor_file()
				monitor.connect("changed", self.server_portfile_changed)
				self.local_servers_watchers[f] = monitor

	def server_portfile_changed(self, monitor, file1, file2, evt_type):
		self.sdebug("evt_type=%s" % type(evt_type), monitor, file1, file2, evt_type)
		self.check_local_server(True, "server port file modified")

	def server_dir_changed(self, monitor, d, ignored, evt_type):
		self.sdebug("evt_type=%s" % type(evt_type), monitor, d, ignored, evt_type)
		self.check_local_server(True, "server dir modified")

	def stop_mdns_listener(self):
		self.sdebug()
		if self.mdns_listener:
			try:
				self.mdns_listener.stop()
			except Exception, e:
				self.serr("error trying to stop mDNS listener", e)
		self.sdebug("done")

	def start_mdns_listener(self):
		self.sdebug("enabled=%s" % self.settings.mdns_listener)
		if not self.settings.mdns_listener:
			return
		try:
			if not WIN32 and not OSX:
				try:
					from winswitch.net.avahi_listener import AvahiListener
					self.mdns_listener = AvahiListener(MDNS_TYPE, None, self.mdns_add, self.mdns_remove)
				except ImportError, e:
					self.serror("failed to load avahi listener: %s" % e)
			if self.mdns_listener is None:
				try:
					from winswitch.net.twisted_bonjour_listener import TwistedBonjourListeners
					self.mdns_listener = TwistedBonjourListeners(self.reactor, [""], MDNS_TYPE, None, self.mdns_add, self.mdns_remove)
				except ImportError, e:
					self.serror("failed to load pybonjour listener: %s" % e)

			def check_mdns_data_received():
				self.sdebug("mdns_add_count=%s" % self.mdns_add_count)
				config = get_local_server_config(False, False, show_warnings=False, load_ssh_key=False)
				if not config:
					self.serror("no server config found!")
					return
				if not config.mDNS_publish:
					self.slog("server config does not publish mDNS records... so we cannot assume a record should have been received")
					return
				if self.mdns_add_count>0:
					self.slog("mdns seems to be working ok, we have already received %s records" % self.mdns_add_count)
					return
				#A record should have been seen!
				#Ensure there are valid network interfaces up:
				if has_netifaces:
					ips = get_bind_IPs()
					non_local_ips = [x for x in ips if (x!=LOCALHOST and not x.startswith("169.254."))]
					if len(non_local_ips)==0:
						self.slog("this host does not seem to be connected to any non-local networks - ignoring lack of mDNS records")
						return
					self.slog("this host is connected to at least one non-local IP: %s, mDNS records should have been received")
				def show_mdns_info(*args):
					self.open_url(MDNS_INFO_URL)
				self.notify("mDNS misconfiguration", "mDNS records from the local server should have been received."
														"\nYour firewall or your operating system may be blocking access to the mDNS port (UDP port 5353)"
														"\nThis prevents automatic host detection.", callback=("Show mDNS Help", show_mdns_info),
														notification_type=NOTIFY_ERROR)

			if self.mdns_listener:
				self.mdns_listener.start()
				#figure out if we expect to receive at least one multicast entry from our local serve:
				if self.settings.start_local_server:
					""" run the checks in 30s, giving enough time for the server to startup,
						create its id and start publishing its mDNS record: """
					callLater(30, check_mdns_data_received)
		except Exception, e:
			self.serr(None, e)

	def mdns_remove(self, ID):
		self.slog(None, ID)
		for server in self.servers:
			if ID == server.ID:
				if server.local:
					self.slog("not removing %s as it is marked local and should still be accessible.." % server, ID)
				else:
					self.servers.remove(server)

	def mdns_add(self, iface_index, service_name, domain, host, address, port, text=None):
		self.mdns_add_count += 1
		try:
			self.do_mdns_add(iface_index, service_name, domain, host, address, port, text)
		except Exception, e:
			self.serr(None, e, iface_index, service_name, domain, host, address, port, alphanumfile(text))

	def do_mdns_add(self, iface_index, service_name, domain, host, address, port, text):
		sig = msig(iface_index, service_name,domain,host,address,port,alphanumfile(text))
		iface = ""
		if if_indextoname:
			iface = if_indextoname(iface_index)
		ID = None
		if not text:
			self.error(sig+ " invalid mdns data: missing text record!")
			return
		if "ID" in text:
			ID = text["ID"]
			server = self.find_server_by_ID(ID)
			if server and server.is_connected():
				self.debug(sig+ " already connected to %s" % server.get_display_name())
				return
			self.log(sig+ " ID=%s, type(text)=%s, len(text)=%d" % (ID, type(text), len(text)))
		if not ID:
			self.error(sig+ " invalid mdns data: missing ID!")
			return
		ID = "%s" % ID				#ensure it is a string

		# Resolve the hostname if needed:
		resolve = not address or len(address)==0 or address.find(":")>=0
		if not resolve:
			#or if address is not an IP (only digits separated by dots)
			for part in address.split("."):
				if not part.isdigit():
					resolve = True
					break
		if resolve:
			import socket
			try:
				address = socket.gethostbyname(host)
			except Exception, e:
				self.error(sig+" failed to resolve %s: %s" % (host, e))
				address = host
			self.debug(sig+" resolved %s to %s" % (host,address))
		address = "%s" % address		#ensure it is a string
		host = "%s" % host				#ensure it is a string


		# Try to find it in the list of known servers:
		server = self.find_server_by_ID(ID)
		if not server:
			# Try from disk:
			server = load_server_config(ID)
			if server:
				self.add_server(server)		# add it to live list

		if server:
			# Found an existing config, update it with changed host:port if needed:
			port = int(port)
			if server.local:
				kick = False
				address = server.host
				port = server.port
			else:
				kick = server.host!=address or server.command_port!=port
				server.host = address
				server.default_command_port = 0
				server.command_host = address
				server.command_port = port
				server.dynamic = True
			if self.settings.auto_connect and server.enabled and (kick or (server.auto_connect and not (server.is_connected() or server.is_connecting()))):
				self.kick_server(server, address, port)
			return

		username = ""
		if "username" in text:
			username = text["username"]
		if username and self.settings.mdns_match_username:
			match = False
			for test in self.settings.mdns_match_username_string.replace(",", " ").split():
				ure = test.replace("*", ".*")
				if re.match(ure, username):
					self.slog(sig+" server username '%s' matched regular expression '%s'" % (username, ure))
					match = True
					break;
			if not match:
				self.slog(sig+" server ignored as it does not match mdns_match_username any of the usernames in: %s" % self.settings.mdns_match_username)
				return

		# not found: add it!
		self.log(sig+" server ID %s not seen before" % ID)
		trim = [domain, ".", "local", "localhost", "localdomain"]		#list of strings we remove from the name
		trim.append(".")
		name = "%s" % host
		for _ in [0,1]:
			for t in trim:
				if t and name.endswith(t):
					name = name [:len(name)-len(t)]
		if not name:
			name = "%s" % address

		if iface=="lo" or is_localhost(host) or is_localhost(address):
			self.log(sig+" this hostname seems to match the local machine, testing local detection")
			self.detect_local_servers(False, ID)
			server = self.find_server_by_ID(ID)
			if server and (not server.enabled or not server.auto_connect):
				return
		if not server:
			server = ServerConfig()
			server.is_new = True
			server.ID = ID
			server.timeout = 8				#medium timeout for lan servers found via mDNS
			server.dynamic = True
			server.name = name
			server.line_speed = get_interface_speed(iface_index, iface, self.settings.default_lan_speed)
			server.username = username
			server.host = address
			server.ssh_tunnel = "ssh_tunnel" in text and get_bool(text["ssh_tunnel"])
			server.port = 22
			if "ssh_port" in text:
				server.port = get_int(text["ssh_port"], 22)
			self.debug(sig+ " name=%s, guessed speed(%s)=%s, ID=%s, ssh_tunnel=%s, ssh_port=%s" % (server.name, iface, server.line_speed, ID, server.ssh_tunnel, server.port))
			#next 2 checks could be because of tunnels:
			server.default_command_port = 0		#means automatic
			server.command_port = int(port)
			server.command_host = address
			self.add_server(server)
		self.debug(sig+ " server located at: %s:%s" % (server.command_host, server.command_port))
		if self.settings.mdns_auto_connect and self.settings.auto_connect:
			self.start_link(server)


	def do_disconnect_server(self, server):
		link = server.link
		self.sdebug("link=%s" % link, server)
		if link:
			link.stop()

	def do_retry_server(self, server):
		self.slog(None, server)
		self.kick_server_link(server)

	def do_resume_all(self, server):
		self.slog(None, server)
		for session in server.get_live_sessions():
			self.do_resume_session(server, session)

	def do_resume_session(self, server, session):
		display_util = self.get_remote_util(session.session_type)
		if server.link and display_util:
			display_util.client_resume_session(server, session)
			return	True
		return	False

	def attach_to_session(self, server, session, host, port):
		"""
		This will be called by ServerLink. Don't call directly!
		"""
		self.sdebug("session.password=%s" % session.password, server, session, host, port)
		if session.password:
			display_util = self.get_remote_util(session.session_type)
			self.sdebug("calling attach on %s" % display_util, server, session, host, port)
			try:
				display_util.attach(server, session, host, port)
			except Exception, e:
				self.serr(None, e, server, session, host, port)

	def do_toggle_remote_clone(self, server, session):
		self.do_toggle_sound(server, session, True, True)
	def do_toggle_local_clone(self, server, session):
		self.do_toggle_sound(server, session, False, True)
	def do_toggle_soundin(self, server, session):
		self.do_toggle_sound(server, session, True, False)
	def do_toggle_soundout(self, server, session):
		self.do_toggle_sound(server, session, False, False)

	def do_toggle_sound(self, server, session, in_or_out, monitor):
		live = session.is_sound_live(in_or_out)
		self.sdebug("live=%s" % live, server, session, in_or_out, monitor)
		if live:
			session.stop_sound(in_or_out)
		client = self.get_link_client(server)
		if client:
			""" Ask the server to start/stop the sound pipe
			(opposite state to what it is now: not live)
			(opposite end is reversed: not in_or_out) """
			client.request_sound(session, not live, not in_or_out, monitor)



	def connected_here(self, session):
		return session.is_connected_to(self.settings.uuid)

	def do_idle_connected_sessions(self):
		self.slog("servers=%s" % str(self.servers))
		self.attention("Marking connected sessions as idle")
		for server in self.servers:
			for session in server.sessions.values():
				if self.connected_here(session):
					self.do_idle_session(server, session)

	def do_idle_session(self, server, session):
		self.sdebug(None, server, session)
		client = self.get_link_client(server)
		if client and self.connected_here(session) and session.status == Session.STATUS_CONNECTED:
			session.set_status(Session.STATUS_IDLE)
			client.set_session_status(session)
			return	True
		return	False

	def do_resume_idle_sessions(self):
		self.slog("servers=%s" % csv_list(self.servers))
		self.attention("Resuming any idle sessions")
		for server in self.servers:
			for session in server.sessions.values():
				self.do_resume_idle_session(server, session)
	def do_resume_idle_session(self, server, session):
		if session.status==Session.STATUS_IDLE:
			client = self.get_link_client(server)
			if client:
				if self.connected_here(session):
					session.set_status(Session.STATUS_CONNECTED)
					client.set_session_status(session)
				else:
					self.do_resume_session(server, session)
				return	True
		return	False

	def do_close_all_sessions(self, server):
		self.slog(None, server)
		for session in server.get_live_sessions(allow_preload=True):
			self.do_close_session(server, session)

	def do_close_session(self, server, session):
		self.slog(None, server, session)
		if self.connected_here(session):
			client = self.get_link_client(server)
			remote_util = self.get_remote_util(session.session_type)
			if remote_util:
				remote_util.client_close_session(server, client, session)
				return	True
		return	False

	def do_raise_session(self, server, session):
		self.slog(None, server, session)
		if self.connected_here(session):
			remote_util = self.get_remote_util(session.session_type)
			if remote_util:
				remote_util.client_raise_session(server, session)

	def do_kill_session(self, server, session):
		if not self.connected_here(session) and session.owner!=self.settings.uuid:
			self.serror("session not connected here and we are not the owner, cannot kill it!", server, session)
			return	False
		self.slog(None, server, session)
		client = self.get_link_client(server)
		remote_util = self.get_remote_util(session.session_type)
		remote_util.client_kill_session(server, client, session)
		return	True

	def do_reconnect_session(self, server, session):
		self.slog(None, server, session)
		if self.do_detach_session(server, session):
			return	self.do_resume_session(server, session)

	def do_detach_all_servers(self):
		self.slog("servers=%s" % csv_list(self.servers))
		for server in self.servers:
			try:
				self.do_detach_all_sessions(server)
			except Exception, e:
				self.exc(e)

	def do_detach_all_sessions(self, server):
		live = server.get_live_sessions(allow_preload=True)
		self.slog("live sessions=%s" % csv_list(live), server)
		for session in live:
			try:
				self.do_detach_session(server, session)
			except Exception, e:
				self.exc(e)

	def do_detach_session(self, server, session):
		self.slog(None, server, session)
		if self.connected_here(session):
			client = self.get_link_client(server)
			remote_util = self.get_remote_util(session.session_type)
			if remote_util:
				remote_util.client_detach_session(server, client, session)
				return	True
			for in_or_out in [True,False]:
				for monitor_flag in [True,False]:
					if session.is_sound_live(in_or_out, monitor_flag):
						session.stop_sound(in_or_out, monitor_flag)
		return	False


	def bookmark_command(self, server, command, session_type, screen_size, opts=None, bookmark=False):
		if session_type:
			bookmark = "%s:" % session_type				#ie: nx:
		else:
			bookmark = ":"
		if opts:
			new_opts = opts.copy()
		else:
			new_opts = {}
		if screen_size:
			new_opts["screen_size"] = screen_size
		if len(new_opts):
			opts_str = ";".join(["%s=%s" % (k,v) for k,v in new_opts.items()])
			bookmark += "%s:" % opts_str				#ie: nx:screen_size=1024x768;depth=8;compression=20:
		else:
			bookmark += ":"
		bookmark += command.command						#ie: nx:screen_size=1024x768;depth=8;compression=20:gnome-terminal
		server.local_shortcuts.append(bookmark)
		server.touch()
		modify_server_config(server, ["local_shortcuts"])
		self.slog("added bookmark: %s" % bookmark, server, command, session_type, screen_size, opts, bookmark)


	def default_start_session(self, server, command):
		return self.do_start_session(server, command, server.preferred_session_type, "")


	def	do_start_session(self, server, command, session_type, screen_size, opts=None, bookmark=False):
		self.slog(None, server, command, session_type, screen_size, opts, bookmark)
		client = self.get_link_client(server)
		if client is None:
			self.notify("Cannot start command %s" % command.name, "Server %s is not started or not connected" % server.name,
					notification_type=NOTIFY_ERROR, from_server=server)
			return	False
		display_util = self.get_remote_util(session_type)
		if not display_util:
			def show_settings(*args):
				self.sdebug(None, *args)
				self.show_settings()
			self.notify("Cannot start command %s" % command.name, "Session type %s is not enabled" % session_type,
					callback=("Show Configuration", show_settings), notification_type=NOTIFY_ERROR)
			return	False
		self.attention("Starting %s" % command.name, None)
		try:
			display_util.client_start_session(server, client, command, screen_size, opts)
		except Exception, e:
			self.serr("failed to start with %s" % display_util, e, server, command, session_type, screen_size, opts, bookmark)
			return False
		if bookmark and command.command:
			self.bookmark_command(server, command, session_type, screen_size, opts, bookmark)
		return	True

	def	do_shadow_session(self, server, session, read_only, shadow_type, screen_size, options):
		self.slog(None, server, session, read_only, shadow_type, screen_size, options)
		client = self.get_link_client(server)
		if client:
			client.shadow_session(session.ID, read_only, shadow_type, screen_size, options)

	def do_send_session(self, server, session, user_uuid):
		"""
		Send this session to another user
		"""
		self.slog(None, server, session, user_uuid)
		client = self.get_link_client(server)
		if client:
			if self.do_detach_session(server, session):
				client.send_session_to_user(session, user_uuid)

	def do_send_all_sessions(self, server, unused, user_uuid):
		"""
		Send all the sessions we have on this server to another user
		"""
		self.slog(None, server, unused, user_uuid)
		for session in server.get_live_sessions():
			self.do_send_session(server, session, user_uuid)


	def update_session_status(self, server, session, status, screen_size=None):
		"""
		Used by display_util to tell us about status or screen_size changes detected by the client util class.
		For example by rdp_client_util when windows re-connects the session (session events).
		"""
		self.slog(None, server, session, status)
		if screen_size:
			session.screen_size = screen_size
		if status:
			session.set_status(status)					#we just trust the remote_util instance is not doing anything crazy
			if session_status_has_actor(status):
				session.actor = self.settings.uuid		#if this status requires an actor, it must be us (otherwise we wouldn't know)
		elif screen_size:
			session.touch()		#ensure we touch() if only screen_size was changed
		client = self.get_link_client(server)
		if client:
			client.set_session_status(session)



	def get_default_server(self):
		# Servers we can connect to
		for server in self.servers:
			if server.ID == self.settings.default_server:
				return	server
		return	None

	def open_on_server(self, server_id, files):
		self.slog(None, server_id, files)
		server = None
		def open_err(title, message):
			self.notify(title, message, 10, None, notification_type=NOTIFY_ERROR, from_server=server)

		if server_id:
			server = self.find_server_by_ID(server_id)
			if not server:
				open_err("Cannot find server specified",
						"The %s file(s) given could not be opened.\n"
						"The missing server ID is %s" % (len(files), server_id))
				return
		else:
			server = self.get_default_server()
			if not server:
				open_err("No default server defined",
						"The %s file(s) given could not be opened.\n"
						"You must set the default server in the preferences." % len(files))
				pass
			if not server.is_connected():
				open_err("Not connected to default Server %s" % server.name,
						"The %s file(s) given could not be opened.\n"
						"Ensure that a connection is established with the default server,\n"
						"Or change the default server to one that is active." % len(files))
				pass
		self.do_open_local_files(server, files)

	def do_open_local_files(self, server, files):
		self.sdebug(None, server, files)
		if not server:
			self.serr("server is not set!", None, server, files)
			return
		if server.local and not FILE_TRANSFER_TO_LOCAL:
			self.slog("server %s is local, not copying the files" % server, server, files)
			for f in files:
				self.open_remote_file(server, f)
		else:
			thread.start_new_thread(self.do_copy_local_files, (server, files))

	def do_copy_local_files(self, server, filenames):
		if not server.allow_file_transfers:
			self.serror("file transfers not allowed by this server!", server, filenames)
			return

		self.slog(None, server, filenames)
		from winswitch.ui.progress_bar import ProgressBarWindow
		from winswitch.ui import icons
		mappings = {}
		for filename in filenames:
			basename = os.path.basename(filename)
			mappings[filename] = os.path.join(server.download_directory, basename)
			self.slog("remote(%s)=%s" % (filename, mappings[filename]), server, filenames)
		if len(filenames)==1:
			descr = filenames[0]
		else:
			descr = "%d files" % len(filenames)
		progress = ProgressBarWindow("Copying %s to %s" % (descr, server.name), window_icon=icons.get("winswitch"))
		#copier.start()
		client = self.get_link_client(server)
		fcs = client.send_files(filenames, progress.set_progress, progress.close)
		def cancel_copy():
			self.slog("file copy in progress: %s" % str(fcs))
			for fc in fcs:
				fc.cancel()
			progress.close()
		progress.cancel_callback = cancel_copy

	def open_remote_file(self, server, filename):
		"""
		Open the remote file on the server using whatever it thinks is appropriate (see mime_open.py)
		"""
		self.slog(None, server, filename)
		client = self.get_link_client(server, "Cannot open file")
		if client:
			self.sdebug("sending using client=%s" % client, server, filename)
			mode="view"
			client.open_file(filename, mode)

	def find_server_by_ID(self, ID):
		for server in self.servers:
			if ID == server.ID:
				return	server
		return	None

	def kick_server(self, server, host, port):
		server.command_port = int(port)
		server.command_host = host
		if not server.enabled:
			self.slog("server is disabled", server, host, port)
			return	False
		return self.kick_server_link(server)

	def kick_server_link(self, server):
		if server.is_connected():
			self.slog("already connected", server)
			return True
		if server.link:
			callLater(0, server.link.kick)
			return True
		else:
			self.slog("no link found, creating one", server)
			callLater(0, self.start_link, server)
		return False

	def require_link(self, server, message=""):
		"""
		Returns the link object associated with this server, if it does not exist
		an error will be logged and the user will be notified with the message passed in as title.
		"""
		assert server
		link = server.link
		if not link or FAKE_LINK_ERROR:
			self.notify("Not connected to %s" % server.get_display_name(), message, notification_type=NOTIFY_ERROR, from_server=server)
			raise Exception("link not found for server ID %s" % server.ID)
		return link

	def get_link_client(self, server, message=None, connection_status=[ServerConfig.STATUS_CONNECTED]):
		""" Returns a ServerLineConnection instance if we are connected to the server, None otherwise """
		link = server.link
		if link is None:
			self.slog("no link", server, message)
			return	None
		if link.client is None:
			self.slog("no link client", server, message)
			return	None
		if server.status not in connection_status:
			self.slog("server status is %s, not %s" % (server.status, connection_status))
			return	None
		return link.client

	def start_links(self):
		self.debug("auto_connect=%s" % self.settings.auto_connect)
		if not self.settings.auto_connect:
			return
		for server in self.servers:
			#dont start links to local servers here, should happen when we detect them
			if not server.local and server.enabled and server.auto_connect:
				self.start_link(server)

	def start_link(self, server):
		assert server
		if server.link:
			self.slog("link already exists for '%s', kicking it" % server.name, server)
			server.link.kick()
		else:
			self.slog("creating new link", server)
			self.create_link(server)

	def create_link(self, server, callback=None):
		server.link = ServerLink(server,
							self.notify, self.dialog_util,
							lambda sess : self.do_detach_session(server, sess),
							lambda sess,host,port: self.attach_to_session(server, sess, host, port))
		server.link.connect()
		if callback:
			callback()

	def stop_all_links(self):
		for server in self.servers:
			try:
				self.stop_server_link(server, False)
			except Exception, e:
				self.exc(e)

	def stop_server_link(self, server, warn=True):
		link = server.link
		if link:
			link.stop()
		elif warn and server.enabled:
			self.serr("warning: link not found for '%s'" % server.name, None, server)
