# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
#
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"

import os
import sys

from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, \
	logSys, PRODUCTION, exit

SERVER = "fail2ban-server"

##
# \mainpage Fail2Ban
#
# \section Introduction
#
class Fail2banServer(Fail2banCmdLine):

	# def __init__(self):
	# 	Fail2banCmdLine.__init__(self)

	##
	# Start Fail2Ban server in main thread without fork (direct, it can fork itself in Server if daemon=True).
	#
	# Start the Fail2ban server in background/foreground (daemon mode or not).

	@staticmethod
	def startServerDirect(conf, daemon=True, setServer=None):
		logSys.debug("  direct starting of server in %s, daemon: %s", os.getpid(), daemon)
		from ..server.server import Server
		server = None
		try:
			# Start it in foreground (current thread, not new process),
			# server object will internally fork self if daemon is True
			server = Server(daemon)
			# notify caller - set server handle:
			if setServer:
				setServer(server)
			# run:
			server.start(conf["socket"],
							conf["pidfile"], conf["force"],
							conf=conf)
		except Exception as e: # pragma: no cover
			try:
				if server:
					server.quit()
			except Exception as e2:
				if conf["verbose"] > 1:
					logSys.exception(e2)
			raise
		finally:
			# notify waiting thread server ready resp. done (background execution, error case, etc):
			if conf.get('onstart'):
				conf['onstart']()

		return server

	##
	# Start Fail2Ban server.
	#
	# Start the Fail2ban server in daemon mode (background, start from client).

	@staticmethod
	def startServerAsync(conf):
		# Forks the current process, don't fork if async specified (ex: test cases)
		pid = 0
		frk = not conf["async"] and PRODUCTION
		if frk: # pragma: no cover
			pid = os.fork()
		logSys.debug("  async starting of server in %s, fork: %s - %s", os.getpid(), frk, pid)
		if pid == 0:
			args = list()
			args.append(SERVER)
			# Start async (don't read config) and in background as requested.
			args.append("--async")
			args.append("-b")
			# Set the socket path.
			args.append("-s")
			args.append(conf["socket"])
			# Set the pidfile
			args.append("-p")
			args.append(conf["pidfile"])
			# Force the execution if needed.
			if conf["force"]:
				args.append("-x")
			if conf["verbose"] > 1:
				args.append("-" + "v"*(conf["verbose"]-1))
			# Logging parameters:
			for o in ('loglevel', 'logtarget', 'syslogsocket'):
				args.append("--"+o)
				args.append(conf[o])
			try:
				# Directory of client (to try the first start from current or the same directory as client, and from relative bin):
				exe = Fail2banServer.getServerPath()
				if not frk:
					# Wrapr args to use the same python version in client/server (important for multi-python systems):
					args[0] = exe
					exe = sys.executable
					args[0:0] = [exe]
				logSys.debug("Starting %r with args %r", exe, args)
				if frk: # pragma: no cover
					os.execv(exe, args)
				else:
					# use P_WAIT instead of P_NOWAIT (to prevent defunct-zomby process), it started as daemon, so parent exit fast after fork):
					ret = os.spawnv(os.P_WAIT, exe, args)
					if ret != 0: # pragma: no cover
						raise OSError(ret, "Unknown error by executing server %r with %r" % (args[1], exe))
			except OSError as e: # pragma: no cover
				if not frk: #not PRODUCTION:
					raise
				# Use the PATH env.
				logSys.warning("Initial start attempt failed (%s). Starting %r with the same args", e, SERVER)
				if frk: # pragma: no cover
					os.execvp(SERVER, args)

	@staticmethod
	def getServerPath():
		startdir = sys.path[0]
		exe = os.path.abspath(os.path.join(startdir, SERVER))
		if not os.path.isfile(exe): # may be unresolved in test-cases, so get relative starter (client):
			startdir = os.path.dirname(sys.argv[0])
			exe = os.path.abspath(os.path.join(startdir, SERVER))
			if not os.path.isfile(exe): # may be unresolved in test-cases, so try to get relative bin-directory:
				startdir = os.path.dirname(os.path.abspath(__file__))
				startdir = os.path.join(os.path.dirname(os.path.dirname(startdir)), "bin")
				exe = os.path.abspath(os.path.join(startdir, SERVER))
		return exe

	def _Fail2banClient(self):
		from .fail2banclient import Fail2banClient
		cli = Fail2banClient()
		cli.applyMembers(self)
		return cli

	def start(self, argv):
		server = None
		try:
			# Command line options
			ret = self.initCmdLine(argv)
			if ret is not None:
				return ret

			# Commands
			args = self._args

			cli = None
			# Just start:
			if len(args) == 1 and args[0] == 'start' and not self._conf.get("interactive", False):
				pass
			else:
				# If client mode - whole processing over client:
				if len(args) or self._conf.get("interactive", False):
					cli = self._Fail2banClient()
					return cli.start(argv)

			# Start the server, corresponding options:
			#   background = True, if should be new process running in background, otherwise start in
			#     foreground process will be forked in daemonize, inside of Server module.
			#   nonsync = True, normally internal call only, if started from client, so configures
			#     the server via asynchronous thread.
			background = self._conf["background"]
			nonsync = self._conf.get("async", False)

			# If was started not from the client:
			if not nonsync:
				# Load requirements on demand (we need utils only when asynchronous handling):
				from ..server.utils import Utils
				# Start new thread with client to read configuration and
				# transfer it to the server:
				cli = self._Fail2banClient()
				cli._conf = self._conf
				phase = dict()
				logSys.debug('Configure via async client thread')
				cli.configureServer(phase=phase)

			# Start server, daemonize it, etc.
			pid = os.getpid()
			server = Fail2banServer.startServerDirect(self._conf, background,
				 cli._set_server if cli else None)
			# If forked - just exit other processes
			if pid != os.getpid(): # pragma: no cover
				os._exit(0)
			if cli:
				cli._server = server

			# wait for client answer "done":
			if not nonsync and cli:
				Utils.wait_for(lambda: phase.get('done', None) is not None, self._conf["timeout"], 0.001)
				if not phase.get('done', False):
					if server: # pragma: no cover
						server.quit()
					exit(255)
				if background:
					logSys.debug('Starting server done')

		except Exception as e:
			if self._conf["verbose"] > 1:
				logSys.exception(e)
			else:
				logSys.error(e)
			if server: # pragma: no cover
				server.quit()
			exit(255)

		return True

	@staticmethod
	def exit(code=0): # pragma: no cover
		if code != 0:
			logSys.error("Could not start %s", SERVER)
		exit(code)

def exec_command_line(argv):
	server = Fail2banServer()
	if server.start(argv):
		exit(0)
	else:
		exit(255)
