#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Author: Andreas Büsching <crunchy@bitkipper.net>
#
# Copyright (C) 2004, 2005, 2006
#	Andreas Büsching <crunchy@bitkipper.net>
#
# This library is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version
# 2.1 as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""child process control using notifier."""

__all__ = [ 'Process', 'RunIt', 'Shell' ]

# python imports
import os
import fcntl
import popen2
import glob
import re
import shlex
import logging
import types

# notifier imports
import notifier
import signals
import log

_processes = []

class Process( signals.Provider ):
	"""
	Base class for starting child processes and monitoring standard
	output and error.
	"""
	def __init__( self, cmd, stdout = True, stderr = True ):
		""" Init the child process 'cmd'. This can either be a string or a list
		of arguments (similar to popen2). stdout and stderr of the child
		process can be handled by connecting to the signals 'stdout'
		and/or 'stderr'. The signal functions have one argument that is
		of type list and contains all new lines. By setting one of the
		boolean arguments 'stdout' and 'stderr' to False the monitoring
		of these files can be deactivated.


		Signals:
		'stderr' ( pid, line )
			emmited when the IO_Handler reported another line of input
			(from stdout).
			pid			: PID of the process that produced the output
			line		: line of output
		'stdout' ( pid, line )
			emmited when the IO_Handler reported another line of input
			(from stderr).
			pid			: PID of the process that produced the output
			line		: line of output
		"""
		signals.Provider.__init__( self )
		if stderr: self.signal_new( 'stderr' );
		if stdout: self.signal_new( 'stdout' );
		self.signal_new( 'killed' );

		self._cmd = self._normalize_cmd(cmd)
		if self._cmd:
			self._name = self._cmd[ 0 ].split( '/' )[ -1 ]
		else:
			self._name = '<unknown>'
		self.stopping = False
		self.pid = None
		self.child = None

		self.__dead = True
		self.__kill_timer = None

		global _processes
		if not _processes:
			notifier.dispatcher_add( _watcher )
		_processes.append( self )

	def _normalize_cmd( self, cmd ):
		"""
		Converts a command string into a list while honoring quoting or
		removes empty strings if the cmd is a list.
		"""
		if cmd == None:
			return []
		elif type( cmd ) == list:
			# Remove empty strings from argument list.
			while '' in cmd:
				cmd.remove( '' )
			return cmd

		assert( type( cmd ) in types.StringTypes )

		cmdlist = shlex.split( str( cmd ) )

		return cmdlist

	def _read_stdout( self, line ):
		"""emit signal 'stdout', announcing that there is another line
		of output"""
		self.signal_emit( 'stdout', self.pid, line )

	def _read_stderr( self, line ):
		"""emit signal 'stdout', announcing that there is another line
		of output"""
		self.signal_emit( 'stderr', self.pid, line )

	def start( self, args = None ):
		"""
		Starts the process.	 If args is not None, it can be either a list or
		string, as with the constructor, and is appended to the command line
		specified in the constructor.
		"""
		if not self.__dead:
			raise SystemError, "process is already running."
		if self.stopping:
			raise SystemError, "process is currently dying."

		cmd = self._cmd + self._normalize_cmd( args )
		self.__kill_timer = None
		self.__dead = False
		self.binary = cmd[ 0 ]

		self.stdout = self.stderr = None
		if not self.signal_exists( 'stdout' ) and \
			   not self.signal_exists( 'stderr' ):
			self.pid = os.spawnvp( os.P_NOWAIT, self.binary, cmd )
		else:
			self.child = popen2.Popen3( cmd, True, 1000 )
			self.pid = self.child.pid

			if self.signal_exists( 'stdout' ):
				# IO_Handler for stdout
				self.stdout = IO_Handler( 'stdout', self.child.fromchild,
										  self._read_stdout, self._name )
				self.stdout.signal_connect( 'closed', self._closed )

			if self.signal_exists( 'stderr' ):
				# IO_Handler for stderr
				self.stderr = IO_Handler( 'stderr', self.child.childerr,
										  self._read_stderr, self._name )
				self.stderr.signal_connect( 'closed', self._closed )

		log.info( 'running %s (pid=%s)' % ( self.binary, self.pid ) )

		return self.pid

	def dead( self, pid, status ):
		self.__dead = True
		self.signal_emit( 'killed', pid, status )

	def _closed( self, name ):
		if name == 'stderr':
			self.stderr = None
		elif name == 'stdout':
			self.stdout = None

		if not self.stdout and not self.stderr:
			try:
				pid, status = os.waitpid( self.pid, os.WNOHANG )
				if pid:
					self.dead( pid, status )
			except OSError: # already dead and buried
				pass

	def write( self, line ):
		"""
		Pass a string to the process
		"""
		try:
			self.child.tochild.write( line )
			self.child.tochild.flush()
		except ( IOError, ValueError ):
			pass

	def is_alive( self ):
		"""
		Return True if the process is still running
		"""
		return not self.__dead

	def stop( self ):
		"""
		Stop the child. Tries to kill the process with signal 15 and after that
		kill -9 will be used to kill the app.
		"""
		if self.stopping:
			return

		self.stopping = True

		if self.is_alive() and not self.__kill_timer:
			cb = Callback( self.__kill, 15 )
			self.__kill_timer = notifier.timer_add( 0, cb )

	def __kill( self, signal ):
		"""
		Internal kill helper function
		"""
		if not self.is_alive():
			self.__dead = True
			self.stopping = False
			return False
		# child needs some assistance with dying ...
		try:
			os.kill( self.pid, signal )
		except OSError:
			pass

		if signal == 15:
			cb = Callback( self.__kill, 9 )
		else:
			cb = Callback( self.__killall, 15 )

		self.__kill_timer = notifier.timer_add( 3000, cb )
		return False

	def __killall( self, signal ):
		"""
		Internal killall helper function
		"""
		if not self.is_alive():
			self.__dead = True
			self.stopping = False
			return False
		# child needs some assistance with dying ...
		try:
			# kill all applications with the string <appname> in their
			# commandline. This implementation uses the /proc filesystem,
			# it is Linux-dependent.
			unify_name = re.compile( '[^A-Za-z0-9]' ).sub
			appname = unify_name( '', self.binary )

			cmdline_filenames = glob.glob( '/proc/[0-9]*/cmdline' )

			for cmdline_filename in cmdline_filenames:
				try:
					fd = open( cmdline_filename )
					cmdline = fd.read()
					fd.close()
				except IOError:
					continue
				if unify_name( '', cmdline ).find( appname ) != -1:
					# Found one, kill it
					pid = int( cmdline_filename.split( '/' )[ 2 ] )
					try:
						os.kill( pid, signal )
					except:
						pass
		except OSError:
			pass

		log.info( 'kill -%d %s' % ( signal, self.binary ) )
		if signal == 15:
			cb = Callback( self.__killall, 9 )
			self.__kill_timer = notifier.timer_add( 2000, cb )
		else:
			log.critical( 'PANIC %s' % self.binary )

		return False

