// Copyright Maarten L. Hekkelman, Radboud University 2008-2013.
//        Copyright Maarten L. Hekkelman, 2014-2026
//  Distributed under the Boost Software License, Version 1.0.
//     (See accompanying file LICENSE_1_0.txt or copy at
//           http://www.boost.org/LICENSE_1_0.txt)

// Source code specifically for Unix/Linux
// Utility routines to build daemon processes

#include "zeep/http/daemon.hpp"

#include "signals.hpp"
#include "zeep/config.hpp"
#include "zeep/exception.hpp"
#include "zeep/http/asio.hpp"
#include "zeep/http/preforked-server.hpp"
#include "zeep/http/server.hpp"
#include "zeep/unicode-support.hpp"

#include <cerrno>
#include <climits>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <exception>
#include <fcntl.h>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/types.h>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>

#ifndef _WIN32
# include <grp.h>
# include <pwd.h>
# include <sys/wait.h>
# include <unistd.h>
#endif

namespace fs = std::filesystem;

namespace zeep::http
{

#if __APPLE__
int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups)
{
	return ::getgrouplist(user, (int)group, (int *)groups, ngroups);
}
#endif

daemon::daemon(server_factory_type &&factory, std::string pid_file,
	std::string stdout_log_file, std::string stderr_log_file)
	: m_factory(std::move(factory))
	, m_pid_file(std::move(pid_file))
	, m_stdout_log_file(std::move(stdout_log_file))
	, m_stderr_log_file(std::move(stderr_log_file))
{
}

daemon::daemon(server_factory_type &&factory, const std::string &name)
	: daemon(std::move(factory), "/var/run/" + name,
		  "/var/log/" + name + "/access.log", "/var/log/" + name + "/error.log")
{
}

int daemon::run_foreground(std::string_view address, uint16_t port)
{
	asio_ns::io_context io_context;
	asio_ns::ip::tcp::endpoint endpoint;

	asio_system_ns::error_code ec;
	auto addr = asio_ns::ip::make_address(address, ec);
	if (not ec)
		endpoint = asio_ns::ip::tcp::endpoint(addr, port);
	else
	{
		asio_ns::ip::tcp::resolver resolver(io_context);
		for (auto &ep : resolver.resolve(address, std::to_string(port)))
		{
			endpoint = ep;
			break;
		}
	}

	asio_ns::ip::tcp::acceptor acceptor(io_context);
	acceptor.open(endpoint.protocol());
	acceptor.set_option(asio_ns::ip::tcp::acceptor::reuse_address(true));
	if (acceptor.bind(endpoint, ec))
		throw std::runtime_error(std::string("Is server running already? ") + ec.message());
	acceptor.listen();

	acceptor.close();

	signal_catcher sc;
	sc.block();

	std::unique_ptr<basic_server> s(m_factory());
	s->bind(address, port);
	std::thread t([s = s.get()]
		{ s->run(1); });

	sc.unblock();

	sc.wait();

	s->stop();

	if (t.joinable())
		t.join();

	return 0;
}

#if HTTP_HAS_UNIX_DAEMON

