/**********************************************************
 * Copyright:   Appliance Studio Ltd
 * License:	GPL
 *
 * Filename:    $Id: wd_keepalive.c,v 1.6 2007/08/17 09:24:54 meskes Exp $    
 * Author:      Marcel Jansen, 22 February 2001
 * 		Michael Meskes, since then
 * Purpose:     This program can be run during critical periods
 *              when the normal watcdog shouldn't be run. It will
 *              read from the same configuration file, it will do
 *              no checks but will keep writing to the device
 *
***********************************************************/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <linux/watchdog.h>
#include <libgen.h>
#include <string.h>
#include <syslog.h>

#include <unistd.h>

#if USE_SYSLOG
#include <syslog.h>
#endif         

#define TRUE  1
#define FALSE 0

#define DEVICE		"watchdog-device"
#define INTERVAL	"interval"
#define PRIORITY        "priority"
#define REALTIME        "realtime"

int watchdog = -1, tint = 10, schedprio = 1;
char *devname = NULL, *progname = NULL;
volatile sig_atomic_t _running = 1;

#if defined(_POSIX_MEMLOCK)
int mlocked = FALSE, realtime = FALSE;
#endif

static void usage(void)
{
    fprintf(stderr, "%s version %d.%d, usage:\n", progname, MAJOR_VERSION, MINOR_VERSION);
    fprintf(stderr, "%s \n", progname);
    exit(1);
}

/* write a log entry on exit */
static void log_end()
{
#if USE_SYSLOG
    /* Log the closing message */
    syslog(LOG_INFO, "stopping watchdog keepalive daemon (%d.%d)", MAJOR_VERSION, MINOR_VERSION);
    closelog();
    sleep(5);           /* make sure log is written */
#endif                          /* USE_SYSLOG */
    return;
}

/* close the device and check for error */
static void close_all()
{
	if (watchdog != -1) {
		if (write(watchdog, "V", 1) < 0 ) {
			int err = errno;
#if USE_SYSLOG
	                syslog(LOG_ERR, "write watchdog device gave error %d = '%m'!", err);
#else                           /* USE_SYSLOG */
	                perror(progname);
#endif                          /* USE_SYSLOG */
	        }
		if (close(watchdog) == -1) {
#if USE_SYSLOG
		        syslog(LOG_ALERT, "cannot close %s (errno = %d)", devname, errno);
#else                           /* USE_SYSLOG */
			perror(progname);
#endif                          /* USE_SYSLOG */
		}
        }
}

void sigterm_handler(int arg) {
    _running = 0;
}

/* on exit we close the device and log that we stop */
void terminate(void) {
#if defined(_POSIX_MEMLOCK)
    if ( realtime == TRUE && mlocked == TRUE ) {
        /* unlock all locked pages */
        if ( munlockall() != 0 ) {
#if USE_SYSLOG            
	    syslog(LOG_ERR, "cannot unlock realtime memory (errno = %d)", errno);
#else                           /* USE_SYSLOG */
            perror(progname);
#endif                          /* USE_SYSLOG */
	}
    }
#endif    	/* _POSIX_MEMLOCK */
    close_all();
    log_end();
    exit(0);
}

static int spool(char *line, int *i, int offset)
{
    for ( (*i) += offset; line[*i] == ' ' || line[*i] == '\t'; (*i)++ );
    if ( line[*i] == '=' )
        (*i)++;
    for ( ; line[*i] == ' ' || line[*i] == '\t'; (*i)++ );
    if ( line[*i] == '\0' )
        return(1);
    else
        return(0);
}

