/*	$NetBSD: wdogctl.c,v 1.21 2015/05/06 23:08:30 pgoyette Exp $	*/

/*-
 * Copyright (c) 2000 Zembu Labs, Inc.
 * All rights reserved.
 *
 * Author: Jason R. Thorpe <thorpej@zembu.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by Zembu Labs, Inc.
 * 4. Neither the name of Zembu Labs nor the names of its employees may
 *    be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY ZEMBU LABS, INC. ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WAR-
 * RANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DIS-
 * CLAIMED.  IN NO EVENT SHALL ZEMBU LABS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#include <sys/cdefs.h>

#ifndef lint
__RCSID("$NetBSD: wdogctl.c,v 1.21 2015/05/06 23:08:30 pgoyette Exp $");
#endif


#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/wdog.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <syslog.h>
#include <unistd.h>
#include <string.h>
#include <paths.h>

static void	enable_kernel(const char *, u_int);
static void	enable_user(const char *, u_int, int);
static void	enable_ext(const char *, u_int);
static void	tickle_ext(void);
static void	disable(void);
static void	prep_wmode(struct wdog_mode *, int,  const char *, u_int);
static void	list_timers(void);
__dead static void	usage(void);

static int	Aflag;

/* Caution -- ordered list; entries >= CMD_EXT_TICKLE set timers */
enum	cmd {
	CMD_NONE,	/* No verb given */
	CMD_DISABLE,
	CMD_DOTICKLE,
	CMD_EXT_TICKLE,
	CMD_KERN_TICKLE,
	CMD_NOCANCEL_TICKLE,
	CMD_USER_TICKLE
};

int
main(int argc, char *argv[])
{
	enum cmd command = CMD_NONE;
	int period_flag = 0;
	int ch, tmp;
	u_int period = WDOG_PERIOD_DEFAULT;

	while ((ch = getopt(argc, argv, "Adekp:utx")) != -1) {
		switch (ch) {
		case 'A':
			Aflag = 1;
			break;

		case 'd':
			if (command != CMD_NONE)
				usage();
			command = CMD_DISABLE;
			break;

		case 'e':
			if (command != CMD_NONE)
				usage();
			command = CMD_EXT_TICKLE;
			break;

		case 'k':
			if (command != CMD_NONE)
				usage();
			command = CMD_KERN_TICKLE;
			break;

		case 't':
			if (command != CMD_NONE)
				usage();
			command = CMD_DOTICKLE;
			break;

		case 'p':
			period_flag = 1;
			tmp = atoi(optarg);
			if (tmp < 0)
				usage();
			period = (unsigned int)tmp;
			break;

		case 'x':
		case 'u':
			if (command != CMD_NONE)
				usage();
			command =
			    (ch == 'u') ? CMD_USER_TICKLE : CMD_NOCANCEL_TICKLE;
			break;

		default:
			usage();
		}
	}
	
	argc -= optind;
	argv += optind;

	if (command < CMD_EXT_TICKLE) {
		if (Aflag || period_flag)
			usage();
		if (argc != 0)
			usage();
	} else if (argc != 1)
		usage();

	switch (command) {
	case CMD_NONE:
		list_timers();
		break;
	case CMD_DISABLE:
		disable();
		break;
	case CMD_DOTICKLE:
		tickle_ext();
		break;
	case CMD_EXT_TICKLE:
		enable_ext(argv[0], period);
		break;
	case CMD_KERN_TICKLE:
		enable_kernel(argv[0], period);
		break;
	case CMD_NOCANCEL_TICKLE:
	case CMD_USER_TICKLE:
		enable_user(argv[0], period, command == CMD_USER_TICKLE);
		break;
	}
	exit(EXIT_SUCCESS);
}

static void
prep_wmode(struct wdog_mode *wp, int mode,  const char *name, u_int period)
{
	if (strlen(name) >= WDOG_NAMESIZE)
		errx(EXIT_FAILURE, "invalid watchdog timer name: %s", name);

	strlcpy(wp->wm_name, name, sizeof(wp->wm_name));
	wp->wm_mode = mode;
	wp->wm_period = period;
	if (Aflag)
		wp->wm_mode |= WDOG_FEATURE_ALARM;
}