int daemon::start(std::string_view address, uint16_t port, int nr_of_procs, int nr_of_threads, const std::string &run_as_user)
{
	int result = 0;

	if (pid_is_for_executable())
	{
		std::clog << "Server is already running.\n";
		result = 1;
	}
	else
	{
		std::error_code ec;

		if (fs::exists(m_pid_file, ec))
			fs::remove(m_pid_file, ec);

		fs::path pidDir = fs::path(m_pid_file).parent_path();
		if (not fs::is_directory(pidDir, ec))
			fs::create_directories(pidDir, ec);

		if (ec)
			std::clog << "Creating directory for pid file failed: " << ec.message() << '\n';

		fs::path outLogDir = fs::path(m_stdout_log_file).parent_path();
		if (not fs::is_directory(outLogDir, ec))
			fs::create_directories(outLogDir, ec);

		if (ec)
			std::clog << "Creating directory " << outLogDir << " for log files failed: " << ec.message() << '\n';

		fs::path errLogDir = fs::path(m_stderr_log_file).parent_path();
		if (not fs::is_directory(errLogDir, ec))
			fs::create_directories(errLogDir, ec);

		if (ec)
			std::clog << "Creating directory " << outLogDir << " for log files failed: " << ec.message() << '\n';

		try
		{
			asio_ns::io_context io_context;

			asio_ns::ip::tcp::endpoint endpoint;
			try
			{
				endpoint = asio_ns::ip::tcp::endpoint(asio_ns::ip::make_address(address), port);
			}
			catch (const std::exception &e)
			{
				asio_ns::ip::tcp::resolver resolver(io_context);
				for (auto &ep : resolver.resolve(address, std::to_string(port)))
				{
					endpoint = ep;
					break;
				}
			}

			asio_ns::ip::tcp::acceptor acceptor(io_context);
			acceptor.open(endpoint.protocol());
			acceptor.set_option(asio_ns::ip::tcp::acceptor::reuse_address(true));
			acceptor.bind(endpoint);
			acceptor.listen();

			acceptor.close();
		}
		catch (exception &e)
		{
			throw std::runtime_error(std::string("Is server running already? ") + e.what());
		}

		int pid = daemonize();
		if (pid == 0)
		{
			for (;;)
			{
				bool hupped = run_main_loop(address, port, nr_of_procs, nr_of_threads, run_as_user);
				if (hupped)
				{
					std::clog << "Server was interrupted, will attempt to resume\n";
					continue;
				}

				break;
			}

			if (fs::exists(m_pid_file, ec))
				fs::remove(m_pid_file, ec);

			if (ec)
				std::clog << "Removing pid file failed: " << ec.message() << '\n';

			_exit(0);
		}

		// Forking is done twice, so mop up the zombie here
		int status, pid_c;
		pid_c = waitpid(-1, &status, WUNTRACED);

		if (pid_c != -1)
		{
			if (WIFSIGNALED(status) and WTERMSIG(status) != SIGKILL)
				std::clog << "child " << pid_c << " terminated by signal " << WTERMSIG(status) << '\n';
			// else
			// 	std::clog << "child terminated normally\n";
		}
	}

	return result;
}

