/*
 * daemon: go do something in the background.
 * If called as 'go', redirect stdout to 'stdout', 'stderr' to stderr,
 * and announce 'go just finished' when the job is done.
 * If called as 'daemon', redirect stdout and stderr to /dev/null, then
 * attempt to keep the process alive indefinitely.
 *
 * usage: daemon [-l lockfile] command args
 *        go [-q] command args
 *
 * Copyright (c) 1996 David L Parsons 
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms are permitted
 * provided that the above copyright notice and this paragraph are
 * duplicated in all such forms and that any documentation,
 * advertising materials, and other materials related to such
 * distribution and use acknowledge that the software was developed
 * by David L Parsons (orc@pell.chi.il.us).  My name may not be used
 * to endorse or promote products derived from this software without
 * specific prior written permission.  THIS SOFTWARE IS PROVIDED
 * ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
 * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE.
 */

static const char rcsid[] = "Mastodon $Id: s.daemon.c 1.2 99/05/01 19:01:32-00:00 orc $";

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/fcntl.h>
#include <stdarg.h>
#include <errno.h>
#include <syslog.h>


enum Action {go, daemon} action;	/* What are we doing */
char *pgm;				/* our program name, for error() */
char verbose = 1;			/* should go alert when done? */
int signaled=0;				/* what signal stopped daemon */
char *lockfile = 0;			/* lockfile for this process */


/*
 * print some error message, with gory details if errno is set,
 * then exit
 */
error(char *fmt, ...)
{
    int myerrno = errno;

    va_list ptr;
    va_start(ptr, fmt);
    fprintf(stderr, "%s: ", pgm);
    vfprintf(stderr, fmt, ptr);
    if (myerrno != 0)
	fprintf(stderr, ": %s", strerror(myerrno));
    fputc('\n', stderr);
    va_end(ptr);
    exit(2);
} /* error */


/*
 * print some usage message appropriate to the way we're invoked,
 * then exit
 */
usage()
{
    if (action == go)
	fprintf(stderr, "usage: go [-q] command [args]\n");
    else
	fprintf(stderr, "usage: daemon [-l lock] command [args]\n");
    exit(255);
} /* usage */


/*
 * go: run some process in the background and attempt to notify the
 * caller when it finishes
 */
go_and_do_it(int argc, char **argv)
{
    pid_t child;
    pid_t grandchild;
    int status;
    FILE *messagef;

    /* point stdout and stderr into files (if they're not already redirected)
     */
    if (isatty(fileno(stdout)))
	freopen("stdout", "w", stdout);
    if (isatty(fileno(stderr)))
	freopen("stderr", "a", stderr);

    /* drop ourselves WAY into the background, then orphan ourself
     * and go
     */
    nice(10);
    child = fork();
    if (child < 0)
	error("Can't spawn child");
    else if (child > 0)
	exit(0);
    grandchild = fork();
    if (grandchild < 0)
	error("Can't spawn grandchild");
    else if (grandchild == 0) {
	setsid();		/* put the grandchild into a different
				 * process group, so that if we log out
				 * it will continue to live.
				 */
	execvp(argv[0], argv);
	error("Can't exec process");
	exit(1);
    }
    else {
	waitpid(grandchild, &status, 0);	/* wait for child */
	if (verbose && freopen("/dev/tty", "a", stderr))
	    fprintf(stderr, "Go just finished!\n");
	exit(status);
    }
} /* go_and_do_it */


/*
 * signal information, for catching and ignoring various signals
 */
struct Sig{
    int signo;		/* the signal */
    char *desc;		/* a description of what it is */
};

/*
 * the signals that are caught by gotcha() and passed to the child
 */
static struct Sig siglist[] = {
    { SIGHUP,  "Hangup" },
    { SIGINT,  "Interrupted" },
    { SIGQUIT, "Quit" },
    { SIGABRT, "Abort" },
    { SIGKILL, "Killed" },
    { SIGTERM, "Terminated" }
};
#define NR_SIGS	(sizeof siglist / sizeof siglist[0])

