/*++
/* NAME
/*	master_spawn 3
/* SUMMARY
/*	Postfix master - child process birth and death
/* SYNOPSIS
/*	#include "master.h"
/*
/*	void	master_spawn(serv)
/*	MASTER_SERV *serv;
/*
/*	void	master_reap_child()
/*
/*	void	master_delete_children(serv)
/*	MASTER_SERV *serv;
/* DESCRIPTION
/*	This module creates and cleans up child processes, and applies
/*	a process creation throttle in case of serious trouble.
/*	This module is the working horse for the master_avail(3) process
/*	creation policy module.
/*
/*	master_spawn() spawns off a child process for the specified service,
/*	making the child process available for servicing connection requests.
/*	It is an error to call this function then the specified service is
/*	throttled.
/*
/*	master_reap_child() cleans up all dead child processes.  One typically
/*	runs this function at a convenient moment after receiving a SIGCHLD
/*	signal. When a child process terminates abnormally after being used
/*	for the first time, process creation for that service is throttled
/*	for a configurable amount of time.
/*
/*	master_delete_children() deletes all child processes that provide
/*	the named service. Upon exit, the process creation throttle for that
/*	service is released.
/* DIAGNOSTICS
/*	Panic: interface violations, internal inconsistencies.
/*	Fatal errors: out of memory.  Warnings: throttle on/off.
/* BUGS
/* SEE ALSO
/*	master_avail(3), process creation policy.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*--*/

/* System libraries. */

#include <sys_defs.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>			/* closelog() */
#include <signal.h>
#include <stdarg.h>
#include <syslog.h>

/* Utility libraries. */

#include <msg.h>
#include <binhash.h>
#include <mymalloc.h>
#include <events.h>
#include <vstring.h>
#include <argv.h>

/* Global library. */

#include <mail_conf.h>

/* Application-specific. */

#include "master_proto.h"
#include "master.h"

BINHASH *master_child_table;
static void master_unthrottle(MASTER_SERV *serv);

/* master_unthrottle_wrapper - in case (char *) != (struct *) */

static void master_unthrottle_wrapper(int unused_event, void *ptr)
{
    MASTER_SERV *serv = (MASTER_SERV *) ptr;

    /*
     * This routine runs after expiry of the timer set in master_throttle(),
     * which gets called when it appears that the world is falling apart.
     */
    master_unthrottle(serv);
}

/* master_unthrottle - enable process creation */

static void master_unthrottle(MASTER_SERV *serv)
{

    /*
     * Enable process creation within this class. Disable the "unthrottle"
     * timer just in case we're being called directly from the cleanup
     * routine, instead of from the event manager.
     */
    if ((serv->flags & MASTER_FLAG_THROTTLE) != 0) {
	serv->flags &= ~MASTER_FLAG_THROTTLE;
	event_cancel_timer(master_unthrottle_wrapper, (void *) serv);
	if (msg_verbose)
	    msg_info("throttle released for command %s", serv->path);
	master_avail_listen(serv);
    }
}

/* master_throttle - suspend process creation */

static void master_throttle(MASTER_SERV *serv)
{

    /*
     * Perhaps the command to be run is defective, perhaps some configuration
     * is wrong, or perhaps the system is out of resources. Disable further
     * process creation attempts for a while.
     */
    if ((serv->flags & MASTER_FLAG_THROTTLE) == 0) {
	serv->flags |= MASTER_FLAG_THROTTLE;
	event_request_timer(master_unthrottle_wrapper, (void *) serv,
			    serv->throttle_delay);
	if (msg_verbose)
	    msg_info("throttling command %s", serv->path);
	master_avail_listen(serv);
    }
}

/* master_spawn - spawn off new child process if we can */