static void read_config(char *configfile, char *progname)
{
    FILE *wc;

    if ( (wc = fopen(configfile, "r")) == NULL ) {
        perror(progname);
        exit(1);
    }

    while ( !feof(wc) ) {
	char *line = NULL;
	size_t n;

	if (getline(&line, &n, wc) == -1) {
            if ( !ferror(wc) )
                break;
            else {
                perror(progname);
                exit(1);
            }
        }
        else {
            int i, j;

            /* scan the actual line for an option */
            /* first remove the leading blanks */
            for ( i = 0; line[i] == ' ' || line[i] == '\t'; i++ );

            /* if the next sign is a '#' we have a comment */
            if ( line[i] == '#' )
                continue;

            /* also remove the trailing blanks and the \n */
            for ( j = strlen(line) - 1; line[j] == ' ' || line[j] == '\t' || line[j] == '\n'; j-- );
            line[j + 1] = '\0';

            /* if the line is empty now, we don't have to parse it */
            if ( strlen(line + i) == 0 )
                continue;

            /* now check for an option */
            if ( strncmp(line + i, INTERVAL, strlen(INTERVAL)) == 0 ) {
                if ( spool(line, &i, strlen(INTERVAL)) )
                    fprintf(stderr, "Ignoring invalid line in config file:\n%s\n", line);
                else
                    tint = atol(line + i);
            }
            else if ( strncmp(line + i, DEVICE, strlen(DEVICE)) == 0 ) {
                if ( spool(line, &i, strlen(DEVICE)) )
                    devname = NULL;
                else
                    devname = strdup(line + i);
	    } else if (strncmp(line + i, REALTIME, strlen(REALTIME)) == 0) {
                (void)spool(line, &i, strlen(REALTIME));
	        realtime = (strncmp(line + i, "yes", 3) == 0) ? TRUE : FALSE;
	    } else if (strncmp(line + i, PRIORITY, strlen(PRIORITY)) == 0) {
	        if (spool(line, &i, strlen(PRIORITY)))
			fprintf(stderr, "Ignoring invalid line in config file:\n%s\n", line);
		else
		        schedprio = atol(line + i);
	    } 
            else {
                /*
		 * do not print an error message here because we usually use
		 * watchdog's config file which may contain far more valid
		 * options than we understand 
		 */
		/* fprintf(stderr, "Ignoring config line: %s\n", line); */
            }
        }
    }

    if ( fclose(wc) != 0 ) {
        perror(progname);
        exit(1);
    }
}


