/* This file is part of Mailfromd.
   Copyright (C) 2005-2025 Sergey Poznyakoff

   This program 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 3, or (at your option)
   any later version.

   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sysexits.h>
#include <mailutils/error.h>
#include <mailutils/errno.h>
#include <mailutils/argcv.h>
#include <mailutils/errno.h>
#include <mailutils/daemon.h>
#include <mailutils/util.h>
#include <mailutils/stream.h>
#include <mailutils/alloc.h>

#include "libmf.h"
#include "srvman.h"

/* Check whether pidfile NAME exists and if so, whether its PID is still
   active. Exit if it is. */
void
mf_server_check_pidfile(const char *name)
{
	unsigned long pid;
	FILE *fp = fopen(name, "r");
	if (!fp) {
		if (errno == ENOENT)
			return;
		mu_error(_("cannot open pidfile `%s': %s"),
			 name, mu_strerror(errno));
		exit(EX_TEMPFAIL);
	}
	if (fscanf(fp, "%lu", &pid) != 1) {
		mu_error(_("cannot get pid from pidfile `%s'"), name);
	} else {
		if (kill(pid, 0) == 0) {
			mu_error(_("%s appears to run with pid %lu. If it does not, remove `%s' and retry."),
				 mu_program_name,
				 pid,
				 name);
			exit(EX_USAGE);
		}
	}
	fclose(fp);
	if (unlink(name)) {
		mu_error(_("cannot unlink pidfile `%s': %s"),
			 name, mu_strerror(errno));
		exit(EX_USAGE);
	}
}

void
mf_server_write_pidfile(const char *name)
{
	FILE *fp = fopen(name, "w");
	if (!fp) {
		mu_error(_("cannot create pidfile `%s': %s"),
			 name, mu_strerror(errno));
		exit(EX_TEMPFAIL);
	}
	fprintf(fp, "%lu\n", (unsigned long) getpid());
	fclose(fp);
}


static int x_argc;          /* A copy of argc and ... */
static char **x_argv;       /* ... argv */ 
static int restart;         /* Set to 1 if restart is requested */
char *mf_server_lint_option;

void
mf_server_save_cmdline(int argc, char **argv)
{
	int i;

	if (argc == 0) {
		/* Clear saved command line */
		for (i = 0; i < x_argc; i++)
			free(x_argv[i]);
		free(x_argv);
		x_argc = 0;
		x_argv = NULL;
	} else if (argv[0][0] != '/') {
		/* Do not save if argv[0] is not a full file name.
		   An appropriate error message will be issued later,
		   if necessary. */
		return;
	}
	x_argc = argc;
	x_argv = mu_calloc(x_argc + 1, sizeof x_argv[0]);
	for (i = 0; i < x_argc; i++)
		x_argv[i] = mu_strdup(argv[i]);
	x_argv[i] = NULL;
}

static void
sig_stop(int sig)
{
	mfd_srvman_stop();
}

static void
sig_restart(int sig)
{
	restart = 1;
}

/* Umask for creating new files */
mode_t mf_server_umask = 0017;

char *mf_server_user = DEFAULT_USER; /* Switch to this user privileges after 
					startup */
struct mf_gid_list *mf_server_retain_groups;
                                     /* List of group IDs to retain when
					switching to user privileges. */

void (*mf_server_startup_hook)(void);
void (*mf_server_shutdown_hook)(void);

void
priv_setup()
{
	if (getuid() == 0) {
		if (!mf_server_user) {
			mu_error(_("when running as root, --user option is mandatory."));
			exit(EX_USAGE);
		} else {
			struct passwd *pw = getpwnam(mf_server_user);
			if (!pw) {
				mu_error(_("no such user: %s"),
					 mf_server_user);
				exit(EX_SOFTWARE);
			}
			if (pw && switch_to_privs(pw->pw_uid, pw->pw_gid,
						  mf_server_retain_groups))
				exit(EX_SOFTWARE);
		}
	}
}