void    master_spawn(MASTER_SERV *serv)
{
    const char *myname = "master_spawn";
    MASTER_PROC *proc;
    MASTER_PID pid;
    int     n;
    static unsigned master_generation = 0;
    static VSTRING *env_gen = 0;

    if (master_child_table == 0)
	master_child_table = binhash_create(0);
    if (env_gen == 0)
	env_gen = vstring_alloc(100);

    /*
     * Sanity checks. The master_avail module is supposed to know what it is
     * doing.
     */
    if (!MASTER_LIMIT_OK(serv->max_proc, serv->total_proc))
	msg_panic("%s: at process limit %d", myname, serv->total_proc);
    if (serv->avail_proc > 0)
	msg_panic("%s: processes available: %d", myname, serv->avail_proc);
    if (serv->flags & MASTER_FLAG_THROTTLE)
	msg_panic("%s: throttled service: %s", myname, serv->path);

    /*
     * Create a child process and connect parent and child via the status
     * pipe.
     */
    master_generation += 1;
    switch (pid = fork()) {

	/*
	 * Error. We're out of some essential resource. Best recourse is to
	 * try again later.
	 */
    case -1:
	msg_warn("%s: fork: %m -- throttling", myname);
	master_throttle(serv);
	return;

	/*
	 * Child process. Redirect child stdin/stdout to the parent-child
	 * connection and run the requested command. Leave child stderr
	 * alone. Disable exit handlers: they should be executed by the
	 * parent only.
	 * 
	 * When we reach the process limit on a public internet service, we
	 * create stress-mode processes until the process count stays below
	 * the limit for some amount of time. See master_avail_listen().
	 */
    case 0:
	msg_cleanup((void (*) (void)) 0);	/* disable exit handler */
	closelog();				/* avoid filedes leak */

	if (master_flow_pipe[0] <= MASTER_FLOW_READ)
	    msg_fatal("%s: flow pipe read descriptor <= %d",
		      myname, MASTER_FLOW_READ);
	if (DUP2(master_flow_pipe[0], MASTER_FLOW_READ) < 0)
	    msg_fatal("%s: dup2: %m", myname);
	if (close(master_flow_pipe[0]) < 0)
	    msg_fatal("close %d: %m", master_flow_pipe[0]);

	if (master_flow_pipe[1] <= MASTER_FLOW_WRITE)
	    msg_fatal("%s: flow pipe read descriptor <= %d",
		      myname, MASTER_FLOW_WRITE);
	if (DUP2(master_flow_pipe[1], MASTER_FLOW_WRITE) < 0)
	    msg_fatal("%s: dup2: %m", myname);
	if (close(master_flow_pipe[1]) < 0)
	    msg_fatal("close %d: %m", master_flow_pipe[1]);

	close(serv->status_fd[0]);		/* status channel */
	if (serv->status_fd[1] <= MASTER_STATUS_FD)
	    msg_fatal("%s: status file descriptor collision", myname);
	if (DUP2(serv->status_fd[1], MASTER_STATUS_FD) < 0)
	    msg_fatal("%s: dup2 status_fd: %m", myname);
	(void) close(serv->status_fd[1]);

	for (n = 0; n < serv->listen_fd_count; n++) {
	    if (serv->listen_fd[n] <= MASTER_LISTEN_FD + n)
		msg_fatal("%s: listen file descriptor collision", myname);
	    if (DUP2(serv->listen_fd[n], MASTER_LISTEN_FD + n) < 0)
		msg_fatal("%s: dup2 listen_fd %d: %m",
			  myname, serv->listen_fd[n]);
	    (void) close(serv->listen_fd[n]);
	}
	vstring_sprintf(env_gen, "%s=%o", MASTER_GEN_NAME, master_generation);
	if (putenv(vstring_str(env_gen)) < 0)
	    msg_fatal("%s: putenv: %m", myname);
	if (serv->stress_param_val && serv->stress_expire_time > event_time())
	    serv->stress_param_val[0] = CONFIG_BOOL_YES[0];

	execvp(serv->path, serv->args->argv);
	msg_fatal("%s: exec %s: %m", myname, serv->path);
	/* NOTREACHED */

	/*
	 * Parent. Fill in a process member data structure and set up links
	 * between child and process. Say this process has become available.
	 * If this service has a wakeup timer that is turned on only when the
	 * service is actually used, turn on the wakeup timer.
	 */
    default:
	if (msg_verbose)
	    msg_info("spawn command %s; pid %d", serv->path, pid);
	proc = (MASTER_PROC *) mymalloc(sizeof(MASTER_PROC));
	proc->serv = serv;
	proc->pid = pid;
	proc->gen = master_generation;
	proc->use_count = 0;
	proc->avail = 0;
	binhash_enter(master_child_table, (void *) &pid,
		      sizeof(pid), (void *) proc);
	serv->total_proc++;
	master_avail_more(serv, proc);
	if (serv->flags & MASTER_FLAG_CONDWAKE) {
	    serv->flags &= ~MASTER_FLAG_CONDWAKE;
	    master_wakeup_init(serv);
	    if (msg_verbose)
		msg_info("start conditional timer for %s", serv->name);
	}
	return;
    }
}