static void
enable_kernel(const char *name, u_int period)
{
	struct wdog_mode wm;
	int fd;

	prep_wmode(&wm, WDOG_MODE_KTICKLE, name, period);

	fd = open(_PATH_WATCHDOG, O_RDWR, 0644);
	if (fd == -1)
		err(EXIT_FAILURE, "open %s", _PATH_WATCHDOG);

	if (ioctl(fd, WDOGIOC_SMODE, &wm) == -1)
		err(EXIT_FAILURE, "WDOGIOC_SMODE");

	(void)close(fd);
}

static void
enable_ext(const char *name, u_int period)
{
	struct wdog_mode wm;  
	int fd;

	prep_wmode(&wm, WDOG_MODE_ETICKLE, name, period);

	fd = open(_PATH_WATCHDOG, O_RDWR, 0644);
	if (fd == -1)
		err(EXIT_FAILURE, "open %s", _PATH_WATCHDOG);
	if (ioctl(fd, WDOGIOC_SMODE, &wm) == -1) {
		err(EXIT_FAILURE, "WDOGIOC_SMODE");
	}
	if (ioctl(fd, WDOGIOC_TICKLE) == -1)
		syslog(LOG_EMERG, "unable to tickle watchdog timer %s: %m",
		    wm.wm_name);

	(void)close(fd);
	return;
}

static void
enable_user(const char *name, u_int period, int cancel_on_close)
{
	struct wdog_mode wm;  
	struct timespec ts;
	pid_t tickler;
	int fd, rv;

	prep_wmode(&wm,
	    (cancel_on_close) ? WDOG_MODE_UTICKLE : WDOG_MODE_ETICKLE, name,
	    period);

	fd = open(_PATH_WATCHDOG, O_RDWR, 0644);
	if (fd == -1)
		err(EXIT_FAILURE, "open %s", _PATH_WATCHDOG);

	/* ...so we can log failures to tickle the timer. */
	openlog("wdogctl", LOG_PERROR|LOG_PID, LOG_DAEMON);

	/*
	 * We fork a child process which detaches from the controlling
	 * terminal once the timer is armed, and tickles the timer
	 * until we send it a SIGTERM.
	 */
	tickler = fork();
	if (tickler == -1)
		err(EXIT_FAILURE, "unable to fork tickler process");
	else if (tickler != 0) {
		if (ioctl(fd, WDOGIOC_SMODE, &wm) == -1) {
			(void)kill(tickler, SIGTERM);
			err(EXIT_FAILURE, "WDOGIOC_SMODE");
		}
		(void)close(fd);
		return;
	}


	/*
	 * Wait for the watchdog to be armed.  When it is, loop,
	 * tickling the timer, then waiting 1/2 the period before
	 * doing it again.
	 *
	 * If the parent fails to enable the watchdog, it will kill
	 * us.
	 */
	do {
		rv = ioctl(fd, WDOGIOC_WHICH, &wm);
	} while (rv == -1);

	if (ioctl(fd, WDOGIOC_TICKLE) == -1)
		syslog(LOG_EMERG, "unable to tickle watchdog timer %s: %m",
		    wm.wm_name);

	/*
	 * Now detach from the controlling terminal, and just run
	 * in the background.  The kernel will keep track of who
	 * we are, each time we tickle the timer.
	 */
	if (daemon(0, 0) == -1) {
		/*
		 * We weren't able to go into the background.  When
		 * we exit, the kernel will disable the watchdog so
		 * that the system won't die.
		 */
		err(EXIT_FAILURE, "unable to detach from terminal");
	}

	if (ioctl(fd, WDOGIOC_TICKLE) == -1)
		syslog(LOG_EMERG, "unable to tickle watchdog timer %s: %m",
		    wm.wm_name);

	for (;;) {
		ts.tv_sec = wm.wm_period / 2;
		ts.tv_nsec = 0;
		(void)nanosleep(&ts, NULL);

		if (ioctl(fd, WDOGIOC_TICKLE) == -1)
			syslog(LOG_EMERG,
			    "unable to tickle watchdog timer %s: %m",
			    wm.wm_name);
	}
	/* NOTREACHED */
}

static void
tickle_ext(void)
{
	int fd;

	fd = open(_PATH_WATCHDOG, O_RDWR, 0644);
	if (fd == -1)
		err(EXIT_FAILURE, "open %s", _PATH_WATCHDOG);
	if (ioctl(fd, WDOGIOC_TICKLE) == -1)
		fprintf(stderr, "Cannot tickle timer\n");

	(void)close(fd);
}