def _watcher():
	global _processes
	finished = []

	for proc in _processes:
		try:
			pid, status = os.waitpid( proc.pid, os.WNOHANG )
			if pid:
				proc.dead( pid, status )
				finished.append( proc )
		except OSError: # already dead and buried
			finished.append( proc )

	for i in finished:
		_processes.remove( i )

	return ( len( _processes ) > 0 )

class IO_Handler( signals.Provider ):
	"""
	Reading data from socket (stdout or stderr)

	Signals:
	'closed' ( name )
		emmited when the file was closed.
		name			: name of the IO_Handler
	"""
	def __init__( self, name, fp, callback, logger = None ):
		signals.Provider.__init__( self )

		self.name = name
		self.fp = fp
		fcntl.fcntl( self.fp.fileno(), fcntl.F_SETFL, os.O_NONBLOCK )
		self.callback = callback
		self.saved = ''
		notifier.socket_add( fp, self._handle_input )
		self.signal_new( 'closed' )

	def close( self ):
		"""
		Close the IO to the child.
		"""
		notifier.socket_remove( self.fp )
		self.fp.close()
		self.signal_emit( 'closed', self.name )

	def _handle_input( self, socket ):
		"""
		Handle data input from socket.
		"""
		try:
			self.fp.flush()
			data = self.fp.read( 10000 )
		except IOError, (errno, msg):
			if errno == 11:
				# Resource temporarily unavailable; if we try to read on a
				# non-blocking descriptor we'll get this message.
				return True
			data = None

		if not data:
			self.close()
			return False

		data  = data.replace('\r', '\n')
		lines = data.split('\n')
		# Only one partial line?
		if len(lines) == 1:
			self.saved += data
			return True

		# Combine saved data and first line, send to app
		self.callback( self.saved + lines[ 0 ] )
		self.saved = ''

		# There's one or more lines + possibly a partial line
		if lines[ -1 ] != '':
			# The last line is partial, save it for the next time
			self.saved = lines[ -1 ]

			# Send all lines except the last partial line to the app
			self.callback( lines[ 1 : -1 ] )
		else:
			# Send all lines to the app
			self.callback( lines[ 1 : ] )

		return True

class RunIt( Process ):
	"""Is a more simple child process handler based on Process that
	caches the output and provides it to the caller with the signal
	'finished'.

	Signals:
	'finished' ( pid, status[, stdout[, stderr ] ] )
		emmited when the child process is dead.
		pid				: process ID
		status			: exit code of the child process
		stdout, stderr	: are only provided when stdout and/or stderr is
						  monitored
		"""
	def __init__( self, command, stdout = True, stderr = False ):
		Process.__init__( self, command, stdout = stdout, stderr = stderr )
		if stdout:
			self.__stdout = []
			cb = notifier.Callback( self._output, self.__stdout )
			self.signal_connect( 'stdout', cb )
		else:
			self.__stdout = None
		if stderr:
			self.__stderr = []
			cb = notifier.Callback( self._output, self.__stderr )
			self.signal_connect( 'stderr', self._stderr )
		else:
			self.__stderr = None
		self.signal_connect( 'killed', self._finished )
		self.signal_new( 'finished' )

	def _output( self, pid, line, buffer ):
		if isinstance( line, list ):
			buffer.extend( line )
		else:
			buffer.append( line )

	def _finished( self, pid, status ):
		if self.__stdout != None:
			if self.__stderr == None:
				self.signal_emit( 'finished', pid, os.WEXITSTATUS( status ),
								  self.__stdout )
			else:
				self.signal_emit( 'finished', pid, os.WEXITSTATUS( status ),
								  self.__stdout, self.__stderr )
		else:
			self.signal_emit( 'finished', pid, os.WEXITSTATUS( status ) )

class Shell( RunIt ):
	"""A simple interface for running shell commands as child processes"""
	BINARY = '/bin/sh'
	def __init__( self, command, stdout = True, stderr = False ):
		cmd = [ Shell.BINARY, '-c' ]
		if type( command ) in types.StringTypes:
			cmd.append( command )
		elif type( command ) in ( types.ListType, types.TupleType ):
			cmd.append( ' '.join( command ) )
		RunIt.__init__( self, cmd, stdout = stdout, stderr = stderr )


syntax highlighted by Code2HTML, v. 0.9.1