int daemon::start(std::string_view address, uint16_t port, int nr_of_threads, const std::string &run_as_user)
{
	using namespace std::literals;

	int result = 0;

	if (pid_is_for_executable())
	{
		std::clog << "Server is already running.\n";
		result = 1;
	}
	else
	{
		std::error_code ec;

		if (fs::exists(m_pid_file, ec))
			fs::remove(m_pid_file, ec);

		fs::path pidDir = fs::path(m_pid_file).parent_path();
		if (not fs::is_directory(pidDir, ec))
			fs::create_directories(pidDir, ec);

		if (ec)
			std::clog << "Creating directory for pid file failed: " << ec.message() << '\n';

		fs::path outLogDir = fs::path(m_stdout_log_file).parent_path();
		if (not fs::is_directory(outLogDir, ec))
			fs::create_directories(outLogDir, ec);

		if (ec)
			std::clog << "Creating directory " << outLogDir << " for log files failed: " << ec.message() << '\n';

		fs::path errLogDir = fs::path(m_stderr_log_file).parent_path();
		if (not fs::is_directory(errLogDir, ec))
			fs::create_directories(errLogDir, ec);

		if (ec)
			std::clog << "Creating directory " << outLogDir << " for log files failed: " << ec.message() << '\n';

		try
		{
			asio_ns::io_context io_context;

			asio_ns::ip::tcp::endpoint endpoint;
			try
			{
				endpoint = asio_ns::ip::tcp::endpoint(asio_ns::ip::make_address(address), port);
			}
			catch (const std::exception &e)
			{
				asio_ns::ip::tcp::resolver resolver(io_context);
				for (auto &ep : resolver.resolve(address, std::to_string(port)))
				{
					endpoint = ep;
					break;
				}
			}

			asio_ns::ip::tcp::acceptor acceptor(io_context);
			acceptor.open(endpoint.protocol());
			acceptor.set_option(asio_ns::ip::tcp::acceptor::reuse_address(true));
			acceptor.bind(endpoint);
			acceptor.listen();

			acceptor.close();
		}
		catch (exception &e)
		{
			throw std::runtime_error(std::string("Is server running already? ") + e.what());
		}

		int pid = daemonize();
		if (pid == 0) // Child process
		{
			open_log_file();

			std::clog << "starting server\n"
					  << "Listening to " << address << ':' << port << '\n';

			signal_catcher sc;
			sc.block();

			// Drop privileges
			if (not run_as_user.empty())
			{
				struct passwd *pw = getpwnam(run_as_user.c_str());
				if (pw == nullptr)
				{
					std::clog << "Failed to set uid to " << run_as_user << ": " << strerror(errno) << '\n';
					exit(1);
				}

				if (pw->pw_uid != getuid())
				{
					int ngroups = 0;
					if (getgrouplist(pw->pw_name, pw->pw_gid, nullptr, &ngroups) == -1 and ngroups > 0)
					{
						std::vector<gid_t> groups(ngroups);
						if (getgrouplist(pw->pw_name, pw->pw_gid, groups.data(), &ngroups) != -1 and
							setgroups(ngroups, groups.data()) == -1)
						{
							std::clog << "Failed to set groups for " << run_as_user << ": " << strerror(errno) << '\n';
							exit(1);
						}
					}

					if (setgid(pw->pw_gid) < 0)
					{
						std::clog << "Failed to set gid for " << run_as_user << ": " << strerror(errno) << '\n';
						exit(1);
					}

					if (setuid(pw->pw_uid) < 0)
					{
						std::clog << "Failed to set uid to " << run_as_user << ": " << strerror(errno) << '\n';
						exit(1);
					}
				}
			}

			for (;;)
			{
				sc.block();

				std::unique_ptr<basic_server> server;
				try
				{
					server.reset(m_factory());
					server->bind(address, port);
				}
				catch (const exception &e)
				{
					std::clog << "Failed to launch server: " << e.what() << '\n';
					exit(1);
				}

				std::thread t([nr_of_threads, &server]()
					{ server->run(nr_of_threads); });

				sc.unblock();
				int sig = sc.wait();

				std::clog << "Process " << getpid() << " received signal " << sig << "\n";

				server->stop();

				if (t.joinable())
					t.join();

				if (sig == SIGHUP)
				{
					// re-open log files
					open_log_file();

					std::clog << "re-starting server\n"
							  << "Listening to " << address << ':' << port << '\n';

					continue;
				}

				break;
			}

			if (fs::exists(m_pid_file, ec))
				fs::remove(m_pid_file, ec);

			if (ec)
				std::clog << "Removing pid file failed: " << ec.message() << '\n';

			// We're done. Exit
			_exit(0);
		}

		// avoid zombies
		int status, pid_c;
		pid_c = waitpid(-1, &status, WUNTRACED);

		if (pid_c != -1)
		{
			if (WIFSIGNALED(status) and WTERMSIG(status) != SIGKILL)
				std::clog << "child " << pid_c << " terminated by signal " << WTERMSIG(status) << '\n';
			// else
			// 	std::clog << "child terminated normally\n";
		}
	}

	return result;
}

int daemon::stop()
{
	int result = 1;

	if (pid_is_for_executable())
	{
		std::ifstream file(m_pid_file);
		if (not file.is_open())
			throw std::runtime_error("Failed to open pid file");

		int pid;
		file >> pid;
		file.close();

		result = ::kill(pid, SIGINT);
		if (result != 0)
			std::clog << "Failed to stop process " << pid << ": " << strerror(errno) << '\n';

		// avoid zombies
		int status, pid_c;
		pid_c = waitpid(pid, &status, WUNTRACED);

		if (pid_c != -1)
		{
			if (WIFSIGNALED(status) and WTERMSIG(status) != SIGKILL)
				std::clog << "child " << pid_c << " terminated by signal " << WTERMSIG(status) << '\n';
			// else
			// 	std::clog << "child terminated normally\n";
		}

		std::error_code ec;
		if (fs::exists(m_pid_file, ec))
			fs::remove(m_pid_file, ec);
		if (ec)
			std::clog << "Could not remove pid file: " << ec.message() << '\n';
	}
	else
		throw std::runtime_error("Not my pid file: " + m_pid_file);

	return result;
}

