#!/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 re
import shutil
import sys
import thread
import threading
import time

from winswitch.consts import LOCALHOST, X_PORT_BASE, IPP_PORT_BASE, APPLICATION_NAME
from winswitch.globals import USER_ID, USERNAME, WIN32, OSX
from winswitch.net.net_util import get_port_mapper, wait_for_socket, get_iface_ipmasks
from winswitch.objects.server_command import ServerCommand
from winswitch.objects.session import Session
from winswitch.objects.server_session import ServerSession
from winswitch.util.common import CAN_USE_GIO, DISABLE_GIO_ARG, parse_screensize, \
	is_valid_file, is_valid_path, is_valid_dir, is_valid_exe, \
	csv_list, delete_if_exists, visible_command, save_binary_file, generate_UUID
from winswitch.util.config import save_session
from winswitch.util.file_io import get_session_snapshot_filename, get_session_filename, \
	get_protocols_dir, load_properties, save_properties, populate_object_from_properties, \
	get_session_wrapper_log_filename, get_session_pulseaudio_log_filename, get_session_xauth_filename, get_server_session_dir, \
	get_session_cupsd_config_filename, get_session_cupsd_socket_filename, get_session_cupsd_temp_dir, get_session_cupsd_log_filename, \
	mksubdir, RWX
from winswitch.util.firewall_util import get_firewall_util
from winswitch.util.icon_cache import guess_icon_from_name
from winswitch.util.process_util import exec_nopipe, kill0, send_kill_signal, get_output
from winswitch.util.simple_logger import Logger, msig
from winswitch.virt.options_common import LOCALE
from winswitch.virt.virt_server_daemonizer import start_daemon
from winswitch.virt.display_util import DisplayUtil
from winswitch.util.xmodmap_util import set_xmodmap
from winswitch.util.xkbmap_util import set_keymap
from winswitch.util.commands_util import XDG_OPEN_COMMAND, XSCREENSAVER_COMMAND_COMMAND
from winswitch.util.paths import WINSWITCH_LIBEXEC_DIR, GNOME_SETTINGS_DAEMON
from winswitch.util.main_loop import callLater, callFromThread


COMMAND_FIELD_CODES = re.compile("%[fFuUdDnNickvm]")

