from contextlib import contextmanager
import struct
import subprocess
import pickle
import ssl
import logging
import logging.config
import yaml
import sys
import os
import errno
import SocketServer
import ConfigParser

try:
	with open('logging.config') as f:
		logging.config.dictConfig(yaml.load(f))
	logging.captureWarnings(True)
except Exception as e:
	print (
		"Failed to load logging configuration: %s. "
		"No logging will be performed." % (
			str(e)
			)
	)

logger = logging.getLogger('parallels')
logger.info("Start Parallels Panel Transfer agent")

logger.info("Read configuration")
config_parser = ConfigParser.ConfigParser()
config_filename = 'config.ini'


@contextmanager
def log_failure(message):
	try:
		yield
	except Exception:
		logger.error(message)
		logger.debug("Exception: ", exc_info=True)
		raise


class ServerConfiguration(object):
	def __init__(self):
		self.use_ssl = False
		self.client_cert = None
		self.server_cert = None
		self.server_key = None
		self.port = None

	def __repr__(self):
		return (
			"ServerConfiguration("
			"use_ssl={use_ssl}, "
			"client_cert={client_cert}, "
			"server_cert={server_cert}, "
			"server_key={server_key}, "
			"port={port}"
			")".format(
				use_ssl=self.use_ssl, 
				client_cert=self.client_cert, 
				server_cert=self.server_cert, 
				server_key=self.server_key, 
				port=self.port
				)
		)

try:
	config_parser.read(config_filename)
	configuration = ServerConfiguration()
	configuration.use_ssl = config_parser.getboolean('GLOBAL', 'use-ssl')
	configuration.client_cert = config_parser.get('GLOBAL', 'client-cert')
	configuration.server_cert = config_parser.get('GLOBAL', 'server-cert')
	configuration.server_key = config_parser.get('GLOBAL', 'server-key')
	configuration.port = config_parser.getint('GLOBAL', 'port')
except Exception as e:
	logger.debug(u'Exception:', exc_info=e)
	logger.info("Failed to read configuration file '%s': %s", config_filename, e)
	sys.exit(1)

logger.debug("Server configuration: %r", configuration)


class LocalRunner(object):
	def sh_unchecked(self, cmd_str, args=None, stdin_content=None):
		if args is not None:
			cmd_str = self._format_command(cmd_str, **args)
		logger.debug("Execute command: %s", cmd_str)

		p = subprocess.Popen(cmd_str, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout_str, stderr_str = [
			s.decode('utf-8', 'strict') 
			for s in p.communicate(stdin_content)
		]
		logger.debug(u"Command stdout: %s" % stdout_str)
		logger.debug(u"Command stderr: %s" % stderr_str)
		logger.debug(u"Command exit code: %s" % p.returncode)
		return p.returncode, stdout_str, stderr_str

	@staticmethod
	def get_file_contents(remote_filename):
		logger.debug("Get contents of file '%s'", remote_filename)

		with open(remote_filename, 'rb') as fp:
			return fp.read()

	@staticmethod
	def upload_file_content(filename, contents):
		logger.debug("Put contents to file '%s'", filename)

		with open(filename, "wb") as fp:
			fp.write(contents)

	@staticmethod
	def mkdir(dirname):
		logger.debug("Create directory '%s'", dirname)

		try:
			os.makedirs(dirname)
		except OSError as exc:
			if exc.errno == errno.EEXIST:
				pass
			else:
				raise

	@staticmethod
	def remove_file(filename):
		logger.debug("Remove file '%s'", filename)

		try:
			os.remove(filename)
		except OSError as e:
			# if file does not exist - just skip
			if e.errno != errno.ENOENT:
				raise

	@staticmethod
	def file_exists(filename):
		logger.debug("Check if file '%s' already exists", filename)
		return os.path.exists(filename)

	def _format_command(self, template, *args, **kw):
		qargs = map(self._quote_arg, args)
		qkw = dict((k, self._quote_arg(unicode(v))) for k, v in kw.iteritems())
		return unicode(template).format(*qargs, **qkw)

	@staticmethod
	def _quote_arg(arg):
		return '"%s"' % arg.replace('"', '""')


class ConnectionHandler(SocketServer.BaseRequestHandler):
	def handle(self):
		client_address = self.client_address[0]
		logger.info("Connected by '%s'", client_address)
		runner = LocalRunner()
		try:
			while True:
				with log_failure("Failed to receive length of a command"):
					length = self._receive(4)
					if not length:
						break
					length, = struct.unpack("I", length)

				with log_failure("Failed to receive command"):
					command = self._receive(length)
					if not command:
						break

				with log_failure("Failed to unserialize command"):
					function, args, kwargs = pickle.loads(command)

				try:
					execution_result = getattr(runner, function)(*args, **kwargs)
				except:
					logger.error("Failed to execute received command for function '%s'" % function)
					logger.debug("Exception: ", exc_info=True)
					self.request.sendall(struct.pack("I", 0))
				else:
					with log_failure("Failed to serialize command execution results"):
						serialized_result = pickle.dumps(execution_result)

					with log_failure("Failed to send execution results back"):
						self.request.sendall(struct.pack("I", len(serialized_result)))
						self.request.sendall(serialized_result)
		finally:
			logger.info("Finished connection from '%s'", client_address)

	def _receive(self, size):
		b = ''
		while len(b) < size:
			r = self.request.recv(size - len(b))
			if not r:
				return b
			b += r
		return b


class AgentServer(SocketServer.TCPServer):
	def __init__(self, configuration):
		SocketServer.TCPServer.__init__(
			self,
			('', configuration.port), ConnectionHandler, 
			bind_and_activate=False
		)
		self.configuration = configuration
		if self.configuration.use_ssl:
			self.socket = ssl.wrap_socket(
				self.socket, server_side=True, 
				keyfile=self.configuration.server_key,
				certfile=self.configuration.server_cert, 
				cert_reqs=ssl.CERT_REQUIRED, 
				ca_certs=configuration.client_cert
			)

		self.server_bind()
		self.server_activate()

server = AgentServer(configuration)
server.serve_forever()