int daemon::status()
{
	int result;

	if (pid_is_for_executable())
	{
		std::clog << "server is running\n";
		result = 0;
	}
	else
	{
		std::clog << "server is not running\n";
		result = 1;
	}

	return result;
}

int daemon::reload()
{
	int result;

	if (pid_is_for_executable())
	{
		std::ifstream file(m_pid_file);
		if (not file.is_open())
			throw std::runtime_error("Failed to open pid file");

		int pid;
		file >> pid;

		result = ::kill(pid, SIGHUP);
	}
	else
	{
		std::clog << "server is not running\n";
		result = 1;
	}

	return result;
}

int daemon::daemonize()
{
	int pid = fork();

	if (pid == -1)
	{
		std::clog << "Fork failed\n";
		exit(1);
	}

	// exit the parent (=calling) process
	if (pid != 0)
		return pid;

	if (setsid() < 0)
	{
		std::clog << "Failed to create process group: " << strerror(errno) << '\n';
		exit(1);
	}

	// This in-between process should not catch SIGHUP
	(void)signal(SIGHUP, SIG_IGN);

	// fork again, to avoid being able to attach to a terminal device
	pid = fork();

	if (pid == -1)
		std::clog << "Fork failed\n";

	if (pid != 0)
		_exit(0);

	// write our pid to the pid file
	std::ofstream pidFile(m_pid_file);
	if (not pidFile.is_open())
	{
		std::clog << "Failed to write to " << m_pid_file << ": " << strerror(errno) << '\n';
		exit(1);
	}

	pidFile << getpid() << '\n';
	pidFile.close();

	if (chdir("/") != 0)
	{
		std::clog << "Cannot chdir to /: " << strerror(errno) << '\n';
		exit(1);
	}

	// close stdin
	close(STDIN_FILENO);
	(void)open("/dev/null", O_RDONLY); // NOLINT(hicpp-vararg)

	// The final process should however catch SIGHUP
	(void)signal(SIGHUP, SIG_DFL);

	return 0;
}

void daemon::open_log_file()
{
	// Flush the IO first
	std::cout.flush();
	std::cerr.flush();
	std::clog.flush();

	// open the log file
	int fd_out = open(m_stdout_log_file.c_str(), O_CREAT | O_APPEND | O_RDWR, 0644); // NOLINT(hicpp-vararg)
	if (fd_out < 0)
	{
		std::clog << "Opening log file " << m_stdout_log_file << " failed\n";
		exit(1);
	}

	int fd_err;

	if (m_stderr_log_file == m_stdout_log_file)
		fd_err = fd_out;
	else
	{
		fd_err = open(m_stderr_log_file.c_str(), O_CREAT | O_APPEND | O_RDWR, 0644); // NOLINT(hicpp-vararg)
		if (fd_err < 0)
		{
			std::clog << "Opening log file " << m_stderr_log_file << " failed\n";
			exit(1);
		}
	}

	// redirect stdout and stderr to the log file
	dup2(fd_out, STDOUT_FILENO);
	dup2(fd_err, STDERR_FILENO);

	// close the actual file descriptors to avoid leaks
	close(fd_out);
	if (fd_err != fd_out)
		close(fd_err);
}

