/*
 * economy shutdown, for mastodon
 *
 *  Copyright (c) 2000 David Parsons. All rights reserved.
 *
 *  This software is released under a berkeley-style copyright; the
 *  details can be found in the file COPYRIGHT.
 *
 * usage: shutdown [-rhck] [-t sec] [-nf] {time|+mins|now} message
 *
 * -r:   reboot
 * -h:   halt
 * -c:   cancel
 * -k:   send the messages, but don't shut down
 * -t:   ignored
 * -n:   ignored
 * -f:   ignored
 * -q:   don't ask for a message
 * -m:   ask for a message
 */

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdarg.h>
#include <malloc.h>
#include "init.h"

extern int getopt();
extern int opterr;
extern int optind;
extern char *optarg;

extern void why_why_why_are_we(char *reason, char *bfr, int bfrlen);

char *pgm;		/* program name, for diagnostics */

#define LOCK	PREFIX "/var/lock/shutdown.pid"
#define NOLOGIN	PREFIX "/etc/nologin"

int reboot_flag = 0,	/* we want to reboot */
    halt_flag = 0,	/* we want to halt */
    dryrun = 0,		/* just give messages, don't do anything */
    cancel = 0,		/* cancel an already running shutdown */
    no_reason = 1;	/* we don't want to give a reason why */

char msg[4000];		/* shutdown message */


/*
 * cancel a running shutdown (signalled by kill -HUP or shutdown -c)
 */
void
dont_do_it(int sig)
{
    fprintf(stderr, "shutdown cancelled\n");
    unlink(LOCK);
    unlink(NOLOGIN);
    exit(0);
} /* dont_do_it */


/*
 * spit out a usage message, then die
 */
void
usage()
{
    fprintf(stderr, "usage: %s [-rhck] {+mins|now} [message]\n", pgm);
    exit(1);
} /* usage */


/* 
 * spit out an error message, then die
 */
void
error(char *fmt, ...)
{
    va_list ptr;

    va_start(ptr, fmt);
    vfprintf(stderr, fmt, ptr);
    va_end(ptr);
    fputc('\n', stderr);
    exit(1);
} /* error */


/*
 * stop a shutdown from continuing to run amuk
 */
void
cancel_shutdown()
{
    int lockf, size;
    char buf[80];
    pid_t pid;

    if ( (lockf = open(LOCK, O_RDWR)) >= 0)
	if ( (size = read(lockf, buf, sizeof buf)) > 0)
	    if (sscanf(buf, "%d", &pid) > 0)
		if (kill(pid, SIGHUP) == -1)
		    error("%s", strerror(errno));
		else
		    exit(0);
    error("shutdown is not running");
} /* cancel_shutdown */


/*
 * give a message about the impending doom.
 */
void
alert(int time)
{
    int minutes = (time % 60);
    int hours   = (time / 60) % 24;
    int size;

    static char *fmt = 0;
    static int fmtsize = 200;

    if (fmt == 0)
	fmt = malloc(fmtsize+strlen(msg));

    size = snprintf(fmt, fmtsize, "\r\nSystem shutting down ");

    if (hours > 0) {
	size += snprintf(fmt+size, fmtsize-size,
			 "in %d hour%s", hours, (hours!=1)?"s":"");
	if (minutes)
	    size += snprintf(fmt+size, fmtsize-size,
			     " and %d minute%s", minutes, (minutes!=1)?"s":"");
    }
    else if (minutes > 0) {
	size += snprintf(fmt+size, fmtsize-size,
		    "in %d minute%s", minutes, (minutes!=1)?"s":"");
	if (minutes < 5)
	    size += snprintf(fmt+size, fmtsize-size, " -- log out now");
    }
    else
	size += snprintf(fmt+size, fmtsize-size, "immediately");

    if (msg[0]) {
	strcat(fmt, "\r\n\r\n");
	strcat(fmt, msg);
    }

    wall(fmt);
} /* alert */


/*
 * wait a while before the next shutdown alert
 *
 * we want to give an alert every 1/2'th the remaining time,
 * except that we want to give alerts at 15, 10, 5, 1, and 0
 * minutes left.
 */