/* master_delete_child - destroy child process info */

static void master_delete_child(MASTER_PROC *proc)
{
    MASTER_SERV *serv;

    /*
     * Undo the things that master_spawn did. Stop the process if it still
     * exists, and remove it from the lookup tables. Update the number of
     * available processes.
     */
    serv = proc->serv;
    serv->total_proc--;
    if (proc->avail == MASTER_STAT_AVAIL)
	master_avail_less(serv, proc);
    else
	master_avail_listen(serv);
    binhash_delete(master_child_table, (void *) &proc->pid,
		   sizeof(proc->pid), (void (*) (void *)) 0);
    myfree((void *) proc);
}

/* master_reap_child - reap dead children */

void    master_reap_child(void)
{
    MASTER_SERV *serv;
    MASTER_PROC *proc;
    MASTER_PID pid;
    WAIT_STATUS_T status;

    /*
     * Pick up termination status of all dead children. When a process failed
     * on its first job, assume we see the symptom of a structural problem
     * (configuration problem, system running out of resources) and back off.
     */
    while ((pid = waitpid((pid_t) - 1, &status, WNOHANG)) > 0) {
	if (msg_verbose)
	    msg_info("master_reap_child: pid %d", pid);
	if ((proc = (MASTER_PROC *) binhash_find(master_child_table,
					(void *) &pid, sizeof(pid))) == 0) {
	    if (init_mode)
		continue;			/* non-Postfix process */
	    msg_panic("master_reap: unknown pid: %d", pid);
	}
	serv = proc->serv;

#define MASTER_KILL_SIGNAL	SIGTERM
#define MASTER_SENT_SIGNAL(serv, status) \
	(MASTER_MARKED_FOR_DELETION(serv) \
	    && WTERMSIG(status) == MASTER_KILL_SIGNAL)

	/*
	 * XXX The code for WIFSTOPPED() is here in case some buggy kernel
	 * reports WIFSTOPPED() events to a Postfix daemon's parent process
	 * (the master(8) daemon) instead of the tracing process (e.g., gdb).
	 * 
	 * The WIFSTOPPED() test prevents master(8) from deleting its record of
	 * a child process that is stopped. That would cause a master(8)
	 * panic (unknown child) when the child terminates.
	 */
	if (!NORMAL_EXIT_STATUS(status)) {
	    if (WIFSTOPPED(status)) {
		msg_warn("process %s pid %d stopped by signal %d",
			 serv->path, pid, WSTOPSIG(status));
		continue;
	    }
	    if (WIFEXITED(status))
		msg_warn("process %s pid %d exit status %d",
			 serv->path, pid, WEXITSTATUS(status));
	    if (WIFSIGNALED(status) && !MASTER_SENT_SIGNAL(serv, status))
		msg_warn("process %s pid %d killed by signal %d",
			 serv->path, pid, WTERMSIG(status));
	    /* master_delete_children() throttles first, then kills. */
	    if (proc->use_count == 0
		&& (serv->flags & MASTER_FLAG_THROTTLE) == 0) {
		msg_warn("%s: bad command startup -- throttling", serv->path);
		master_throttle(serv);
	    }
	}
	master_delete_child(proc);
    }
}

/* master_delete_children - delete all child processes of service */

void    master_delete_children(MASTER_SERV *serv)
{
    BINHASH_INFO **list;
    BINHASH_INFO **info;
    MASTER_PROC *proc;

    /*
     * XXX turn on the throttle so that master_reap_child() doesn't. Someone
     * has to turn off the throttle in order to stop the associated timer
     * request, so we might just as well do it at the end.
     */
    master_throttle(serv);
    for (info = list = binhash_list(master_child_table); *info; info++) {
	proc = (MASTER_PROC *) info[0]->value;
	if (proc->serv == serv)
	    (void) kill(proc->pid, MASTER_KILL_SIGNAL);
    }
    while (serv->total_proc > 0)
	master_reap_child();
    myfree((void *) list);
    master_unthrottle(serv);
}