/*
 * signals that daemon will ignore.
 */
static struct Sig ignorelist[] = {
    { SIGUNUSED, "Unused signal" },
    { SIGUSR1,   "User signal 1" },
    { SIGUSR2,   "User signal 2" },
    { SIGPIPE,   "Broken pipe" },
    { SIGABRT,   "Abort"       },
    { SIGFPE,    "Floating point exception" },
    { SIGTRAP,   "Trap" },
    { SIGALRM,   "Alarm clock" },
};
#define NR_IGNORE (sizeof ignorelist / sizeof ignorelist[0])

/*
 * what's the signal, kenneth?
 */
char *
signame(int sig)
{
    int x;
    static char msg[22];

    for (x=0; x<NR_SIGS; x++)
	if (siglist[x].signo == sig)
	    return siglist[x].desc;
    for (x=0; x<NR_IGNORE; x++)
	if (ignorelist[x].signo == sig)
	    return ignorelist[x].desc;
    sprintf(msg, "signal $d", sig);
    return msg;
}


/*
 * catch a signal, pass it to the children
 */
void
gotcha(int signo)
{
    signaled = signo;
    syslog(LOG_INFO, "%s\n", signame(signo));
    ignore_signals();
    kill(-getpgrp(), signo);
    catch_signals();
}


/*
 * pass all termination signals to gotcha
 */
catch_signals()
{
    int x;

    for (x=0; x<NR_SIGS; x++)
	signal(siglist[x].signo, gotcha);
    for (x=0; x<NR_IGNORE; x++)
	signal(ignorelist[x].signo, SIG_IGN);
}


/*
 * ignore all termination signals
 */
ignore_signals()
{
    int x;

    for (x=0; x<NR_SIGS; x++)
	signal(siglist[x].signo, SIG_IGN);
}


/*
 * allow all signals
 */
allow_signals()
{
    int x;
    for (x=0; x<NR_SIGS; x++)
	signal(siglist[x].signo, SIG_DFL);
    for (x=0; x<NR_IGNORE; x++)
	signal(ignorelist[x].signo, SIG_DFL);
}


/*
 * start a daemon, keep restarting it forever
 */
summon_a_daemon(int argc, char **argv)
{
    pid_t child;
    pid_t grandchild;
    pid_t pid_waited_for;
    int status;
    int restarts;
    unsigned long now, respawn_time;
    FILE *tty;

    child = fork();
    signal(SIGHUP, exit);
    if (child < 0)
	error("Can't spawn child");
    else if (child == 0) {
	if (lockfile)
	    setlock();
	else {
	    printf("%d\n", getpid());	/* tell the world our process id */
	    fflush(stdout);
	}
	kill(getppid(), SIGHUP);	/* and tell our parent to die */
	if (isatty(fileno(stdout)))
	    freopen("/dev/null", "w", stdout);
	if (isatty(fileno(stderr)))
	    freopen("/dev/null", "w", stderr);
	setsid();			/* make ourself a new process grp */

	openlog(argv[0], LOG_DAEMON, LOG_PID);

	catch_signals();

	time(&respawn_time);
	respawn_time += 300L;
	restarts = 0;
	while (1) {
	    ignore_signals();
	    grandchild = fork();
	    catch_signals();
	    if (grandchild < 0) {
		syslog(LOG_ERR, "Can't spawn grandchild: %m");
		goto check_times;
	    }
	    else if (grandchild == 0) {
		rewind(stdin);
		allow_signals();
		execv(argv[0], argv);
		syslog(LOG_ERR, "Can't exec process: %m");
		exit(2);
	    }
	    else {
		syslog(LOG_INFO, "Waiting for process %d", grandchild);
		do {
		    pid_waited_for = waitpid(grandchild, &status, 0);
		    if (signaled) {
			syslog(LOG_DEBUG, "> %s", signame(signaled));
			kill(grandchild, signaled);
		    }
		    else
			syslog(LOG_DEBUG, "waitpid; pid = %d, status = %d",
					pid_waited_for, status);
		} while (pid_waited_for != grandchild);
		if (signaled) {
		    if (kill(grandchild, 0) != -1) {
			syslog(LOG_ERR, "%s did not die!", argv[0]);
			kill(grandchild, SIGINT);
		    }
		    syslog(LOG_INFO, "shutting down");
		    if (lockfile)
			unlink(lockfile);
		    exit(0);
		}
	check_times:
		/* If the process is repeatedly crashing, drop an alert
		 * into syslog then sleep for five minutes before starting
		 * the parade again.
		 */
		time(&now);
		if (++restarts > 30) {
		    if (now < respawn_time) {
			syslog(LOG_ERR, "process spawning too quickly; sleeping 5 minutes");
			catch_signals();
			sleep(300);
		    }
		    restarts=0;
		    time(&respawn_time);
		    respawn_time += 300;
		}
	    } /* grandchild loop */
	} /* while 1 */
    } /* child loop */
    else {
	wait(&status);
	exit(0);
    }
} /* summon_a_daemon */


