#!/usr/bin/env python
# -*- coding: utf-8 -*-

####
log_path = '/var/log/sid.log'

dbus_name = 'net.fraggod.SID'
dbus_iface = 'net.fraggod.SID'
dbus_path = '/net/fraggod/SID'
####


from optparse import OptionParser
parser = OptionParser(usage='%prog [options]',
	description=("Watch over instances of apps, passed via dbus calls,"
		" just switching focus to them instead if they're still running."))
parser.add_option('-d', '--debug',
	action='store_true', dest='debug',
	help='print lots of debug info')
optz,argz = parser.parse_args()
if argz: parser.error('This command takes no arguments')


from fgc import log
log.cfg(level=(log.DEBUG if optz.debug else log.WARNING))



import itertools as it, operator as op, functools as ft
from dbus.mainloop.glib import DBusGMainLoop
import dbus, dbus.service, gobject
from threading import Lock
from fgc import exe, wm
import os, sys, signal

DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
bus_name = dbus.service.BusName(dbus_name, bus)



class SID(dbus.service.Object):

	_instance = None
	def __new__(cls, *argz, **kwz):
		if cls._instance is None:
			cls._instance = super(SID, cls).__new__(cls, *argz, **kwz)
		return cls._instance


	def __init__(self, *argz, **kwz):
		super(SID, self).__init__(*argz, **kwz)
		try: self.log = open(log_path, 'a')
		except (OSError, IOError), err:
			log.warn('Unable to open logfile ({0}): {1}'.format(log_path, err))
			self.log = open('/dev/null', 'w')
		log.add_stream(self.log, level=log.INFO, **log.date_format)
		log.info('SID watcher daemon started'.format(argz))

	@dbus.service.method( dbus_iface,
		in_signature='asa{ss}', out_signature='s' )
	def instance_request(self, argz, env):
		try:
			data = self.pop_instance(argz, env)
			return data if data else ''
		except Exception, err: return 'ERROR: {0}'.format(err)


	apps = dict()
	lock, lock_req = Lock(), False

	def pop_instance(self, argz, env):
		ps = argz[0]
		log.info('InstanceRequest: {0}'.format(argz))
		if ps[0] != '/': raise TypeError, 'App path must be absolute'
		ps = os.path.realpath(ps)
		log.debug('Pulling out "{0}"'.format(ps))
		try:
			app = self.apps[ps]
			log.debug('App "{0}" exists, pulling to fg'.format(ps))
			app.show()
		except KeyError:
			log.debug('No app "{0}", starting'.format(ps))
			self.apps[ps] = AppInstance(argz, env, self.log)
			return 'Started'

	def reap_apps(self, sig, frm):
		log.debug('Got app exit signal')
		try:
			locked = self.lock.acquire(False)
			self.lock_req = True # indicates that apps have to be re-checked
			if not locked:
				log.debug('Reap is in progress, re-check scheduled')
				return

			while self.lock_req:
				self.lock_req = False
				log.debug('Reaping dead apps')
				for k,app in self.apps.iteritems():
					if app.dead:
						del self.apps[k]
						log.debug('App "{0}" was released'.format(k))

		finally:
			if locked: self.lock.release()
			global loop_interrupt
			loop_interrupt = True
			log.debug('Reaper done')



class AppInstance(object):
	_id = None # for debugging purposes only
	_ps = _win = None

	def __init__(self, argz, env, logfile=False):
		log.debug('Creating instance with argz: {0}'.format(argz))
		self._id = argz[0]
		self._ps = exe.proc( *argz,
			preexec_fn=os.setsid, env=env,
			stdout=logfile, stderr=exe.STDOUT, stdin=False )

	def show(self):
		if self.windows:
			for win in self.windows: win.focus()
		else: log.debug('No window for app "{0}"'.format(self._id))

	@property
	def windows(self):
		if self._win is None:
			self._win = wm.Window.by_pid(self._ps.pid)
			if self._win: self._win = list(self._win) # all windows for pid
			else: self._win = False
		return self._win

	@property
	def dead(self):
		return self._ps.wait(0) is not None



sid = SID(bus, dbus_path)
signal.signal(signal.SIGCHLD, sid.reap_apps)

loop = gobject.MainLoop()
loop_interrupt = False
log.debug('Starting gobject loop')

while True:
	try: loop.run()
	except KeyboardInterrupt:
		if not loop_interrupt: raise
		log.debug('Got loop interruption')
		loop_interrupt = False