class	ServerUtilBase(DisplayUtil):
	"""
	Utility superclass for the server-side of the remote display utility classes.
	Contains common code/behaviour.
	"""
	def	__init__(self, session_type, port_base, config, add_session, remove_session, update_session_status, session_failed):
		Logger(self, log_colour=Logger.CYAN)
		self.session_type = session_type
		self.port_base  = port_base
		self.config = config
		self.add_session = add_session
		self.remove_session = remove_session
		self.update_session_status = update_session_status
		self.session_failed = session_failed

		self.lock = threading.Lock()

		self.bind_to_all_interfaces = True
		self.dbus_enabled = not WIN32 and not OSX
		self.disable_gnome_screensaver = self.dbus_enabled
		self.disable_xscreensaver = True
		self.gnome_settings_daemon_enabled = not WIN32 and not OSX and is_valid_file(GNOME_SETTINGS_DAEMON)
		self.orbit_socket_dir_enabled = not WIN32 and not OSX
		self.use_global_xauth = True

		self.ignored_options_for_compare = [LOCALE]		# list of options which may differ and the pre-loaded session will still be accepted
		self.test_using_sockets = False
		self.session_start_timeout = 30
		self.prelaunch_enabled = self.config.prelaunch_enabled
		self.max_prelaunch_failures = 4
		self.prelaunch_failures = []
		self.shadow_start_timeout = 10				#how long to wait for shadow sessions to start
		self.connecting_timeout = 0					#dont timeout automatically, if set it will check after the delay to prevent session stuck in connecting state
		self.port_mapper = get_port_mapper()
		self.firewall_util = get_firewall_util(self.config)
		self.local_users = []						#list of xpra users (which we may preload sessions for)
		self.watched_directories = []
		self.batch_detect_sessions_delay = 0.1		#how long do we batch for
		self.detect_pending = False
		self.filechange_monitors = {}
		self.init_done = False
		self.debug_log_read = False
		self.selinux_enforcing = False
		self.enabled = ("--no-%s" % self.session_type) not in sys.argv

	def get_config_options_base(self, detect=False, log=False, start=False, add_desktop_session_options=False):
		options = ["# support for %s sessions:" % self.session_type, "enabled",
				"# the TCP port number offset we try to allocate from", "port_base"]
		if detect:
			options.append("# when detecting new sessions, use this delay to batch detection")
			options.append("batch_detect_sessions_delay")
		if log:
			options.append("# log all session output (debugging)")
			options.append("debug_log_read")
		if start:
			options.append("# how long to wait before assuming the session has timed out")
			options.append("session_start_timeout")
		if add_desktop_session_options:
			options += ["# bind to all interfaces or just loopback (which requires tunnels)",
					"bind_to_all_interfaces",
					"# start a dbus instance for each session",
					"dbus_enabled",
					"# disable screensavers after starting sessions",
					"disable_gnome_screensaver",
					"disable_xscreensaver",
					"# start (and more importantly stop) the gnome settings daemon ourselves",
					"gnome_settings_daemon_enabled",
					"# define a new ORBIT_SOCKETDIR for each session",
					"orbit_socket_dir_enabled",
					"# use the global xauth file for all sessions",
					"use_global_xauth"]
		return options

	def get_config_options(self):
		options = self.get_config_options_base(True, True, True, True)
		# "prelaunch_enabled", "max_prelaunch_failures", "session_start_timeout"
		return	options

	def load_config(self):
		filename = os.path.join(get_protocols_dir(), "%s.conf" % self.session_type)
		options = self.get_config_options()
		if is_valid_file(filename):
			props = load_properties(filename)
			populate_object_from_properties(props, self, type(self), keys_in=options, warn_on_missing_keys=True, warn_on_extra_keys=True)
		else:
			#create the initial config
			props = {}
			for option in options:
				if option.startswith("#"):
					props[option] = ""
				else:
					props[option] = getattr(self, option)
			save_properties(filename, props, header="# %s settings\n" % self.session_type, key_order=options)
		self.slog("=%s" % props)

	def init(self):
		if self.init_done:
			return
		self.do_init()
		self.init_done = True

	def do_init(self):
		pass

	def get_local_client_ID(self):
		# get the ID from the local client config file
		try:
			from winswitch.util.config import load_uuid_from_settings
			uuid = load_uuid_from_settings()
			if uuid:
				return	uuid
		except Exception, e:
			self.serr(None, e)
		return ""

	def save_session(self, session):
		save_session(session)

	def detect_existing_sessions(self, reloaded_sessions):
		"""
		Probe the list of existing sessions and try to find if the server process is still running.
		"""
		if os.name!="posix":
			self.slog("non-posix OS, ignoring existing session files", reloaded_sessions)
			return
		if not is_valid_dir("/proc"):
			self.serror("cannot access /proc! ignoring existing session files", reloaded_sessions)
			return
		for session in reloaded_sessions:
			if self.detect_existing_session(session):
				self.found_live_session(session)
			else:
				self.session_cleanup(session)

	def detect_existing_session(self, session):
		def may_warn(err_msg):
			if session.status!=Session.STATUS_CLOSED:
				self.serror(err_msg, session)
			return	False
		if not session.server_process_pid or session.server_process_pid<=0:
			return may_warn("session does not have a server_process_pid, assuming it is dead and deleting it")
		path = os.path.join("/proc", str(session.server_process_pid))
		if not is_valid_dir(path):
			return may_warn("cannot find server process directory %s" % path)
		fd_dir = os.path.join(path, "fd")
		if not is_valid_dir(fd_dir):
			return may_warn("cannot find server process file descriptor directory %s" % fd_dir)
		session_logfile = self.get_log_file(session)
		try:
			files = os.listdir(fd_dir)			#xpra opens the file itself, so it won't be fd1 or fd2
		except Exception, e:
			return may_warn("cannot access file descriptor directory %s, probably not our process: %s" % (fd_dir, e))
		files.insert(0, "2")				#try stderr first
		files.insert(1, "1")				#try stdout next
		for filename in files:
			test_file = os.path.join(fd_dir, filename)
			if is_valid_file(test_file) and os.path.islink(test_file):
				real_path = os.path.realpath(test_file)
				if real_path==session_logfile:
					ok = kill0(session.server_process_pid)
					self.slog("found server process %s which has log file %s opened, testing with 'kill -0': %s" % (session.server_process_pid, session_logfile, ok))
					return ok
		return	False

	def found_live_session(self, session):
		assert session
		self.sdebug(None, session)
		self.config.add_session(session)
		self.add_session(session)
		self.watch_existing_log(session)

	def zap_existing_sessions(self, reloaded_sessions):
		"""
		Deletes all previous sessions.
		"""
		self.slog(None, reloaded_sessions)
		for session in reloaded_sessions:
			self.session_cleanup(session)

	def setup_directory_watcher(self):
		self.sdebug("watching %s" % csv_list(self.watched_directories))
		if not self.watched_directories:
			return
		if not CAN_USE_GIO:
			self.serror("cannot watch directories without gio!")
			return
		import gio
		for d in self.watched_directories:
			if is_valid_dir(d):
				gfile = gio.File(d)
				monitor = gfile.monitor_directory()
				monitor.connect("changed", self.watched_directory_changed)
				self.filechange_monitors[d] = monitor

	def watched_directory_changed(self, monitor, d, ignored, evt_type):
		import gio
		if evt_type in [gio.FILE_MONITOR_EVENT_CHANGED, gio.FILE_MONITOR_EVENT_CHANGES_DONE_HINT, gio.FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED]:
			return		#ignore those (dont even log them)
		self.sdebug(None, monitor, d, ignored, evt_type)
		if (evt_type in (gio.FILE_MONITOR_EVENT_CREATED, gio.FILE_MONITOR_EVENT_DELETED)):
			self.schedule_detect_sessions()

	def get_session_dir(self, session):
		return	get_server_session_dir(session.display, session.user, USER_ID==0)

	def get_log_file(self, session):
		return	self.do_get_log_file(session, self.session_type)

	def do_get_log_file(self, session, name):
		logfile = os.path.join(self.get_session_dir(session), "%s.log" % name)
		return	logfile

	def clear_existing_logfile(self, session):
		logfile = self.get_log_file(session)
		if is_valid_file(logfile):
			old = logfile+".old"
			delete_if_exists(old)
			os.rename(logfile, old)

	def watch_existing_log(self, session):
		if session.log_watcher:
			self.sdebug("log_watcher already present: %s" % session.log_watcher, session)
			return
		logfilename = self.get_log_file(session)
		if not is_valid_file(logfilename):
			self.serror("cannot find logfile: %s" % logfilename, session)
			return
		logfile = os.open(logfilename, os.O_RDONLY)
		try:
			session.reloading = True
			self.do_read_from_log(logfilename, logfile, session)
		finally:
			session.reloading = False
		self.slog("after log replay: actor=%s, owner=%s, status=%s" % (session.actor, session.owner, session.status), session)
		self.do_watch_session_log(logfilename, logfile, session)

	def watch_session_log(self, logfilename, session):
		# get file handle
		self.sdebug(None, logfilename, session)
		logfile = os.open(logfilename, os.O_RDONLY)
		self.sdebug("logfile=%s" % logfile, logfilename, session)
		self.do_watch_session_log(logfilename, logfile, session)

	def use_gio(self, session):
		return CAN_USE_GIO and not sys.platform.startswith("freebsd")

	def stop_watching_logfile(self, session):
		if session.log_watcher_cancel and not session.log_watcher_cancel.is_cancelled():
			session.log_watcher_cancel.cancel()

	def do_watch_session_log(self, logfilename, logfile, session):
		# watch log file for changes
		use_gio = self.use_gio(session)
		if use_gio:
			self.sdebug("will use gio to monitor for changes", logfilename, logfile, session)
			import gio
			gfile = gio.File(logfilename)
			session.log_watcher_cancel = gio.Cancellable()
			mfile = gfile.monitor_file(cancellable=session.log_watcher_cancel)
			def session_logfile_changed(*args):
				self.sdebug()
				self.read_from_log(logfilename, logfile, session)
			mfile.connect("changed", session_logfile_changed)
			session.log_watcher = mfile
			self.slog("watching log with gio: %s" % mfile, logfilename, logfile, session)
		else:
			self.slog("gio unavailable or disabled, scheduling timers for parsing log file...")
			#wrapper class to make an IDelayedCall look like a gio.Cancellable
			# we only implement cancel() and is_cancelled() as this is all we need here
			class fake_cancellable_gio(object):
				def __init__(self, idelayedcall):
					self.idelayedcall = idelayedcall
				def is_cancelled(self):
					return	not self.idelayedcall.active()
				def cancel(self):
					self.idelayedcall.cancel()
			def call_watcher(delay, function_to_call):
				session.log_watcher = function_to_call
				idelayedcall = callLater(delay, function_to_call)
				session.log_watcher_cancel = fake_cancellable_gio(idelayedcall)
			def read_from_timer(*args):
				self.sdebug(None, *args)
				self.read_from_log(logfilename, logfile, session)
				if session.status!=Session.STATUS_CLOSED:
					call_watcher(10, read_from_timer)
			call_watcher(1, read_from_timer)

		#ensure we stop watching for changes when the session ends:
		def close_session_stop_watching_logfile():
			self.stop_watching_logfile(session)
		session.add_status_update_callback(None, Session.STATUS_CLOSED, close_session_stop_watching_logfile, timeout=None)
		#FIXME: must prevent client application's stdout/stderr from flooding us!
		session.read_pending = False
		self.do_read_from_log(logfilename, logfile, session)


	def read_from_log(self, logfilename, logfile, session):
		"""
		As this method is fired from gio.File, it can fire quite often,
		so we queue up a read request to run soon,
		giving time for other events to be batched up together.
		We also ignore messages written after the session is closed.
		"""
		if session.debug_log_read:
			self.sdebug("read_pending=%s" % session.read_pending, logfilename, logfile, session)
		if session.read_pending or session.status==Session.STATUS_CLOSED:
			return	False	#dont run again if running from gobject.io_watch
		session.read_pending = True
		callLater(0.1, self.start_read_from_log, logfilename, logfile, session)
		return	True	#keep running if from gobject.io_watch

	def start_read_from_log(self, logfilename, logfile, session):
		t = thread.start_new_thread(self.do_read_from_log, (logfilename, logfile, session))
		self.sdebug("started thread: %s" % t, logfilename, logfile, session)
		return False

	def do_read_from_log(self, logfilename, logfile, session):
		""" this may be called from multiple threads, so use the read_lock """
		self.sdebug(None, logfilename, logfile, session)
		if not session.read_lock.acquire(False):
			self.sdebug("read_lock already held", logfilename, logfile, session)
			return
		session.debug_log_read = True
		try:
			#FIXME: could get a DoS if the line is very long and consumes memory...
			BUFSIZE=1024
			while True:
				# clear flag since we are about to read until we find the end:
				session.read_pending = False
				# raw read:
				if session.debug_log_read:
					self.sdebug("about to read, current buffer: %s bytes" % len(session.log_buffer), logfilename, logfile, session)
				raw = os.read(logfile, BUFSIZE)
				if session.debug_log_read:
					self.sdebug("read %s bytes" % len(raw), logfilename, logfile, session)
				if len(raw)==0:
					break
				# append to buffer:
				data = session.log_buffer+raw
				lines = data.splitlines(True)
				last = lines[len(lines)-1]
				if not (last.endswith('\n') or last.endswith('\r')):
					""" save the incomplete line in the buffer """
					session.log_buffer = last
					lines = lines[:len(lines)-1]
				else:
					""" all lines are complete, clear the whole buffer """
					session.log_buffer = ''
				if session.debug_log_read:
					self.sdebug("read %s lines" % len(lines), logfilename, logfile, session)
				self.process_log_data(session, lines)
				if len(raw)<BUFSIZE and not session.read_pending:
					""" if we read less than the buffer, we're at the end - unless someone just reset the read_pending flag """
					break
			self.poll_server_process(logfilename, session)
		finally:
			session.read_lock.release()
		return session.status!=Session.STATUS_CLOSED					#if running from a timer (has_gio=False), stop when closed

	def process_log_data(self, session, lines):
		"""
		Given a session and the lines read from the logfile,
		process each line using process_log_line().
		If "session.reloading" is not set, the new status is set (if defined) each time around the loop,
		otherwise only once at the end.
		This may be called from threads, so care must be taken in process_log_line()
		"""
		sig = msig(session, "%d lines" % len(lines))
		if session.debug_log_read:
			self.debug(sig+" reloading=%s, lines=%s" % (session.reloading, str(lines)))
		if not lines:
			return
		repeated = 0
		last_status = None
		def set_status(new_status):
			""" this method may be called from another thread - see start_read_from_log """
			callFromThread(self.update_session_status, session, new_status)
		for line in lines:
			if session.last_log_line and line==session.last_log_line:
				repeated += 1
				continue
			if repeated>1:
				if session.debug_log_read:
					self.sdebug("previous line repeated %s times!" % repeated, session, visible_command(csv_list(lines)))
				repeated = 0
			try:
				new_status = self.process_log_line(session, line)
				if session.debug_log_read:
					self.debug(sig+" new status(%s)=%s" % (line, new_status))
				if not session.reloading and self.update_session_status and new_status:
					self.log(sig+" setting new status=%s from line=%s" % (new_status, line))
					set_status(new_status)
				if new_status:
					last_status = new_status
			except Exception, e:
				self.serr("on line=%s" % line, e, session, visible_command(csv_list(lines)))
			session.last_log_line = line
		if session.reloading and last_status and self.update_session_status:
			self.log(sig+" final new status=%s" % last_status)
			set_status(last_status)


	def process_log_line(self, session, line):
		self.serror("should be overridden by %s" % type(self), session, line)

	def schedule_detect_sessions(self, batch_delay=0.1):
		"""
		Will schedule detect_sessions_from_timer to run very soon.
		Giving other events time to accumulate so we don't fire too many times.
		"""
		if self.detect_pending:
			self.sdebug("already pending - not scheduling it")
			return
		self.detect_pending = True
		callLater(self.batch_detect_sessions_delay, self.batch_detect_sessions)

	def batch_detect_sessions(self):
		"""
		Just clears the pending flag before calling detect_screen_sessions()
		"""
		self.sdebug()
		self.detect_pending = False
		self.detect_sessions()
		return False

	def detect_sessions(self):
		self.sdebug("not implemented for %s" % self.session_type)

	def get_free_port(self):
		return	self.port_mapper.get_free_port(self.port_base)

	def stop(self):
		self.log()
		self.stop_preloaded_sessions()


	def session_cleanup(self, session):
		"""
		Perform any necessary cleanup when a session is closed.
		"""
		self.stop_watching_logfile(session)
		if not self.config.delete_session_files:
			return
		session_dir = self.get_session_dir(session)
		self.slog("clearing %s" % session_dir, session)
		try:
			shutil.rmtree(session_dir)
		except Exception, e:
			self.serror("failed to delete %s: %s - trying again" % (session_dir, e), session)
			#this may succeed now, if the error was because of a race:
			try:
				shutil.rmtree(session_dir)
			except Exception, e:
				self.serr("failed to delete %s" % session_dir, e, session)
		#now clear pulse
		if session.pulse_address and session.pulse_address.startswith("unix:"):
			socket = session.pulse_address[len("unix:"):]
			if socket.startswith("/tmp/"):			#basic sanity check
				self.sdebug("removing socket: %s" % socket)
				delete_if_exists(socket)
				pulse_dir = os.path.dirname(socket)
				if is_valid_dir(pulse_dir) and pulse_dir!="/tmp":
					try:
						native_socket_file = os.path.join(pulse_dir, "native")
						delete_if_exists(native_socket_file)
						os.rmdir(pulse_dir)
					except Exception, e:
						self.serror("failed to remove %s: %s" % (pulse_dir, e), session)
						self.serror("contents of %s: %s" % (pulse_dir, os.listdir(pulse_dir)))

	def stop_preloaded_sessions(self):
		for session in self.config.sessions.values():
			if session.preload and session.session_type==self.session_type and not session.status in [Session.STATUS_CLOSED]:
				try:
					self.stop_preloaded_session(session)
				except Exception, e:
					self.serr("failed to stop %s" % session, e)

	def stop_preloaded_session(self, session):
		"""
		Defaults to stopping using standard stop_display() method.
		"""
		self.sdebug(None, session)
		self.stop_display(session, None, session.display)

	def get_prelaunch_session(self, uuid, username, screen_size, opts):
		"""
		Tries to find a compatible pre-launch session.
		"""
		self.sdebug("going to test: %s" % csv_list(self.config.sessions.values()), uuid, username, screen_size, opts)
		try:
			self.lock.acquire()
			for session in self.config.sessions.values():
				if session.session_type != self.session_type:
					self.sdebug("session %s is not a %s session: %s" % (session, self.session_type, session.session_type), uuid, username, screen_size, opts)
					continue			#not the right type!
				if username and username!=session.user:
					self.sdebug("session %s is already associated with another user: %s" % (session, session.user), uuid, username, screen_size, opts)
					continue			#in case we run as root
				if not session.status:
					self.serror("session %s does not have a status!?" % session, uuid, username, screen_size, opts)
					continue			#this should never happen!
				if not session.preload:
					self.sdebug("session %s is not a preloaded session" % session, uuid, username, screen_size, opts)
					continue			#not a preload session!
				if session.actor and (not uuid or session.actor!=uuid):
					self.sdebug("session %s is already pre-connected to another user: %s" % (session, session.actor), uuid, username, screen_size, opts)
					continue			#pre-connected to another uuid
				if not self.is_prelaunched_session_compatible(session, screen_size, opts):
					self.sdebug("session %s is not compatible" % session, uuid, username, screen_size, opts)
					continue
				self.sdebug("=%s" % session, uuid, username, screen_size, opts)
				return session		#OK!
		finally:
			self.lock.release()
		return None

	def is_prelaunched_session_compatible(self, session, screen_size, opts):
		"""
		Tests if the given session is compatible with the options given.
		"""
		depth = None
		if screen_size:
			#only the depth needs to match (dimensions can be changed on the fly using xrandr)
			parsed = parse_screensize(screen_size)
			if parsed:
				(_,_,depth) = parsed
		if not self.is_prelaunch_session_options_compatible(session, session.options, opts):
			self.sdebug("prelaunch session options are not compatible with request: %s" % session.options, session, screen_size, opts)
			return	False
		if depth and depth>0:
			parsed = parse_screensize(session.screen_size)
			if not parsed:
				self.sdebug("bit depth missing (we need %s)" % depth, session, screen_size, opts)
				return False
			(_,_,d) = parsed
			if d!=depth:
				self.sdebug("bit depth does not match: %s vs %s" % (d, depth), session, screen_size, opts)
				return False
		return True

	def is_prelaunch_session_options_compatible(self, session, session_opts, opts):
		""" compare session options and deny if there are any different options """
		return	self.filter_options_for_compare(session_opts)==self.filter_options_for_compare(opts)

	def filter_options_for_compare(self, options):
		""" ensure None equals {} so we never compare None with {}, which would fail the test """
		if options is None:
			return	{}
		if not self.ignored_options_for_compare:
			""" nothing to filter """
			return	options
		opts = options.copy()
		for x in self.ignored_options_for_compare:
			if x in opts:
				del opts[x]
		return	opts



	def	may_schedule_prelaunch(self, uuid, delay=5.0):
		"""
		Schedules a prelaunch session to be started for the user if one does not exist already.
		"""
		if not self.prelaunch_enabled:
			return
		if len(self.clean_prelaunch_failures())>=self.max_prelaunch_failures:
			self.serr("too many prelaunch failures: %s - not starting any more" % len(self.prelaunch_failures), uuid, delay)
			return
		existing = self.get_prelaunch_session(uuid, None, None, {})
		if not existing:
			callLater(delay, self.may_start_prelaunch_session, uuid)
		else:
			self.sdebug("prelaunch session already exists: %s" % existing, uuid, delay)

	def clean_prelaunch_failures(self):
		"""
		We record the time when the prelaunch failure occured,
		so we need to "clean" the list and remove old ones before making any decisions.
		"""
		now = time.time()
		self.prelaunch_failures = [t for t in self.prelaunch_failures if ((now-t)<60*10)]
		return	self.prelaunch_failures

	def may_start_prelaunch_session(self, uuid):
		"""
		We re-check that there aren't any prelaunch sessions before starting a new one.
		(existing ones could have been found on disk during startup)
		"""
		existing = self.get_prelaunch_session(uuid, None, None, {})
		if not existing:
			self.start_prelaunch_session(uuid)
		return	False


	def start_prelaunch_session(self, uuid):
		"""
		Starts a placeholder session by calling create_prelaunch_session
		which should be implemented by all subclasses that offer preloading (just return None if not).
		"""
		try:
			session = self.do_start_session(None, uuid, None, True)
			if session:
				self.sdebug("saved %s, sending it to clients" % session, uuid)
				self.add_session(session)
			else:
				self.sdebug("session=%s" % session, uuid)
		except Exception, e:
			self.serr(None, e, uuid)
		return	False			#just run once


	def prelaunch_start_command(self, session, server_command, user, screen_size, opts, filenames):
		"""
		Starts an existing PRELAUNCH session by saving the configuration file with the new real command.
		The delayed_start script should be monitoring this file and picking up the modification.
		The actual saving of the file is not done here, see start_session()
		"""
		self.slog(None, session, server_command, user, screen_size, opts, filenames)
		session.command = server_command.get_command_for_display(session.display, filenames)
		session.commands = [session.command]
		session.owner = user.uuid
		session.screen_size = screen_size
		session.start_time = int(time.time())
		session.command_uuid = server_command.uuid
		session.preload = False		#this will fire the real command when the file is saved
		session.options = opts
		self.initialize_session_from_command(session, server_command)


	def add_local_user(self, username, uuid):
		"""
		Used by the server when running in multi-user mode (as root), to tell us about which
		users are connected (or likely to be connected "preloaded_users") so we can start prelaunch
		sessions for them.
		"""
		self.sdebug(None, username, uuid)
		if username in self.local_users:
			return	False
		self.local_users.append(username)
		if uuid:
			self.may_schedule_prelaunch(uuid, 1.0)
		return	True

	def remove_user(self, uuid):
		"""
		Used by the server to notify us that a client has disconnected.
		"""
		pass

	#***************************************************************************************************
	# Session start/state management
	def can_client_set_status(self, session, user_id, _from, _to):
		"""
		By default, clients can only change from CONNECTED to IDLE and back.
		All the other state changes are triggered by the server process (it knows best).
		"""
		return	(_from==Session.STATUS_CONNECTED and _to==Session.STATUS_IDLE) \
			or (_from==Session.STATUS_IDLE and _to==Session.STATUS_CONNECTED)

	def get_session_bind_address(self):
		"""
		By default we now bind to all interfaces, binding to a specific interface is very tricky
		as we have no way of knowing which interface is really used when multiple IPs from the same range
		are assigned. (ie: eth0=192.168.0.1, wlan0=192.168.0.2 - only one is really accessible...)
		Returns a tuple: (host, tunnel)
		"""
		if self.config.ssh_tunnel:
			return	("127.0.0.1", True)
		if self.bind_to_all_interfaces:
			return	("0.0.0.0", False)		#Listen on all interfaces
		iface_ipmasks = get_iface_ipmasks()
		can_bind = {}
		for (iface, ipmask) in iface_ipmasks.items():
			if iface == "lo":
				continue			#ignore loopback
			if len(ipmask)!=1:
				continue			#ignore interfaces with multiple IPs (could cause us problems)
			can_bind[iface] = ipmask
		self.sdebug("can bind(%s)=%s" % (iface_ipmasks, can_bind))
		if len(can_bind)!=1:
			return	(LOCALHOST, True)		#Always tunnel if there is not exactly one active interface (apart from lo)
		(iface, ipmasks) = can_bind.items()[0]
		self.sdebug("found 1 interface we can bind to: %s / %s" % (iface, ipmasks))
		(ip, mask) = ipmasks[0]
		self.sdebug("using %s/%s" % (ip, mask))
		return	(ip, False)



	def get_connecting_timeout(self):
		"""
		For implementations that do not time out automatically, return a delay in seconds.
		The session will be moved back to "available" state if it does not reach "connected" in time.
		"""
		return self.connecting_timeout

	def get_test_port(self, session):
		"""
		The tcp port that can be used for testing if the session is ready.
		Only used if test_using_sockets=True
		"""
		raise Exception("Must be overriden")

	def wait_for_session_readyness(self, session, user, success_callback=None, error_callback=None):
		"""
		Waits for the session to become available (using the socket_test only here).
		if test_using_sockets is not set, another mechanism must be responsible for
		setting the session's status to AVAILABLE.

		When it is, it will fire success_callback(), error_callback otherwise.
		"""
		sig = msig(session, user, success_callback, error_callback)
		self.log(sig+" adding/firing session status update callbacks")
		""" If the session is already "available", fire the callback immediately,
		otherwise make it fire when the session reaches that state. """
		if session.status == Session.STATUS_AVAILABLE:
			success_callback()
		else:
			session.add_status_update_callback(Session.STATUS_STARTING, Session.STATUS_AVAILABLE, success_callback)

		if self.test_using_sockets:
			""" this will fire success_callback() via the status update callback mechanism """
			port = self.get_test_port(session)
			self.log(sig+" testing tcp port=%d for display=%s" % (port, session.display))
			def session_port_ready():
				self.sdebug()
				self.update_session_status(session, Session.STATUS_AVAILABLE)
			wait_for_socket(session.host, port, self.session_start_timeout,
							success_callback=session_port_ready,
							error_callback=error_callback,
							abort_test=lambda : self._is_session_closed(session))
		else:
			def check_session_has_started():
				self.sdebug("status=%s, preload=%s" % (session.status, session.preload))
				if session.status==Session.STATUS_STARTING and not session.preload:
					self.stop_display(session, None, session.display)
					self.early_failure(session, "Session failed to start, waited %s seconds" % self.session_start_timeout)
			callLater(self.session_start_timeout, check_session_has_started)


	def _is_session_closed(self, session):
		return	session.status==Session.STATUS_CLOSED

	def client_attach_timeout(self, session, user):
		self.slog("connection timeout! resetting to available!")
		session.set_status(Session.STATUS_AVAILABLE)

	def	can_send_session(self, session, user):
		return	True

	def	prepare_session_for_attach(self, session, user, disconnect, call_when_done):
		"""
		Ensures that the user will be able to connect to the session.
		"""
		if session.port>0:
			#port already assigned, open firewall now
			self.firewall_util.allow(user.remote_host, session.host, session.port)
			cb = call_when_done
		else:
			#open port when we have it:
			def session_prepared(*args):
				self.firewall_util.allow(user.remote_host, session.host, session.port)
				self.sdebug(None, *args)
				call_when_done()
			cb = session_prepared
		self.do_prepare_session_for_attach(session, user, disconnect, cb)
		#if set, check for connection timeouts:
		if self.connecting_timeout>0:
			count = session.status_update_count
			def verify_connected():
				self.sdebug("checking session %s: previous count=%s, current status_update_count=%s" % (session, count, session.status_update_count))
				if session.status==Session.STATUS_CONNECTING and count==session.status_update_count:
					if not disconnect:
						self.slog("connection timeout! retrying with disconnect=True")
						def noop():
							self.sdebug()
						self.prepare_session_for_attach(session, user, True, noop)
					else:
						self.client_attach_timeout(session, user)
			callLater(self.connecting_timeout, verify_connected)

	def session_detached(self, session, user):
		"""
		Takes care of freeing resources that were used by this user to connect to the session.
		At least, tell the firewall (if used) that the hole is no longer needed.
		"""
		if user:
			self.firewall_util.remove(user.remote_host, session.host, session.port)

	def stop_display(self, session, user, display):
		self.slog(None, session, user, display)
		self.kill_display(session, session.server_process_pid)

	def kill_display(self, session, process_pid, kill_signal=signal.SIGTERM):
		self.slog(None, session, process_pid, kill_signal)
		if process_pid and process_pid>1 and session.status!=Session.STATUS_CLOSED and kill0(process_pid):
			send_kill_signal(process_pid, kill_signal)
			if kill_signal==signal.SIGTERM:
				#try one last time with SIGKILL...
				callLater(5, self.kill_display, session, process_pid, signal.SIGKILL)

	def disconnect(self, session):
		self.serror("disconnect not supported!", session)


	#***************************************************************************************************
	# session creation:
	def start_session(self, user, server_command, screen_size, opts, filenames):
		"""
		Start a session. This method will first try to use a prelaunched session if one exists,
		otherwise it will call do_start_session() to create a new one.
		"""
		session = None
		if self.prelaunch_enabled:
			""" If enabled: try to find a compatible pre-launched session: """
			session = self.get_prelaunch_session(user.uuid, user.username, screen_size, opts)
			if session:
				""" Sets the session attributes - save will cause it to start """
				self.prelaunch_start_command(session, server_command, user, screen_size, opts, filenames)
				self.add_session(session)
				self.save_session(session)
				self.may_schedule_prelaunch(user.uuid, 5.0)		#in case we used up all the pre-launch
				return	session

		""" Start a new one """
		session = self.do_start_session(user, user.username, server_command, False, screen_size, opts, filenames)
		if not session:
			return None
		# find icon
		if not session.default_icon_data and server_command:
			session.set_default_icon_data(guess_icon_from_name(server_command.command))
			if not session.default_icon_data:
				session.set_default_icon_data(guess_icon_from_name(server_command.name))

		self.save_session(session)
		self.add_session(session)	#misnamed: this sends it to all the users (even if they can't bind to it yet)
									#actually adding to the list of sessions is done in do_start_session()
		return	session


	def is_no_windarwin_no_shadow(self, session):
		if WIN32 or OSX:
			#no xauth on win32 or osx
			return	False
		return	not session.shadowed_display

	def xauth_enabled(self, session):
		return	self.is_no_windarwin_no_shadow(session)

	def set_keyboard_mappings(self, session, user):
		self.slog(None, session, user)
		session.env = session.get_env()
		set_keymap(session.env,
							user.xkbmap_layout, user.xkbmap_variant,
							user.xkbmap_print, user.xkbmap_query)
		self.set_xmodmap(session, user)

	def set_xmodmap(self, session, user):
		set_xmodmap(session, user)


	def do_start_session(self, user, uuid, server_command, is_preload=False, screen_size=None, opts=None, filenames=None):
		"""
		Creates a new session, calling start_display(), also starting sound/print,etc
		user may be null when doing preload
		"""
		sig = msig(user, uuid, server_command, is_preload, screen_size, opts, filenames)
		self.debug(sig)

		session = self.initialize_new_session(uuid, server_command, is_preload, screen_size, options=opts)
		def close_session_cleanup():
			self.session_cleanup(session)
		session.add_status_update_callback(None, Session.STATUS_CLOSED, close_session_cleanup, timeout=None)
		if user:
			session.owner = user.uuid
		if is_preload:
			session.command = ""	#will be set when we start the real command
		else:
			session.command = self.get_session_command(session, server_command, screen_size, filenames)

		if self.xauth_enabled(session):
			self.create_xauth(session)
			def close_session_remove_xauth():
				self.remove_xauth(session)
			session.add_status_update_callback(None, Session.STATUS_CLOSED, close_session_remove_xauth, True, None)
			if user:
				self.set_xauth(session, user)
		#dbus:
		if self.dbus_enabled:
			self.start_dbus(session)
		#print
		if self.config.tunnel_printer:
			self.start_ipp(session)
		#sound
		if self.config.tunnel_sink or self.config.tunnel_source:
			self.start_sound_server(session)
		session.env = session.get_env()
		self.save_session(session)
		self.config.add_session(session)
		#catch early session errors:
		def start_failed():
			self.early_failure(session, "Session failed to start")
		session.add_status_update_callback(session.status, Session.STATUS_CLOSED, start_failed, True, 10)
		return	self.start_session_object(session, user, is_preload)

	def get_session_command(self, session, server_command, screen_size, filenames):
		return server_command.get_command_for_display(session.display, filenames)

	def start_session_object(self, session, user, is_preload):
		started = None
		try:
			started = self.start_display(session, user, is_preload)
			self.sdebug("started=%s" % started, session, user, is_preload)
		except Exception, e:
			self.serr(None, e, session, user, is_preload)
		if not started:
			self.slog("Error: process failed to start!", session, user, is_preload)
			self.config.remove_session(session)
			self.session_cleanup(session)
			return	None
		self.firewall_util.add(session.host, session.port)
		if not is_preload:
			self.firewall_util.allow(user.remote_host, session.host, session.port)
		return session

	def start_daemon(self, session, args, env, use_daemon_wrapper=True):
		session_dir = self.get_session_dir(session)
		if not is_valid_dir(session_dir):
			os.mkdir(session_dir)
		if not env:
			env = session.get_env()
		try:
			if WIN32:
				return	self.start_win32_nodaemon(session, args, env)
			else:
				self.clear_existing_logfile(session)
				logfilename = self.get_log_file(session)
				self.slog("starting %s, logging to %s" % (args, logfilename), session, env, use_daemon_wrapper)
				pid = start_daemon(logfilename, args, env=env, cwd=session.get_cwd(), use_daemon_wrapper=use_daemon_wrapper)
				if not pid:
					self.early_failure(session, "failed to start daemon process!")
					return	False
				assert pid>0, "invalid pid for %s: %s" % (args, pid)
				self.session_process_started(pid, session)
				self.watch_session_log(logfilename, session)
				self.poll_server_process(logfilename, session)
				return True
		except Exception, e:
			self.serr("%s process failed to start: %s" % (self.session_type, e), e, args, session, env, use_daemon_wrapper)
			self.early_failure(session, "%s failed to start" % self.session_type)
			return	False

	def start_win32_nodaemon(self, session, args, env):
		#the rubbish platform again causing trouble..
		#See the changes to this file for how many different options were tested and all had problems...
		#LineProcessProtocolWrapper: doesn't seem to terminate or show lines of output..
		#Using subprocess with files and "os.fdopen(logfile.fileno(), 'w', 0)" to force the log file to not be buffered: got a hard application crash on winxp
		#See also:
		#http://bugs.python.org/issue3907
		#http://stackoverflow.com/questions/803265/getting-realtime-output-using-subprocess
		#Could also have tried: http://www.noah.org/wiki/Pexpect#Description_of_Pexpect
		from winswitch.util.process_util import SimpleLineProcess
		def line_handler(line):
			self.slog(None, line)
			self.process_log_data(session, [line])
		def started(proc):
			self.sdebug()		#pid is not set at this point!
		def ended(proc):
			self.slog(None, proc)
			self.update_session_status(session, Session.STATUS_CLOSED)
		proc = SimpleLineProcess(args, env, os.getcwd(), line_handler, started, ended, log_full_command=True)
		proc.start()
		pid = None
		if proc.pid:
			pid = proc.pid
		self.slog("proc=%s, pid=%s" % (proc, pid), session, env)
		if pid>0:
			self.session_process_started(pid, session)
			self.update_session_status(session, Session.STATUS_AVAILABLE)
		else:
			self.early_failure(session, "Process failed to start")
		return pid>0			#we should use a deferred here... (that gets populated on started() or ended())


	def wait_for_server_exit(self, session, proc):
		self.slog(None, session, proc)
		try:
			(out, err) = proc.communicate()
			self.sdebug("out=%s, err=%s" % (out, err), session, proc)
		except Exception, e:
			self.serr(None, e, session, proc)
		self.slog("closing session", session, proc)
		self.update_session_status(session, Session.STATUS_CLOSED)

	def session_process_started(self, pid, session):
		self.slog(None, pid, session)
		session.server_process_pid = pid
		session.onexit_kill_pids["99-server-process"] = pid

	def session_process_ended(self, pid, condition, session):
		self.do_session_process_ended(pid, condition, session)

	def do_session_process_ended(self, pid, condition, session, new_status=Session.STATUS_CLOSED):
		self.slog(None, pid, condition, session)
		session.remove_kill_pid(pid)			#already dead...
		self.update_session_status(session, new_status)

	def early_failure(self, session, message=None, new_status=Session.STATUS_CLOSED):
		"""
		This method fires when the session goes from "starting" to "closed" state within 10 seconds.
		"""
		self.session_error(session, message, new_status)

	def session_error(self, session, message=None, new_status=Session.STATUS_CLOSED):
		self.serror("notification handler=%s" % self.session_failed, session, message, new_status)
		if self.session_failed:
			self.session_failed(session, message, new_status)

	def poll_server_process(self, logfilename, session):
		"""
		Checks to see if the server process pid is still active by sending a kill -0
		TODO: checks to see if the server process still has the logfile opened.
		(this is the only way to tell if it is still running since it is now a daemon process:
		we cannot use os.waitpid or glib's child_watch_add)
		"""
		pid = session.server_process_pid
		self.sdebug("pid=%s" % pid, logfilename, session)
		if not pid or pid<=0:
			if session.status!=Session.STATUS_CLOSED:
				self.serror("session is still active, but we do not have a server_process_pid to poll!", logfilename, session)
			else:
				self.sdebug("terminated: server_process_pid unset and session is already closed", logfilename, session)
		if not kill0(pid):
			self.session_process_ended(pid, "poll failed", session)

	def initialize_new_session(self, uuid, server_command, is_preload, screen_size, display=None, tcp_port=None, options=None):
		"""
		Allocates a display, initializes host/port, ID, password, ..
		"""
		session = ServerSession(self.config)
		if not display:
			display = ":%d" % self.port_mapper.get_free_X_display()
		if not tcp_port:
			tcp_port = self.get_free_port()
		#ensure the ports are freed when session closes:
		def close_session_free_ports():
			self.free_session_ports(session)
		session.add_status_update_callback(None, Session.STATUS_CLOSED, close_session_free_ports, clear_it=True, timeout=None)
		(host, tunnel) = self.get_session_bind_address()
		session.display = display
		if server_command:
			self.initialize_session_from_command(session, server_command)
		if options:
			session.options = options
		else:
			session.options = {}
		session.host = host
		session.port = tcp_port
		session.ID = self.new_ID()
		session.requires_tunnel = tunnel
		session.password = self.new_password()
		session.xauth_cookie = session.password				#needs to be identical to password for NX! VNC will change and encrypt the password and won't use xauth..
		session.user = USERNAME
		session.owner = uuid
		session.status = Session.STATUS_STARTING
		session.preload = is_preload
		session.screen_size = screen_size
		session.session_type = self.session_type
		session.start_time = int(time.time())
		session.env = session.get_env()
		session.debug_log_read = self.debug_log_read
		self.sdebug("=%s" % session, uuid, server_command, is_preload, screen_size, display, tcp_port)
		return	session

	def initialize_session_from_command(self, session, server_command):
		session.full_desktop = (server_command.type == ServerCommand.DESKTOP)
		session.set_default_icon_data(server_command.get_icon_data())
		session.command_uuid = server_command.uuid
		session.name = server_command.name
		session.uses_sound_out = server_command.uses_sound_out
		session.uses_sound_in = server_command.uses_sound_in
		self.sdebug("now set name=%s, uses_sound_out=%s, uses_sound_in=%s" % (session.name, session.uses_sound_out, session.uses_sound_in), session, server_command)


	def free_session_ports(self, session):
		self.port_mapper.free_port(session.port)
		self.port_mapper.free_X_display(session.display)

	def new_password(self):
		return	"%s%s" % (generate_UUID(),generate_UUID())

	def new_ID(self):
		return generate_UUID()






	#***************************************************************************************************
	# Shadow stuff used by NX and VNC:
	def create_shadow_session(self, session, user, read_only, screen_size, options):
		"""
		Calls new_shadow_session()
		saves the new session object and adds it to the config list and propagates it to all the clients (via self.add_session)
		"""
		shadow = self.new_shadow_session(session, user, read_only, screen_size, options)
		self.save_session(shadow)
		self.config.add_session(shadow)
		self.add_session(shadow)
		return	shadow

	def new_shadow_session(self, session, user, read_only, screen_size, options):
		"""
		Creates a shadow of another session (just the container object - it does not actually start the shadow).
		"""
		self.slog(None, session, user, read_only)
		assert user is not None
		shadow = self.initialize_new_session(user.username, None, False, None, options=options)
		shadow.update_fields(["default_icon_data", "default_icon", "screen_size", "pulse_address",
							"command_uuid", "command", "name"], session, False, False)
		shadow.shadowed_display = session.display
		shadow.full_desktop = True
		shadow.shared_desktop = True
		shadow.owner = session.owner
		shadow.session_type = self.session_type
		shadow.read_only = read_only
		if screen_size:
			shadow.screen_size = screen_size
		shadow.options = options
		if not self.xauth_enabled(shadow):
			shadow.xauth_cookie = ""		#should not be used
		try:
			from winswitch.ui.icons import getraw
			if read_only:
				shadow.default_icon_data = getraw("shadow")
			else:
				shadow.default_icon_data = getraw("copy")
		except:
			pass
		return	shadow


	def shadow_start(self, session, user, success_callback, error_callback):
		def shadow_start_failed():
			self.early_failure(session, "Shadow session failed to start")
		session.add_status_update_callback(session.status, Session.STATUS_CLOSED, shadow_start_failed, True, self.shadow_start_timeout)
		session.add_status_update_callback(session.status, Session.STATUS_CLOSED, error_callback, True, self.shadow_start_timeout)
		if session.status==Session.STATUS_AVAILABLE:
			success_callback()
		else:
			session.add_status_update_callback(None, Session.STATUS_AVAILABLE, success_callback, True, self.shadow_start_timeout)
		self.start_session_object(session, user, False)





	#***************************************************************************************************
	# Common X methods xmodmap, xauth...
	def get_X_port(self, session):
		return	X_PORT_BASE + int(session.display[1:])

	def prepare_display(self, session, user):
		"""
		Once the display is ready, start the real session command.
		"""
		if not session.shadowed_display:
			#actual command:
			self.start_session_command(session)
		#allow access through the firewall
		self.firewall_util.allow(user.remote_host, session.host, session.port)

	def start_session_command(self, session):
		"""
		Ensure we start the real command via delayed_start so it can outlive this process.
		"""
		# ensure the session on disk reflects the current state before launching
		self.save_session(session)
		if self.orbit_socket_dir_enabled:
			self.set_orbit_dir(session)
		#gnome-settings-daemon
		if self.gnome_settings_daemon_enabled:
			self.start_gnome_settings_daemon(session)
		self.sdebug("start_gnome_keyring_daemon=%s" % self.config.start_gnome_keyring_daemon, session)
		if self.config.start_gnome_keyring_daemon:
			self.start_gnome_keyring_daemon(session)
		if session.full_desktop:
			#start the full_desktop only session commands:
			self.do_start_session_commands(session, self.config.desktop_session_commands)
		#launch the wrapper:
		self.start_session_wrapper(session)


	def start_session_wrapper(self, session):
		session_file = get_session_filename(session.display, session.user, USER_ID==0)
		wrapper_log_file = get_session_wrapper_log_filename(session.display, session.user, USER_ID==0)
		delete_if_exists(wrapper_log_file)
		if WINSWITCH_LIBEXEC_DIR:
			delayed_start = os.path.join(WINSWITCH_LIBEXEC_DIR, "delayed_start")
		else:
			delayed_start = "delayed_start"
		args = [delayed_start, session_file, "--print-pid", "--daemon", "--log-file", wrapper_log_file]
		if not CAN_USE_GIO:
			args.append(DISABLE_GIO_ARG)
		out, err = None, None
		try:
			env = session.get_env()
			from winswitch.server.xdg_open import WSW_XDG_OPEN
			self.sdebug("XDG_OPEN_COMMAND=%s, WSW_XDG_OPEN=%s, valid exe=%s" % (XDG_OPEN_COMMAND, WSW_XDG_OPEN, is_valid_exe(XDG_OPEN_COMMAND)))
			if is_valid_exe(XDG_OPEN_COMMAND) and is_valid_file(WSW_XDG_OPEN) and XDG_OPEN_COMMAND.find("winswitch")<0:
				env["WSW_XDG_OPEN"] = WSW_XDG_OPEN
				env["WSW_ORIGINAL_XDG_OPEN"] = XDG_OPEN_COMMAND
				env["WSW_SESSION_ID"] = session.ID
				#tweak the PATH so our wrapper comes first:
				bin_override="%s/bin-override" % WINSWITCH_LIBEXEC_DIR
				env["PATH"] = bin_override+":"+os.environ.get("PATH", "")
			from winswitch import __version__
			env["WINSWITCH_SERVER"] = __version__
			env["_WINSWITCH_SERVER_PID"] = "%s" % os.getpid()
			#apparently this fixes Qt/KDE performance in some cases:
			env["QT_GRAPHICSSYSTEM"] = "native"
			self.sdebug("args=%s, env=%s, session_file=%s" % (csv_list(args), env, session_file), session)
			self.save_session(session)
			code, out, err = get_output(args, env=env)
			if out and code==0:
				pid = int(out)
				self.slog("pid(%s)=%s" % (args, pid), session)
				session.command_process_pid = pid
				if pid:
					session.onexit_kill_pids["50-session-wrapper"] = pid
				return pid
			else:
				self.serror("failure: args=%s, returncode=%s, stdout=%s, stderr=%s" % (csv_list(args), code, out, err), session)
		except Exception, e:
			self.serr("args_list=%s, stdout=%s, stderr=%s: %s" % (csv_list(args), out, err, e), session)
		self.early_failure(session, "session wrapper failed to start")
		self.update_session_status(session, Session.STATUS_CLOSED)
		return None


	def do_start_session_commands(self, session, commands, onexit_kill=False):
		self.sdebug(None, session, commands, onexit_kill)
		for command in commands:
			self.do_start_session_command(session, command, onexit_kill)

	def do_start_session_command(self, session, command, onexit_kill=False):
		if not command:
			return	None
		proc = exec_nopipe(command, env=session.get_env(), shell=True, cwd=session.get_cwd(), setsid=True)
		self.sdebug("process(%s)=%s" % (command, proc), session, command)
		if proc and onexit_kill:
			session.onexit_kill_pids["25-%s" % command] = proc.pid
		return	proc


	def set_xauth(self, session, user):
		"""
		Set the X authentication cookie on this session
		"""
		locations = [session.display, "localhost" + session.display]
		if user.remote_host:
			locations.append(user.remote_host+session.display)
		for location in locations:
			xauth_cmd = ["xauth", "add", location, "MIT-MAGIC-COOKIE-1", session.xauth_cookie]
			exec_nopipe(xauth_cmd, wait=True, env=session.get_env())
			self.sdebug("added xauth cookie %s for %s" % (session.xauth_cookie, location), session, user)

	def remove_xauth(self, session):
		extra_env = {"DISPLAY":session.display}
		xauth_cmd = ["xauth", "remove", session.display]
		exec_nopipe(xauth_cmd, wait=True, extra_env=extra_env)

	def create_xauth(self, session):
		#SELinux can block access to xauth file...
		#try to workaround it by using the global XAUTHORITY if there is one
		#see: http:/.winswitch.devloop.org.uk/trac/ticket/107
		session.xauth_file = os.environ.get("XAUTHORITY")
		if self.use_global_xauth and session.xauth_file:
			pass
		elif self.selinux_enforcing:
			if not session.xauth_file:
				self.serror("SELinux is enabled and may prevent the software from using xauth - if the session fails, this is probably why")
			else:
				self.serror("SELinux is enabled, working around it by using the global XAUTHORITY file %s" % session.xauth_file)
		else:
			session.xauth_file = get_session_xauth_filename(session.display, session.user)
			save_binary_file(session.xauth_file, "")
		self.sdebug("xauth_file=%s" % session.xauth_file, session)


	def get_randr_command(self, session):
		self.sdebug(None, session)
		parsed = parse_screensize(session.screen_size)
		if not parsed:
			return	None
		(w,h,_) = parsed
		return "xrandr -s %dx%d" % (w,h)


	def get_X_geometry_args(self, screen_size):
		"""
		Converts a screen spec string (ie: 640x480x16) into arguments that can be used with Xvnc or Xnest:
		-geometry 640x480 -depth 16
		The arguments are returned as an array of strings (if any - an empty array otherwise)
		"""
		if not screen_size:
			return []
		screen_spec = parse_screensize(screen_size)
		if not screen_spec:
			return []
		(w,h,d) = screen_spec
		args = ["-geometry", "%dx%d" % (w,h)]
		if d>0:
			args += ["-depth", "%d" % d]
		return args



	#***************************************************************************************************
	# Sound / print / dbus
	def start_dbus(self, session):
		cmd = self.config.dbus_command					#ie: "dbus-daemon --fork --print-address=1 --print-pid=1 --session"
		self.sdebug("dbus_command=%s" % cmd)
		if not cmd:
			return
		code, out =  -1, None
		try:
			import shlex
			dbus_command = shlex.split(cmd)
			code, out, _ = get_output(dbus_command)
		except Exception, e:
			self.serr("failed to start cmd='%s'" % cmd, e, session)
			return
		self.sdebug("output(%s)=(%s,%s)" % (cmd, code, out), session)
		if code!=0 or not out:
			self.serror("dbus daemon failed. code=%s, output=%s" % (code, out), session)
			return
		lines = out.splitlines()
		if len(lines)!=2:
			self.serror("expected 2 lines of output from dbus daemon but received %s: %s" % (len(lines), csv_list(lines)), session)
			return
		session.dbus_address = lines[0]
		session.dbus_pid = int(lines[1])
		session.onexit_kill_pids["10-dbus"] = session.dbus_pid
		self.slog("dbus_pid=%s, dbus_address=%s" % (session.dbus_pid, session.dbus_address), session)
		if self.disable_gnome_screensaver:
			""" No rush: this can run a little while later: """
			callLater(10, self.do_disable_gnome_screensaver, session)
		if self.disable_xscreensaver and is_valid_exe(XSCREENSAVER_COMMAND_COMMAND):
			callLater(10, self.do_disable_xscreensaver, session)

	def do_disable_xscreensaver(self, session):
		if session.status==Session.STATUS_CLOSED:
			self.slog("session already closed (failed?)", session)
			return
		self.sdebug(None, session)
		cmd = [XSCREENSAVER_COMMAND_COMMAND, "-exit"]
		exec_nopipe(cmd, env=session.get_env())

	def do_disable_gnome_screensaver(self, session):
		if session.status==Session.STATUS_CLOSED:
			self.slog("session already closed (failed?)", session)
			return
		self.sdebug(None, session)
		# The information on how to disable the screensaver is conflicting and just a PITA to use
		# Some pointers:
		# http://live.gnome.org/GnomePowerManager/DbusInterface
		# https://bbs.archlinux.org/viewtopic.php?pid=321490
		# http://lists.freedesktop.org/archives/portland-bugs/2010-August/000405.html
		# This may also work?:
		# dbus-send --session --dest=org.gnome.ScreenSaver --type=method_call /org/gnome/ScreenSaver org.gnome.ScreenSaver.SimulateUserActivity
		cmd = ["dbus-send", "--session", "--dest=org.gnome.ScreenSaver",
				"--print-reply",
				"--type=method_call", "--reply-timeout=20000", #"--print-reply",
				"/org/gnome/ScreenSaver", "org.gnome.ScreenSaver.Inhibit",
				'string:"%s"' % APPLICATION_NAME, 'string:"Virtual Session"']
		logfilename = self.do_get_log_file(session, "dbus-send-disable-gnome-screensaver")
		start_daemon(logfilename, cmd, env=session.get_env())

	def set_orbit_dir(self, session):
		orbit_dir = os.path.join(self.get_session_dir(session), "pulse-%s" % session.display[1:])
		if not is_valid_dir(orbit_dir):
			os.mkdir(orbit_dir)
		session.orbit_dir = orbit_dir

	def start_gnome_settings_daemon(self, session):
		logfilename = self.do_get_log_file(session, "gnome-settings-daemon")
		pid = start_daemon(logfilename, [GNOME_SETTINGS_DAEMON, "--no-daemon"], env=session.get_env(), cwd=session.get_cwd())
		self.slog("logging to %s, pid=%s" % (logfilename, pid), session)
		session.onexit_kill_pids["15-gnome-settings-daemon"] = pid

	def start_gnome_keyring_daemon(self, session):
		gk_dir = mksubdir(self.get_session_dir(session), "gnome-keyring", mode=RWX)
		self.sdebug("listdir(%s)=%s" % (gk_dir, os.listdir(gk_dir)), session)
		#cmd = ["gnome-keyring-daemon", "-s", "--components=pkcs11,secrets,ssh,gpg", "-d", "-C", gk_dir]
		cmd = ["gnome-keyring-daemon", "-s", "-d", "-C", gk_dir]
		code, out, err = get_output(cmd, env=session.get_env())
		if code!=0:
			self.serror("error running %s: %s, stderr=%s" % (cmd, code, err), session)
			return
		# parse the output:
		d = {}
		for line in out.splitlines():
			if line.startswith("**"):
				self.slog("warning: %s" % line, session)
				continue
			kv = line.split("=")
			if len(kv)!=2:
				self.serror("invalid line: %s" % line, session)
				continue
			d[kv[0]] = kv[1]
		pid_str = d.get("GNOME_KEYRING_PID")
		if pid_str:
			try:
				session.onexit_kill_pids["40-gnome-keyring-daemon"] = int(pid_str)
			except:
				self.serror("invalid pid string: %s" % pid_str, session)
		session.gnome_keyring_env = d
		self.slog("gnome_keyring_env=%s, pid=%s" % (d, pid_str), session)

	def start_ipp(self, session):
		session.ipp_port = self.port_mapper.get_free_port(IPP_PORT_BASE)
		cupsd_config_file = get_session_cupsd_config_filename(session.display, session.user, USER_ID==0)
		ipp_socket = get_session_cupsd_socket_filename(session.display, session.user, USER_ID==0)
		ipp_temp = get_session_cupsd_temp_dir(session.display, session.user, USER_ID==0)
		cupsd_log = get_session_cupsd_log_filename(session.display, session.user, USER_ID==0)
		if not is_valid_dir(ipp_temp):
			os.mkdir(ipp_temp)
		access_log = os.path.join(ipp_temp, "access_log")
		error_log = os.path.join(ipp_temp, "error_log")
		page_log = os.path.join(ipp_temp, "page_log")
		cache_dir = os.path.join(ipp_temp, "cache")
		if not is_valid_dir(cache_dir):
			os.mkdir(cache_dir)
		config = """
LogLevel debug
SystemGroup sys root
Listen localhost:%s
Browsing Off
HostNameLookups Off
DefaultAuthType Basic
KeepAlive Yes
MaxClients 5
Listen %s
# ServerRoot %s
# RequestRoot %s
TempDir %s
AccessLog %s
AccessLogLevel all
ErrorLog %s
PageLog %s
CacheDir %s
Printcap
RemoteRoot root
UseNetworkDefault yes
#DefaultShared yes
# (un)restrict access to the server...
DefaultPolicy default
<Policy default>
	<Limit ALL>
		AuthType None
	</Limit>
</Policy>
<Location />
	Order allow,deny
	Allow from All
</Location>
""" % (session.ipp_port, ipp_socket, ipp_temp, ipp_temp, ipp_temp, access_log, error_log, page_log, cache_dir)
		save_binary_file(cupsd_config_file, config)
		args = [self.config.cupsd_command, "-f", "-c", cupsd_config_file]
		session.ipp_pid = start_daemon(cupsd_log, args, env=session.get_env(), cwd=session.get_cwd())
		if session.ipp_pid>0:
			session.onexit_kill_pids["20-ipp"] = session.ipp_pid

	def start_sound_server(self, session):
		"""
		Start a new sound server for this session.
		"""
		self.sdebug("existing pulse_pid=%s, pulseaudio_command=%s" % (session.pulse_pid, self.config.pulseaudio_command), session)
		if not self.config.supports_sound or (not self.config.tunnel_sink and not self.config.tunnel_source):
			return
		if session.pulse_pid>0:
			return
		if not is_valid_file(self.config.pulseaudio_command):
			return
		socket_dir = "/tmp/pulse-%s" % session.display[1:]
		#unfortunately, we can't place the sockets in a private place? :(
		#socket_dir = os.path.join(self.get_session_dir(session), "pulse-%s" % session.display[1:])
		if not is_valid_path(socket_dir):
			os.mkdir(socket_dir)
		elif not is_valid_dir(socket_dir):
			self.serror("location for pulse directory '%s' exists but is not a valid directory!" % socket_dir, session)
			return
		socket = "%s/native" % socket_dir
		logfile = get_session_pulseaudio_log_filename(session.display, session.user, USER_ID==0)
		if is_valid_file(logfile):
			delete_if_exists(logfile)
		args = [self.config.pulseaudio_command, "--start",
				"-vvvv",
				"--disable-shm=true",
				"--daemonize=false",		#we do this ourselves after grabbing the pid
				"--use-pid-file=false",
				"--system=false",
				"--exit-idle-time=-1",
				"-n",
				"--load=module-suspend-on-idle",
				"--load=module-null-sink",
				"--load=module-native-protocol-unix socket=%s" % socket
				]

		pulse_home = self.get_session_dir(session)
		# generate a cookie:
		def make_cookie():
			c = ""
			while len(c)<256:
				c += generate_UUID()
			return	c[:256]
		cookie = make_cookie()
		cookie_file = os.path.join(pulse_home, ".pulse-cookie")
		save_binary_file(cookie_file, cookie)
		# start the daemon
		env = session.get_env()
		env["HOME"] = pulse_home
		pa_pid = start_daemon(logfile, args, env=env)
		self.sdebug("pulseaudio pid=%s" % pa_pid, session)
		if pa_pid>0:
			session.pulse_pid = pa_pid
			session.onexit_kill_pids["00-pulse"] = session.pulse_pid
			session.pulse_address = "unix:%s" % socket
			session.gst_export_plugin = "pulsesrc"
			session.gst_export_plugin_options = {"server": session.pulse_address}
			session.gst_import_plugin = "pulsesink"
			session.gst_import_plugin_options = {"server": session.pulse_address}
			session.gst_clone_plugin = ""
			session.gst_clone_plugin_options = {}
			#session.add_status_update_callback(self, from_status, to_status, callback, clear_it=True, timeout=30):
			def suspend_stop_sound(*args):
				self.disconnect_sound_client(session)
			session.do_add_status_update_callback([Session.STATUS_CONNECTED,Session.STATUS_IDLE], [Session.STATUS_SUSPENDING,Session.STATUS_SUSPENDED], suspend_stop_sound, clear_it=True, timeout=None)


	def disconnect_sound_client(self, session):
		self.sdebug(None, session)


	#***************************************************************************************************
	# Screen capture
	def can_capture(self, session):
		if session.shadowed_display:
			return	False				#when shadowing a display it is redundant - the client can show the screenshot from the main session instead
		return session.status!=Session.STATUS_CLOSED

	def can_capture_now(self, session):
		return session.status!=Session.STATUS_STARTING		#when the session is starting up, best not to disturb it

	def capture_display(self, session, ok_callback, err_callback):
		""" Captures the session's screen, resizing it to a thumbnail.
			Then loads the file into the session's screen_capture_icon_data """
		if not self.can_capture(session):
			err_callback("cannot capture %s" % session)
			return
		if not self.can_capture_now(session):
			err_callback("cannot capture %s at the moment.." % session)
			return
		filename = get_session_snapshot_filename(session.display, session.user)
		env = session.get_env()
		del env["DISPLAY"]
		def screenshot_ok(data):
			session.set_screen_capture_icon_data(data)
			ok_callback()
		self.take_screenshot(session.ID, session.display, env, filename, screenshot_ok, err_callback)