static int
run_lint()
{
	pid_t pid;
	int p[2];

	if (!mf_server_lint_option)
		return 0;
	
	if (pipe(p)) {
		mu_error(_("pipe: %s"), mu_strerror(errno));
		return -1;
	}
	pid = fork();
	if (pid == -1) {
		mu_error(_("fork: %s"), mu_strerror(errno));
		close(p[0]);
		close(p[1]);
		return -1;
	}

	if (pid) {
		mu_stream_t instream;
		int rc, status;
		
		close(p[1]);
		rc = mu_stdio_stream_create(&instream, p[0], MU_STREAM_READ);
		if (rc)	{
			mu_error(_("cannot open input stream: %s"),
				 mu_strerror (rc));
			return -1;
		}
		rc = mu_stream_copy(mu_strerr, instream, 0, NULL);
		mu_stream_close(instream);
		mu_stream_destroy(&instream);
		waitpid(pid, &status, 0);
		return !(WIFEXITED(status) && WEXITSTATUS(status) == 0);
	}

	/* Child process */
	
	/* Prepare arguments */
	x_argv = mu_realloc(x_argv, (x_argc + 3) * sizeof(x_argv[0]));
	x_argv[x_argc] = "--stderr";
	x_argv[x_argc + 1] = mf_server_lint_option;
	x_argv[x_argc + 2] = NULL;

	close(2);
	dup2(p[1], 2);
	close(p[0]);
	close_fds_above(2);
	execv(x_argv[0], x_argv);
	abort();
}

static int
server_idle_hook(void *data)
{
	if (restart) {
		if (run_lint()) {
			mu_error(_("syntax check failed; restart cancelled"));
			restart = 0;
		} else
			return 1;
	}
	return 0;
}

void
mf_server_start(const char *program, const char *dir, const char *pidfile, 
		int flags)
{
	priv_setup();

	if (dir && chdir(dir)) {
		mu_error(_("cannot change to %s: %s"), dir,
			 strerror(errno));
		exit(EX_OSERR);
	}
	
	if (!(flags & MF_SERVER_FOREGROUND))
		mf_server_check_pidfile(pidfile);

	signal(SIGTERM, sig_stop);
	signal(SIGQUIT, sig_stop);
	if (x_argc == 0) {
		mu_diag_output(MU_DIAG_WARNING,
			       _("program started without full file name"));
		flags |= MF_SERVER_NORESTART;
	}

	if (flags & MF_SERVER_NORESTART) {
		mu_diag_output(MU_DIAG_WARNING,
			       _("restart (SIGHUP) will not work"));
		signal(SIGHUP, sig_stop);
	} else
		signal(SIGHUP, sig_restart);
	signal(SIGINT, sig_stop);
	
 	if (!(flags & MF_SERVER_FOREGROUND)) {
		if (daemon(!!dir, 0) == -1) {
			mu_error(_("cannot become a daemon: %s"),
				 mu_strerror(errno));
			exit(EX_OSERR);
		}
		umask(mf_server_umask);
		mf_server_write_pidfile(pidfile);
	} else {
		umask(mf_server_umask);
	}

	if (mf_server_startup_hook)
		mf_server_startup_hook();
	mf_server_log_setup();

	mu_error(_("%s started"), program);

	srvman_param.idle_hook = server_idle_hook;
	if (mfd_srvman_open())
		exit(EX_UNAVAILABLE);
	mfd_srvman_run(NULL);
	mfd_srvman_shutdown();
	mfd_srvman_free();
	unlink(pidfile);
	if (mf_server_shutdown_hook)
		mf_server_shutdown_hook();
	if (restart) {
		char *s;
		
		mu_error(_("%s restarting"), program);
		mu_daemon_remove_pidfile();
		logger_close();
		if (logger_flags(LOGF_STDERR))
			close_fds_above(2);
		else
			close_fds_above(1);
		execv(x_argv[0], x_argv);
		mf_server_log_setup();
		mu_error(_("cannot restart: %s"), mu_strerror(errno));
		if (mu_argcv_string(x_argc, x_argv, &s) == 0)
			mu_error(_("command line was: %s"), s);
	} else 
		mu_error(_("%s terminating"), program);

	exit(0);
}
