/*
 * SPDX-License-Identifier: ISC
 * SPDX-URL: https://spdx.org/licenses/ISC.html
 *
 * Copyright (C) 2005-2012 Atheme Project (http://atheme.org/)
 * Copyright (C) 2017-2018 Atheme Development Group (https://atheme.github.io/)
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * atheme-services: A collection of minimalist IRC services
 * signal.c: Signal-handling routines.
 */

#include <atheme.h>
#include "internal.h"

static void childproc_check(void);

static volatile sig_atomic_t got_sighup, got_sigint, got_sigterm, got_sigchld, got_sigusr2;

static mowgli_list_t childproc_list;

/*
 * A version of signal(2) that works more reliably across different
 * platforms.
 *
 * It restarts interrupted system calls, does not reset the handler,
 * and blocks the same signal from within the handler.
 */
static void
signal_empty_handler(int signum)
{
	/* do nothing */
}

static void
signal_hup_handler(int signum)
{
	got_sighup = 1;
}

static void
signal_int_handler(int signum)
{
	got_sigint = 1;
}

static void
signal_term_handler(int signum)
{
	got_sigterm = 1;
}

static void
signal_chld_handler(int signum)
{
	got_sigchld = 1;
}

static void
signal_usr2_handler(int signum)
{
	got_sigusr2 = 1;
}

/* XXX */
static void ATHEME_FATTR_NORETURN
signal_usr1_handler(int signum)
{
	int n;
	if (me.connected && curr_uplink != NULL &&
		curr_uplink->conn != NULL)
	{
		if (chansvs.nick != NULL)
		{
			n = send(curr_uplink->conn->fd, ":", 1, 0);
			n = send(curr_uplink->conn->fd, chansvs.nick, strlen(chansvs.nick), 0);
			n = send(curr_uplink->conn->fd, " QUIT :Out of memory!\r\n", 23, 0);
		}
		n = send(curr_uplink->conn->fd, "ERROR :Panic! Out of memory.\r\n", 30, 0);
	}
	if (runflags & (RF_LIVE | RF_STARTING))
		n = write(2, "Out of memory!\n", 15);
	abort();
}

void
init_signal_handlers(void)
{
#ifndef MOWGLI_OS_WIN
#ifdef SIGHUP
	mowgli_signal_install_handler(SIGHUP, signal_hup_handler);
#endif

#ifdef SIGINT
	mowgli_signal_install_handler(SIGINT, signal_int_handler);
#endif

#ifdef SIGTERM
	mowgli_signal_install_handler(SIGTERM, signal_term_handler);
#endif

#ifdef SIGPIPE
	mowgli_signal_install_handler(SIGPIPE, signal_empty_handler);
#endif

#ifdef SIGCHLD
	mowgli_signal_install_handler(SIGCHLD, signal_chld_handler);
#endif

#ifdef SIGUSR1
	mowgli_signal_install_handler(SIGUSR1, signal_usr1_handler);
#endif

#ifdef SIGUSR2
	mowgli_signal_install_handler(SIGUSR2, signal_usr2_handler);
#endif
#endif
}

void
check_signals(void)
{
	/* rehash */
	if (got_sighup)
	{
		got_sighup = 0;
		slog(LG_INFO, "sighandler(): got SIGHUP, rehashing \2%s\2", config_file);

		wallops("Got SIGHUP; reloading \2%s\2.", config_file);

		if (db_save && !readonly)
		{
			slog(LG_INFO, "UPDATE: \2%s\2", "system console");
			wallops("Updating database by request of \2%s\2.", "system console");
			db_save(NULL, DB_SAVE_BG_IMPORTANT);
		}

		slog(LG_INFO, "REHASH: \2%s\2", "system console");
		wallops("Rehashing \2%s\2 by request of \2%s\2.", config_file, "system console");

		/* reload the config, opening other logs besides the core log if needed. */
		if (!conf_rehash())
			wallops("REHASH of \2%s\2 failed. Please correct any errors in the file and try again.", config_file);

		return;
	}

	/* usually caused by ^C */
	if (got_sigint && (runflags & RF_LIVE))
	{
		got_sigint = 0;
		wallops("Exiting on signal %d.", SIGINT);
		if (chansvs.me != NULL && chansvs.me->me != NULL)
			quit_sts(chansvs.me->me, "caught interrupt");
		me.connected = false;
		slog(LG_INFO, "sighandler(): caught interrupt; exiting...");
		runflags |= RF_SHUTDOWN;
	}
	else if (got_sigint && !(runflags & RF_LIVE))
	{
		got_sigint = 0;
		wallops("Got SIGINT; restarting.");

		slog(LG_INFO, "RESTART: \2%s\2", "system console");
		wallops("Restarting by request of \2%s\2.", "system console");

		runflags |= RF_RESTART;
	}

	if (got_sigterm)
	{
		got_sigterm = 0;
		wallops("Exiting on signal %d.", SIGTERM);
		slog(LG_INFO, "sighandler(): got SIGTERM; exiting...");
		runflags |= RF_SHUTDOWN;
	}

	if (got_sigusr2)
	{
		got_sigusr2 = 0;
		wallops("Got SIGUSR2; restarting.");

		slog(LG_INFO, "RESTART: \2%s\2", "system console");
		wallops("Restarting by request of \2%s\2.", "system console");

		runflags |= RF_RESTART;
	}

	if (got_sigchld)
	{
		got_sigchld = 0;
		childproc_check();
	}
}

struct childproc
{
	mowgli_node_t node;
	pid_t pid;
	char *desc;
	void (*cb)(pid_t pid, int status, void *data);
	void *data;
};

/* Registers a child process.
 * Will call cb(pid, status, data) after the process terminates
 * where status is the status from waitpid(2).
 */
void
childproc_add(pid_t pid, const char *desc, void (*cb)(pid_t pid, int status, void *data), void *data)
{
	struct childproc *const p = smalloc(sizeof *p);
	p->pid = pid;
	p->desc = sstrdup(desc);
	p->cb = cb;
	p->data = data;
	mowgli_node_add(p, &p->node, &childproc_list);
}

static void
childproc_free(struct childproc *p)
{
	sfree(p->desc);
	sfree(p);
}

/* Forgets about all child processes with the given callback.
 * Useful if the callback is in a module which is being unloaded.
 */
void
childproc_delete_all(void (*cb)(pid_t pid, int status, void *data))
{
	mowgli_node_t *n, *tn;
	struct childproc *p;

	MOWGLI_ITER_FOREACH_SAFE(n, tn, childproc_list.head)
	{
		p = n->data;
		if (p->cb == cb)
		{
			mowgli_node_delete(&p->node, &childproc_list);
			childproc_free(p);
		}
	}
}

static void
childproc_check(void)
{
#ifndef MOWGLI_OS_WIN
	pid_t pid;
	int status;
	mowgli_node_t *n;
	struct childproc *p;

	while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
	{
		MOWGLI_ITER_FOREACH(n, childproc_list.head)
		{
			p = n->data;
			if (p->pid == pid)
			{
				mowgli_node_delete(&p->node, &childproc_list);
				if (p->cb)
					p->cb(pid, status, p->data);
				childproc_free(p);
				break;
			}
		}
	}
#endif
}

/* vim:cinoptions=>s,e0,n0,f0,{0,}0,^0,=s,ps,t0,c3,+s,(2s,us,)20,*30,gs,hs
 * vim:ts=8
 * vim:sw=8
 * vim:noexpandtab
 */
