#!/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 time
from collections import deque

from winswitch.consts import X11_TYPE, X_PORT_BASE, ROOT_WINDOW_SESSIONID_PROPERTY, ROOT_WINDOW_XPRA_SERVER_PROPERTY
from winswitch.globals import USERNAME, USER_ID, OSX
from winswitch.objects.session import Session
from winswitch.objects.server_settings import DEFAULT_UTMP_DETECT_BATCH_DELAY
from winswitch.util.common import is_valid_file, csv_list, CAN_USE_GIO
from winswitch.virt.server_util_base import ServerUtilBase
from winswitch.util.main_loop import callLater

UTMP_TRAFFIC_LIMIT = 10			#more than 10 entries per minute and we will start slowing down

from winswitch.virt.utmp_wrapper import getutents, SYS_WTMP_FILE


class	LocalX11ServerUtil(ServerUtilBase):

	def	__init__(self, config, add_session, remove_session, update_session_status, session_failed):
		ServerUtilBase.__init__(self, X11_TYPE, X_PORT_BASE, config, add_session, remove_session, update_session_status, session_failed)
		self.prelaunch_enabled = False
		self.detect_local_displays_pending = False
		self.wtmp_monitor = None
		self.wtmp_traffic = deque([], UTMP_TRAFFIC_LIMIT)
		self.display0_session_name = "Main Unix Display"
		self.display_session_name_prefix = "Unix Display "
		self.max_x11_display_number = 10
		self.failed_wtmp_entries = []

	def get_config_options(self):
		return	self.get_config_options_base(detect=True)+[
						"# the name of the session for display :0",
						"display0_session_name",
						"# the prefix for session names (the display number will be appended to it)",
						"display_session_name_prefix",
						"# x11 display numbers higher than this value will be ignored",
						"max_x11_display_number"]

	def	can_send_session(self, session, user):
		""" X11 sessions can never be sent! (but you can shadow them..) """
		return	False

	def detect_existing_sessions(self, reloaded_sessions):
		#delete existing files:
		ServerUtilBase.zap_existing_sessions(self, reloaded_sessions)
		#detect local displays if enabled:
		if not self.config.export_local_displays or OSX:
			return
		if not getutents:
			self.serror("cannot detect local X11 displays without utmp support (either python-utmp or pyutmp)")
			return
		self.slog("found getutents=%s, we should be able to detect local displays" % getutents)
		self.detect_local_displays()
		if not CAN_USE_GIO:
			self.slog("gio is disabled, will use sub-optimal polling for %s" % SYS_WTMP_FILE)
			def poll_wtmp():
				self.wtmp_modified()
				callLater(30, poll_wtmp)
			callLater(10, poll_wtmp)
		elif is_valid_file(SYS_WTMP_FILE):
			import gio
			gfile = gio.File(SYS_WTMP_FILE)
			mfile = gfile.monitor_file()
			mfile.connect("changed", self.wtmp_modified)
			self.wtmp_monitor = mfile
			self.sdebug("watching for wtmp traffic using %s" % mfile)

	def add_local_user(self, username, uuid):
		"""
		Override so we can detect local displays.
		"""
		self.sdebug(None, username)
		ServerUtilBase.add_local_user(self, username, uuid)
		self.schedule_detect_local_displays()

	def remove_user(self, uuid):
		"""
		Override so we can check to see if the local display owner has changed (display may even have gone completely)
		"""
		self.sdebug(None, uuid)
		ServerUtilBase.remove_user(self, uuid)
		self.schedule_detect_local_displays()


	def wtmp_modified(self, *args):
		"""
		Whenever utmp is modified, we try to detect local displays as one may have been added or removed.
		The schedule_detect_local_displays will batch the detection,
		either using the specified config.utmp_batch_delay or auto-tuning if the value is negative.
		"""
		self.sdebug(None, *args)
		now = time.time()
		delay_ms = self.config.utmp_batch_delay
		if delay_ms<=0:
			delay_ms = DEFAULT_UTMP_DETECT_BATCH_DELAY
			if len(self.wtmp_traffic)>=UTMP_TRAFFIC_LIMIT:
				last = self.wtmp_traffic[0]
				diff_time = now-last
				if diff_time<10:
					delay_ms = 10*delay_ms		#lots of traffic, let's slow down a bit!
				elif diff_time<30:
					delay_ms = 5*delay_ms
				else:
					pass	#OK: the UTMP_TRAFFIC_LIMIT items took longer than 60 seconds to record
		self.wtmp_traffic.append(now)
		self.schedule_detect_local_displays(delay_ms=delay_ms)


	def schedule_detect_local_displays(self, delay_ms=DEFAULT_UTMP_DETECT_BATCH_DELAY):
		self.sdebug("detect_local_displays_pending=%s" % self.detect_local_displays_pending, delay_ms)
		if not self.detect_local_displays_pending and getutents:
			self.detect_local_displays_pending = True
			callLater(float(delay_ms)/1000, self.detect_local_displays)

	def detect_local_displays(self):
		if not getutents:
			return
		self.sdebug()
		self.detect_local_displays_pending = False
		local_sessions = self.config.get_sessions_by_type(X11_TYPE)
		not_found = local_sessions[:]
		displays = {}
		try:
			recs = getutents()
			if not recs:
				self.slog("no records found using getutents=%s" % getutents)
				return False
			self.sdebug("%s()=%s" % (getutents, csv_list(recs)))
			for ute in recs:
				def X11_display(display):
					if not display:
						return	None
					if display.startswith("/") or display.startswith("tty"):	#plain tty
						self.sdebug("skipped plain tty %s" % ute, display)
						return	None
					if display.find("pts")>=0:			#pseudo tty
						self.sdebug("skipped pts %s" % ute, display)
						return	None
					if display.find(".")>0:			#ie: 0.0
						#strip it:
						display = display[:display.find(".")]
					if not display.startswith(":"):
						self.sdebug("utmp entry %r does not look like a display or tty" % ute, display)
						return	None
					return display

				#self.sdebug("testing %s" % ute)
				if not ute.ut_user or (USER_ID!=0 and USERNAME!=ute.ut_user):
					self.sdebug("session %s ignored (owned by %s)" % (ute, ute.ut_user))
					continue
				display = X11_display("%s" % ute.ut_id) or X11_display("%s" % ute.ut_host)
				if display and display not in displays:
					existing_entry = displays.get(display)
					if existing_entry:
						(_, _, etime) = existing_entry
						if etime>ute.ut_time:
							self.sdebug("found newer existing entry %s for display %s" % (existing_entry, display))
							continue
					entry = ute.ut_user, ute.ut_pid, ute.ut_time
					displays[display] = entry

			self.sdebug("testing: %s" % displays)
			for display, entry in displays.items():
				if entry in self.failed_wtmp_entries:
					self.sdebug("ignoring unchanged failed wtmp entry for display %s: %s" % (display, entry))
					continue
				ut_user, ut_pid, ut_time = entry
				found = False
				for sess in local_sessions:
					if sess.display==display:
						found = True
						mod = False
						owner = self.get_local_display_owner(display)
						if sess.owner!=owner:
							self.slog("found new owner ('%s' was '%s') for display %s, updating clients" % (owner, sess.owner, display))
							sess.owner = owner
							mod = True
						if not sess.screen_size:
							sess.screen_size = self.get_display_size(display)
							self.slog("updated screen size to %s" % sess.screen_size)
							mod = True
						if not sess.command:
							sess.command = self.get_display_command(display, ute.ut_pid)
							if sess.command:
								self.slog("updated command to %s" % sess.command)
								sess.set_window_icon_data(self.get_display_icon_data(sess.command))
								mod = True
						if mod:
							sess.touch()
							self.add_session(sess)			#send the updated session info to all clients
						if sess in not_found:
							not_found.remove(sess)
				if not found:
					session = self.found_display(ut_user, display, ut_pid, ut_time)
					if not session:
						self.slog("wtmp entry %s marked as failed - will not be retried for one hour" % str(entry))
						def clear_failed_entry(entry):
							if entry in self.failed_wtmp_entries:
								self.failed_wtmp_entries.remove(entry)
						self.failed_wtmp_entries.append(entry)
						callLater(60*60, clear_failed_entry, entry)
		except Exception, e:
			self.serr(None, e)
		#clear old sessions:
		for sess in not_found:
			self.sdebug("session %s has disappeared" % sess)
			self.update_session_status(sess, Session.STATUS_CLOSED)
			#self.remove_session(sess)
		return	False


	def found_display(self, username, display, pid, start_time):
		"""
		If this is a normal X11 display that we can access (and therefore export/shadow)
		then create a new session object for it.
		"""
		self.sdebug(None, username, display, pid, start_time)
		try:
			display_no = int(display[1:])
		except Exception, e:
			self.serror("failed to parse display number: '%s', %s" % (display, e), username, display, pid, start_time)
			return	None
		if self.max_x11_display_number>0 and display_no>=self.max_x11_display_number:
			return	None
		if not username:
			return	None
		if not self.can_access_local_display(display_no):
			return	None
		xpra_session_marker = self.xprop_get(display, ROOT_WINDOW_XPRA_SERVER_PROPERTY)
		if xpra_session_marker:
			return	None
		display_size = self.get_display_size(display)
		session = self.initialize_new_session(username, None, False, display_size, display)
		session_id = self.xprop_get(display, ROOT_WINDOW_SESSIONID_PROPERTY)
		if session_id:
			session.ID = session_id					#re-use the session.ID already set for this display
		else:
			self.xprop_set(display, ROOT_WINDOW_SESSIONID_PROPERTY, session.ID)		#store generated session.ID as a root window property
		try:
			start_time = int(start_time)
		except:
			start_time = 0			#start_time may be missing... (if using 'last' for example)
		session.start_time = start_time
		session.command = self.get_display_command(display, pid)
		session.set_window_icon_data(self.get_display_icon_data(session.command))
		try:
			import pwd
			name = pwd.getpwnam(username).pw_gecos
			if not name:
				name = username
			session.name = "%s' Desktop" % name
		except:
			if display_no==0:
				session.name = self.display0_session_name
			else:
				session.name = self.display_session_name_prefix + str(display)
		session.owner = self.get_local_display_owner(display)
		session.actor = session.owner
		session.full_desktop = True
		session.shared_desktop = False
		session.session_type = X11_TYPE
		session.status = Session.STATUS_AVAILABLE
		#sound stuff:
		session.pulse_address = self.get_local_pulse_server(session.display)
		session.sound_clone_device = self.get_clone_device(session.display)
		session.can_clone_sound = self.config.tunnel_clone and session.sound_clone_device is not None
		session.can_export_sound = self.config.tunnel_sink and session.pulse_address is not None
		session.can_import_sound = self.config.tunnel_source and session.pulse_address is not None
		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 = {}
		if session.sound_clone_device:
			session.gst_clone_plugin = "pulsesrc"
			session.gst_clone_plugin_options = {"server": session.pulse_address, "device":session.sound_clone_device}
		session.last_updated = time.time()
		self.save_session(session)
		self.config.add_session(session)
		self.add_session(session)
		self.sdebug("added X11 session %s, owned by %s, size=%s, started %s seconds ago" % (session.ID, username, display_size, (time.time()-start_time)), username, display, pid, start_time)
		return	session


def detect():
	from winswitch.util.main_loop import loop_exit
	from winswitch.objects.server_settings import ServerSettings
	from winswitch.util.config import get_local_server_config
	from winswitch.util.simple_logger import Logger
	logger = Logger("localX11_server_util")
	ss = get_local_server_config() or ServerSettings()
	def add_session(session):
		logger.sdebug("start_time=%s (%s seconds ago), command=%s" % (session.start_time, time.time()-session.start_time, session.command), session)
	lx11 = LocalX11ServerUtil(ss, add_session, None, None, None)
	lx11.detect_local_displays()
	loop_exit()

def main():
	from winswitch.util.main_loop import test_loop
	test_loop(detect)


if __name__ == "__main__":
	main()