bool daemon::run_main_loop(std::string_view address, uint16_t port, int nr_of_procs, int nr_of_threads, const std::string &run_as_user)
{
	int sig = 0;

	int restarts = 0;

	for (;;)
	{
		auto start = time(nullptr);

		open_log_file();

		if (sig == 0)
			std::clog << "starting server\n";
		else
			std::clog << "restarting server\n";

		std::clog << "Listening to " << address << ':' << port << '\n';

		sigset_t new_mask, old_mask;
		sigfillset(&new_mask);
		pthread_sigmask(SIG_BLOCK, &new_mask, &old_mask);

		preforked_server server([=, this]()
			{
			try
			{
				if (not run_as_user.empty())
				{
					struct passwd* pw = getpwnam(run_as_user.c_str());
					if (pw == nullptr)
					{
						std::clog << "Failed to set uid to " << run_as_user << ": " << strerror(errno) << '\n';
						exit(1);
					}

					if (pw->pw_uid != getuid())
					{
						int ngroups = 0;
						if (getgrouplist(pw->pw_name, pw->pw_gid, nullptr, &ngroups) == -1 and ngroups > 0)
						{
							std::vector<gid_t> groups(ngroups);
							if (getgrouplist(pw->pw_name, pw->pw_gid, groups.data(), &ngroups) != -1 and
								setgroups(ngroups, groups.data()) == -1)
							{
								std::clog << "Failed to set groups for " << run_as_user << ": " << strerror(errno) << '\n';
								exit(1);
							}
						}

						if (setgid(pw->pw_gid) < 0)
						{
							std::clog << "Failed to set gid for " << run_as_user << ": " << strerror(errno) << '\n';
							exit(1);
						}

						if (setuid(pw->pw_uid) < 0)
						{
							std::clog << "Failed to set uid to " << run_as_user << ": " << strerror(errno) << '\n';
							exit(1);
						}
					}
				}
				
				return m_factory();
			}
			catch (const exception& e)
			{
				std::clog << "Failed to launch server: " << e.what() << '\n';
				exit(1);
			} });

		std::thread t([s = &server, address, port, nr_of_procs, nr_of_threads]
			{ s->run(address, port, nr_of_procs, nr_of_threads); });

		try
		{
			server.start();
		}
		catch (const exception &ex)
		{
			std::clog << '\n'
					  << "Exception running server: \n"
					  << ex.what() << '\n'
					  << '\n';
			exit(1);
		}

		pthread_sigmask(SIG_SETMASK, &old_mask, nullptr);

		// Wait for signal indicating time to shut down.
		sigset_t wait_mask;
		sigemptyset(&wait_mask);
		sigaddset(&wait_mask, SIGINT);
		sigaddset(&wait_mask, SIGHUP);
		sigaddset(&wait_mask, SIGQUIT);
		sigaddset(&wait_mask, SIGTERM);
		sigaddset(&wait_mask, SIGCHLD);
		pthread_sigmask(SIG_BLOCK, &wait_mask, nullptr);

		sigwait(&wait_mask, &sig);

		pthread_sigmask(SIG_SETMASK, &old_mask, nullptr);

		server.stop();
		t.join();

		if (sig != SIGCHLD)
			break;

		int status, pid;
		pid = waitpid(-1, &status, WUNTRACED);

		if (pid != -1)
		{
			if (WIFSIGNALED(status) and WTERMSIG(status) != SIGKILL)
				std::clog << "child " << pid << " terminated by signal " << WTERMSIG(status) << '\n';
			// else
			// 	std::clog << "child terminated normally\n";
		}

		// did the client crash within the time window?
		if (time(nullptr) - start > m_restart_time_window)
		{
			restarts = 0; // no, it was outside, reset counter
			continue;
		}

		if (++restarts >= m_max_restarts)
		{
			std::clog << "aborting due to excessive restarts\n";
			break;
		}
	}

	return sig == SIGHUP;
}

bool daemon::pid_is_for_executable()
{
	using namespace std::literals;

	bool result = false;

	if (fs::exists(m_pid_file))
	{
		std::ifstream pidfile(m_pid_file);
		if (not pidfile.is_open())
			throw std::runtime_error("Failed to open pid file " + m_pid_file + ": " + strerror(errno));

		int pid;
		pidfile >> pid;

		// if /proc/PID/exe points to our executable, this means we're already running
		char path[PATH_MAX] = "";
		if (readlink(("/proc/" + std::to_string(pid) + "/exe").c_str(), path, sizeof(path)) > 0)
		{
			char exe[PATH_MAX] = "";
			if (readlink("/proc/self/exe", exe, sizeof(exe)) == -1)
				throw std::runtime_error("could not get exe path ("s + strerror(errno) + ")");

			result = strcmp(exe, path) == 0 or
			         (ends_with(path, " (deleted)") and starts_with(path, exe));
		}
		else if (errno == ENOENT)       // link file doesn't exist (can happen on e.g. macOS)
			result = kill(pid, 0) == 0; // simply test using kill with signal 0.
		else
			throw std::runtime_error("Failed to read executable link : "s + strerror(errno));
	}

	return result;
}

#endif

} // namespace zeep::http