int main(int argc, char *const argv[])
{
    FILE *fp;
    char *configfile = CONFIG_FILENAME;
    pid_t child_pid;
    int count = 0;
    int c;
    int oom_adjusted = 0;
    struct stat s;

    /* allow all options watchdog understands too */
#if USE_SYSLOG
    char *opts = "d:i:n:fsvbql:p:t:c:r:m:a:";
    struct option long_options[] =
    {
	{"config-file", required_argument, NULL, 'c'},
	{"force", no_argument, NULL, 'f'},
	{"sync", no_argument, NULL, 's'},
	{"no-action", no_argument, NULL, 'q'},
	{"verbose", no_argument, NULL, 'v'},
	{"softboot", no_argument, NULL, 'b'},
	{NULL, 0, NULL, 0}
    };
    struct watchdog_info ident;
#else				/* USE_SYSLOG */
    char *opts = "d:i:n:fsbql:p:t:c:r:m:a:";
    struct option long_options[] =
    {
	{"config-file", required_argument, NULL, 'c'},
	{"force", no_argument, NULL, 'f'},
	{"sync", no_argument, NULL, 's'},
	{"no-action", no_argument, NULL, 'q'},
	{"softboot", no_argument, NULL, 'b'},
	{NULL, 0, NULL, 0}
    };
#endif				/* USE_SYSLOG */

    progname = basename(argv[0]);

    /* check for the one option we understand */
    while ((c = getopt_long(argc, argv, opts, long_options, NULL)) != EOF) {
	if (c == -1)
	    break;
	switch (c) {
	case 'c':
	    configfile = optarg;
	    break;
	case 'n':
	case 'p':
	case 'a':
	case 'r':
	case 'd':
	case 't':
	case 'l':
	case 'm':
	case 'i':
	case 'f':
	case 's':
	case 'b':
	case 'q':
#if USE_SYSLOG
	case 'v':
#endif				/* USE_SYSLOG */
	     break;
	default:
	    usage();
	}
    }

    read_config(configfile, progname);

    /* make sure we're on the root partition */
    if ( chdir("/") < 0 ) {
        perror(progname);
        exit(1);
    }

#if !defined(DEBUG)
    /* fork to go into the background */
    if ( (child_pid = fork()) < 0 ) {
        perror(progname);
        exit(1);
    }
    else if ( child_pid > 0 ) {
        /* fork was okay          */
        /* wait for child to exit */
        if ( waitpid(child_pid, NULL, 0) != child_pid ) {
            perror(progname);
            exit(1);
        }
        /* and exit myself */
        exit(0);
    }
    /* and fork again to make sure we inherit all rights from init */
    if ( (child_pid = fork()) < 0 ) {
        perror(progname);
        exit(1);
    }
    else if ( child_pid > 0 )
        exit(0);
#endif				/* !DEBUG */

    /* now we're free */
#if USE_SYSLOG
#if !defined(DEBUG)
    /* Okay, we're a daemon     */
    /* but we're still attached to the tty */
    /* create our own session */
    setsid();
    /* with USE_SYSLOG we don't do any console IO */
    close(0);
    close(1);
    close(2);
#endif				/* !DEBUG */

    /* Log the starting message */
    openlog(progname, LOG_PID, LOG_DAEMON);
    syslog(LOG_INFO, "starting watchdog keepalive daemon (%d.%d):", MAJOR_VERSION, MINOR_VERSION);
    if (devname == NULL)
	syslog(LOG_INFO, " no watchdog device configured, aborting");
    else
    	syslog(LOG_INFO, " int=%d alive=%s realtime=%s", tint, devname, realtime ? "yes" : "no");
#endif                          /* USE_SYSLOG */

    /* this daemon has no other function than writing to this device 
     * i.e. if there is no device given we better punt */
    if ( devname == NULL )
	terminate();

    /* open the device */
    watchdog = open(devname, O_WRONLY);
    if ( watchdog == -1 ) {
#if USE_SYSLOG
            syslog(LOG_ERR, "cannot open %s (errno = %d = '%m')", devname, errno);
#else                           /* USE_SYSLOG */
            perror(progname);
#endif                          /* USE_SYSLOG */

            exit(1);
    }

#if USE_SYSLOG
    /* Also log watchdog identity */
    if (ioctl(watchdog, WDIOC_GETSUPPORT, &ident) < 0) {
	syslog(LOG_ERR, "cannot get watchdog identity (errno = %d = '%m')", errno);
    }
    else {
	ident.identity[sizeof(ident.identity) - 1] = '\0'; /* Be sure */
	syslog(LOG_INFO, "hardware watchdog identity: %s", ident.identity);
    }
#endif

    /* tuck my process id away */
    fp = fopen(KA_PIDFILE, "w");
    if ( fp != NULL ) {
        fprintf(fp, "%d\n", getpid());
        (void) fclose(fp);
    }

    /* set signal term to call sigterm_handler() */
    /* to make sure watchdog device is closed */
    signal(SIGTERM, sigterm_handler);

#if defined(_POSIX_MEMLOCK)
    if ( realtime == TRUE ) {
        /* lock all actual and future pages into memory */
        if ( mlockall(MCL_CURRENT | MCL_FUTURE) != 0 ) {
#if USE_SYSLOG
		syslog(LOG_ERR, "cannot lock realtime memory (errno = %d = '%m')", errno);
#else                           /* USE_SYSLOG */
                perror(progname);
#endif                           /* USE_SYSLOG */

        }
        else {
            struct sched_param sp;

            /* now set the scheduler */
            sp.sched_priority = schedprio;
            if ( sched_setscheduler(0, SCHED_RR, &sp) != 0 ) {
#if USE_SYSLOG
		syslog(LOG_ERR, "cannot set scheduler (errno = %d = '%m')", errno);
#else                           /* USE_SYSLOG */
                perror(progname);
#endif                           /* USE_SYSLOG */
            }
            else
                mlocked = TRUE;
        }
    }
#endif

    /* tell oom killer to not kill this process */
#ifdef OOM_SCORE_ADJ_MIN
   if ( ! stat("/proc/self/oom_score_adj", &s) ) {
	fp = fopen("/proc/self/oom_score_adj", "w");
	if (fp) {
		fprintf(fp, "%d\n", OOM_SCORE_ADJ_MIN);
		(void) fclose(fp);
		oom_adjusted = 1;
	}
   }
#endif
#ifdef OOM_DISABLE
   if ( ! oom_adjusted ) {
	if ( ! stat("/proc/self/oom_adj", &s) ) {
		fp = fopen("/proc/self/oom_adj", "w");
		if (fp) {
			fprintf(fp, "%d\n", OOM_DISABLE);
			(void) fclose(fp);
			oom_adjusted = 1;
		}
	}
   }
#endif
#if USE_SYSLOG
   if ( ! oom_adjusted ) {
	syslog(LOG_WARNING, "unable to disable oom handling!");
   }
#endif				/* USE_SYSLOG */

    /* main loop: update after <tint> seconds */
    while ( _running ) {
        if ( write(watchdog, "\0", 1) < 0 ) {
            int err = errno;
#if USE_SYSLOG
	    syslog(LOG_ERR, "write watchdog device gave error %d = '%m'!", err);
#else                   /* USE_SYSLOG */
            perror(progname);
#endif                  /* USE_SYSLOG */
        }

        /* finally sleep some seconds */
        sleep(tint);

        count++;
    }

    terminate();
    /* not reached */
}