int
set_alarm(int time_left)
{
    int alarm_time;
    int status;
    struct timeval tv;
    time_t then, now;

    if (time_left >= 40) {
	alarm_time = time_left/2;
	time_left /= 2;
    }
    else if (time_left > 15) {
	alarm_time = time_left-15;
	time_left = 15;
    }
    else if (time_left > 10) {
	alarm_time = time_left-10;
	time_left = 10;
    }
    else if (time_left > 5) {
	alarm_time = time_left-5;
	time_left = 5;
    }
    else if (time_left > 1) {
	alarm_time = time_left-1;
	time_left  = 1;
    }
    else {
	alarm_time = 1;
	time_left = 0;
    }

#if !DEBUG
    alarm_time *= 60;	/* convert the alarm time to seconds */
#endif

    time(&now);
    then = now + alarm_time;

    while (now < then) {
	tv.tv_sec = then-now;
	tv.tv_usec = 0;
	select(0, 0, 0, 0, &tv);
	time(&now);
    }
    return time_left;
} /* set_alarm */


/*
 * write the nologin file
 */
void
write_nologin_file(int time_left)
{
    FILE *f;
    time_t when;

    time(&when);
    when += time_left*60;

    f = fopen(NOLOGIN, "w");
    fprintf(f, "The system will be rebooted at %s", ctime(&when));
    fclose(f);
} /* write_nologin_file */


/*
 * shutdown
 */
main(int argc, char **argv)
{
    int opt;
    int lockf;
    int x;
    int size;
    int time_til_shutdown = 0;
    char buf[80];
    pid_t pid;

    pgm = basename(argv[0]);

    while ( (opt=getopt(argc, argv, "rhckt:nfmq")) != EOF) {
	switch (opt) {
	case 'r':	reboot_flag++;
			break;
	case 'h':	halt_flag++;
			break;
	case 'k':	dryrun++;
			break;
	case 'c':	cancel++;
			break;
	case 't':
	case 'n':
	case 'f':	break;
	case 'm':	no_reason = 0;
			break;
	case 'q':	no_reason = 1;
			break;
	default:	usage();
	}
    }
    argc -= optind;
    argv += optind;

    if (reboot_flag && halt_flag)
	error("I can't both reboot and halt at the same time");

    if (cancel && (dryrun || halt_flag || reboot_flag) )
	error("I can't both shutdown and cancel shutdown at the same time");

    if (cancel)
	cancel_shutdown();

    if (argc < 1)
	usage();

    if (strcmp(argv[0], "now") == 0)
	time_til_shutdown=0;
    else if (argv[0][0] == '+') {
	char *p;
	for (p = &argv[0][1]; *p; ++p)
	    if (!isdigit(*p))
		error("garbled shutdown time");
	time_til_shutdown=atoi(&argv[0][1]);
    }
    else
	error("garbled shutdown time");

    if (time_til_shutdown > 60*24)
	error("I don't want to shut down that far in the future");

    if (argc < 2) {
	if (isatty(0) && !no_reason)
	    why_why_why_are_we("the reason for the shutdown", msg, sizeof msg);
    }
    else {
	for (x = 1; x < argc; x++) {
	    if (x > 1)
		strcat(msg, " ");
	    strcat(msg, argv[x]);
	}
	strcat(msg, "\r\n");
    }

    if ( (lockf = open(LOCK, O_RDWR)) >= 0) {
	if ( (size = read(lockf, buf, sizeof buf)) > 0) {
	    if (sscanf(buf, "%d", &pid) > 0 && kill(pid, 0) == 0)
		error("already running (pid %d)", pid);
	}
	close(lockf);
	lseek(lockf, 0, 0);
	unlink(LOCK);
    }
    else if ( (lockf = open(LOCK, O_WRONLY|O_CREAT|O_EXCL, 0644)) < 0)
	error("cannot open lockfile: %s", strerror(errno));

    snprintf(buf, sizeof buf, "%d\n", getpid());
    write(lockf, buf, strlen(buf));
    close(lockf);

    signal(SIGHUP, dont_do_it);
    signal(SIGTERM, dont_do_it);
    signal(SIGQUIT, dont_do_it);

    if ((time_til_shutdown < 5) && !dryrun)
	write_nologin_file(time_til_shutdown);

    while (time_til_shutdown > 0) {
	alert(time_til_shutdown);
	if ((time_til_shutdown == 5) && !dryrun)
	    write_nologin_file(time_til_shutdown);
	time_til_shutdown = set_alarm(time_til_shutdown);
    }

    alert(0);

    if (dryrun)
	exit(0);

    signal(SIGHUP, SIG_IGN);
    signal(SIGTERM, SIG_IGN);

    /* it's the apocolypse */
    unlink(LOCK);
    unlink(NOLOGIN);

    if (kill(1, halt_flag ? S_HALT : S_REBOOT) == 0)
	exit(0);
    perror("when signalling init");
    exit(1);
} /* shutdown */