/*
 * setlock() writes out the lockfile
 */
setlock()
{
    int fd;
    FILE *p;
    int lock_owner;

    if ((fd = open(lockfile, O_WRONLY|O_EXCL|O_CREAT, 0600)) < 0) {
	/* lock already exists; read the PID out of it and see who
	 * owns it */
	p = fopen(lockfile, "r");
	if (fscanf(p, "%d", &lock_owner) < 1) {
	    fprintf(stderr, "%s: lock in use? (anonymous owner)\n", lockfile);
	    exit(1);
	}
	fclose(p);
	if (kill(0,lock_owner) != -1 || errno != ESRCH) {
	    fprintf(stderr, "%s: lock in use (pid %d)\n", lockfile, lock_owner);
	    exit(1);
	}
	fprintf(stderr, "%s: stale lock found\n", lockfile);
	fd = open(lockfile, O_WRONLY);
    }
    /* write our pid into the lockfile */
    p = fdopen(fd, "w");
    fprintf(p, "%d\n", getpid());
    if (fclose(p) != 0) {
	fprintf(stderr, "%s: %s\n", lockfile, strerror(errno));
	unlink(lockfile);
	exit(1);
    }
} /* setlock */


/*
 * go and/or daemon, in schitzophrenic flesh
 */
main(int argc, char **argv)
{
    char *p;
    pid_t newgrp;
    int c;
    char *opts;

    if (p = strrchr(argv[0], '/'))
	pgm = 1+p;
    else
	pgm = argv[0];

    if (strcasecmp(pgm, "go") == 0) {
	action = go;
	opts = "q";
    }
    else if (strcasecmp(pgm, "daemon") == 0) {
	action = daemon;
	opts = "l:";
    }
    else {
	fprintf(stderr, "called with a funny name [%s] -- I don't know what to do!\n", pgm);
	exit(1);
    }

    opterr=1;
    while ((c=getopt(argc, argv, opts)) != EOF) {
	switch (c) {
	case 'q':
	    verbose=0;
	    break;
	case 'l':
	    if (lockfile) {
		fprintf(stderr, "only one lock allowed per daemon\n");
		exit(1);
	    }
	    lockfile = optarg;
	    break;
	default:
	    usage();
	    /*NOTREACHED*/
	    break;
	}
    }
    argv += (optind-1);
    argc -= (optind-1);

    /* neither go nor daemon think much of stdin
     */
    if (isatty(fileno(stdin)))
	freopen("/dev/zero", "r", stdin);

    /* daemonize ourself and let fly */
    if (argc < 2)
	usage();
    ++argv, --argc;

    switch (action) {
    case go:
	go_and_do_it(argc, argv);
	break;

    case daemon:
	summon_a_daemon(argc, argv);
	break;

    default:
	errno = 0;
	error("In daemon switch without a valid action -- CAN'T HAPPEN?");
    }
    exit(0);
} /* main */