static void
disable(void)
{
	struct wdog_mode wm;
	pid_t tickler;
	int fd, mode;

	fd = open(_PATH_WATCHDOG, O_RDWR, 0644);
	if (fd == -1)
		err(EXIT_FAILURE, "open %s", _PATH_WATCHDOG);

	if (ioctl(fd, WDOGIOC_WHICH, &wm) == -1) {
		printf("No watchdog timer running.\n");
		(void)close(fd);
		return;
	}
	mode = wm.wm_mode & WDOG_MODE_MASK;

	/*
	 * If the timer is running in UTICKLE mode, we need
	 * to kill the wdogctl(8) process that is tickling
	 * the timer.
	 */
	if (mode == WDOG_MODE_UTICKLE) {
		if (ioctl(fd, WDOGIOC_GTICKLER, &tickler) == -1)
			err(EXIT_FAILURE, "WDOGIOC_GTICKLER");
		(void)close(fd);
		(void)kill(tickler, SIGTERM);
	} else {
		wm.wm_mode = WDOG_MODE_DISARMED;
		if (ioctl(fd, WDOGIOC_SMODE, &wm) == -1) {
			err(EXIT_FAILURE, "unable to disarm watchdog %s",
			    wm.wm_name);
		}
		(void)close(fd);
	}
}

static void
list_timers(void)
{
	struct wdog_conf wc;
	struct wdog_mode wm;
	char *buf, *cp;
	int fd, count, i, mode;
	pid_t tickler;

	fd = open(_PATH_WATCHDOG, O_RDONLY, 0644);
	if (fd == -1)
		err(EXIT_FAILURE, "open %s", _PATH_WATCHDOG);

	wc.wc_names = NULL;
	wc.wc_count = 0;

	if (ioctl(fd, WDOGIOC_GWDOGS, &wc) == -1)
		err(EXIT_FAILURE, "ioctl WDOGIOC_GWDOGS for count");

	count = wc.wc_count;
	if (count == 0) {
		printf("No watchdog timers present.\n");
		goto out;
	}

	buf = malloc(count * WDOG_NAMESIZE);
	if (buf == NULL)
		err(EXIT_FAILURE, "malloc %d byte for watchdog names",
		    count * WDOG_NAMESIZE);
	
	wc.wc_names = buf;
	if (ioctl(fd, WDOGIOC_GWDOGS, &wc) == -1)
		err(EXIT_FAILURE, "ioctl WDOGIOC_GWDOGS for names");

	count = wc.wc_count;
	if (count == 0) {
		printf("No watchdog timers present.\n");
		free(buf);
		goto out;
	}

	printf("Available watchdog timers:\n");
	for (i = 0, cp = buf; i < count; i++, cp += WDOG_NAMESIZE) {
		cp[WDOG_NAMESIZE - 1] = '\0';
		strlcpy(wm.wm_name, cp, sizeof(wm.wm_name));

		if (ioctl(fd, WDOGIOC_GMODE, &wm) == -1)
			continue;
		mode = wm.wm_mode & WDOG_MODE_MASK;
		if (mode == WDOG_MODE_UTICKLE) {
			if (ioctl(fd, WDOGIOC_GTICKLER, &tickler) == -1)
				tickler = (pid_t) -1;
		}

		printf("\t%s, %u second period", cp, wm.wm_period);
		if (mode != WDOG_MODE_DISARMED) {
			switch(mode) {
			case WDOG_MODE_KTICKLE:
				printf(" [armed, kernel tickle");
				break;
			case WDOG_MODE_UTICKLE:
				printf(" [armed, user tickle");
				if (tickler != (pid_t) -1)
					printf(", pid %d", tickler);
				break;
			case WDOG_MODE_ETICKLE:
				printf(" [armed, external tickle");
				break;
			}
			printf("]");
		}
		printf("\n");
	}
 out:
	(void)close(fd);
}

static void
usage(void)
{

	fprintf(stderr, "usage: %s\n", getprogname());
	fprintf(stderr, "       %s -d\n", getprogname());
	fprintf(stderr, "       %s -e [-A] [-p seconds] timer\n",
	    getprogname());
	fprintf(stderr, "       %s -k [-A] [-p seconds] timer\n",
	    getprogname());
	fprintf(stderr, "       %s -t\n", getprogname());
	fprintf(stderr, "       %s -u [-A] [-p seconds] timer\n",
	    getprogname());
	fprintf(stderr, "       %s -x [-A] [-p seconds] timer\n",
	    getprogname());

	exit(1);
}
