/*
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 *
 *   Copyright (C) 2009 Jo-Philipp Wich <xm@subsignal.org>
 */

#include "watchdog.h"

/* Global watchdog fd, required by signal handler */
int wdfd = -1;

/* Handle finished childs */
static void sigchld_handler(int sig)
{
	pid_t pid;

	while( (pid = waitpid(-1, NULL, WNOHANG)) > 0 )
		syslog(LOG_INFO, "Child returned (pid %d)", pid);
}

/* Watchdog shutdown helper */
static void shutdown_watchdog(int sig)
{
	static const char wshutdown = WATCH_SHUTDOWN;

	if( wdfd > -1 )
	{
		syslog(LOG_INFO, "Stopping watchdog timer");
		write(wdfd, &wshutdown, 1);
		close(wdfd);
		wdfd = -1;
	}

	exit(0);
}

/* Get BSSID of given interface */
static int iw_get_bssid(int iwfd, const char *ifname, char *bssid)
{
	struct iwreq iwrq;

	if( iw_ioctl(iwfd, ifname, SIOCGIWAP, &iwrq) >= 0 )
	{
		unsigned char *addr = (unsigned char *)iwrq.u.ap_addr.sa_data;

		sprintf(bssid, "%02X:%02X:%02X:%02X:%02X:%02X",
			addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);

		return 0;
	}

	return -1;
}

/* Get channel of given interface */
static int iw_get_channel(int iwfd, const char *ifname, int *channel)
{
	int i;
	char buffer[sizeof(struct iw_range)];
	double cur_freq, cmp_freq;
	struct iwreq iwrq;
	struct iw_range *range;

	memset(buffer, 0, sizeof(buffer));

	iwrq.u.data.pointer = (char *)buffer;
	iwrq.u.data.length = sizeof(buffer);
	iwrq.u.data.flags = 0;

	if( iw_ioctl(iwfd, ifname, SIOCGIWRANGE, &iwrq) < 0)
	{
		*channel = -1;
		return -1;
	}

	range = (struct iw_range *)buffer;

	if( iw_ioctl(iwfd, ifname, SIOCGIWFREQ, &iwrq) >= 0 )
	{
		cur_freq = ((double)iwrq.u.freq.m) * pow(10, iwrq.u.freq.e);
		if( cur_freq < 1000.00 )
		{
			*channel = (int)cur_freq;
			return 0;
		}

		for(i = 0; i < range->num_frequency; i++)
		{
			cmp_freq = ((double)range->freq[i].m) * pow(10, range->freq[i].e);
			if( cmp_freq == cur_freq )
			{
				*channel = (int)range->freq[i].i;
				return 0;
			}
		}
	}

	*channel = -1;
	return -1;
}

/* Get the (first) pid of given process name */
static int find_process(const char *name)
{
	int pid = -1;
	int file;
	char buffer[128];
	char cmpname[128];
	DIR *dir;
	struct dirent *entry;

	if( (dir = opendir("/proc")) != NULL )
	{
		snprintf(cmpname, sizeof(cmpname), "Name:\t%s\n", name);

		while( (entry = readdir(dir)) != NULL )
		{
			if( !strcmp(entry->d_name, "..") || !isdigit(*entry->d_name) )
				continue;

			sprintf(buffer, "/proc/%s/status", entry->d_name);
			if( (file = open(buffer, O_RDONLY)) > -1 )
			{
				read(file, buffer, sizeof(buffer));
				close(file);

				if( strstr(buffer, cmpname) == buffer )
				{
					pid = atoi(entry->d_name);

					/* Skip myself ... */
					if( pid == getpid() )
						pid = -1;
					else
						break;
				}
			}
		}

		closedir(dir);
		return pid;
	}

	syslog(LOG_CRIT, "Unable to open /proc: %s",
		strerror(errno));

	return -1;
}

/* Get the 5 minute load average */
static double find_loadavg(void)
{
	int fd;
	char buffer[10];
	double load = 0.00;

	if( (fd = open("/proc/loadavg", O_RDONLY)) > -1 )
	{
		if( read(fd, buffer, sizeof(buffer)) == sizeof(buffer) )
			load = atof(&buffer[5]);

		close(fd);
	}

	return load;
}

/* Check if given uci file was updated */
static int check_uci_update(const char *config, time_t *mtime)
{
	struct stat s;
	char path[128];

	snprintf(path, sizeof(path), "/var/state/%s", config);
	if( stat(path, &s) > -1 )
	{
		if( (*mtime == 0) || (s.st_mtime > *mtime) )
		{
			*mtime = s.st_mtime;
			return 1;
		}
	}

	return 0;
}

/* Add tuple */
static void load_wifi_uci_add_iface(const char *section, struct uci_wifi_iface_itr_ctx *itr)
{
	wifi_tuple_t *t;
	const char *ucitmp;
	int val = 0;

	ucitmp = ucix_get_option(itr->ctx, "wireless", section, "mode");
	if( ucitmp && !strncmp(ucitmp, "adhoc", 5) )
	{
		if( (t = (wifi_tuple_t *)malloc(sizeof(wifi_tuple_t))) != NULL )
		{
			ucitmp = ucix_get_option(itr->ctx, "wireless", section, "ifname");
			if(ucitmp)
			{
				strncpy(t->ifname, ucitmp, sizeof(t->ifname));
				val++;
			}

			ucitmp = ucix_get_option(itr->ctx, "wireless", section, "bssid");
			if(ucitmp)
			{
				strncpy(t->bssid, ucitmp, sizeof(t->bssid));
				val++;
			}

			ucitmp = ucix_get_option(itr->ctx, "wireless", section, "device");
			if(ucitmp)
			{
				ucitmp = ucix_get_option(itr->ctx, "wireless", ucitmp, "channel");
				if(ucitmp)
				{
					t->channel = atoi(ucitmp);
					val++;
				}
			}

			if( val == 3 )
			{
				syslog(LOG_INFO, "Monitoring %s: bssid=%s channel=%d",
					t->ifname, t->bssid, t->channel);

				t->next = itr->list;
				itr->list = t;
			}
			else
			{
				free(t);
			}
		}
	}
}

/* Load config */
static wifi_tuple_t * load_wifi_uci(wifi_tuple_t *ifs, time_t *modtime)
{
	struct uci_context *ctx;
	struct uci_wifi_iface_itr_ctx itr;
	wifi_tuple_t *cur, *next;

	if( check_uci_update("wireless", modtime) )
	{
		syslog(LOG_INFO, "Wireless config changed, reloading");

		if( (ctx = ucix_init("wireless")) != NULL )
		{
			if( ifs != NULL )
			{
				for(cur = ifs; cur; cur = next)
				{
					next = cur->next;
					free(cur);
				}
			}

			itr.list = NULL;
			itr.ctx = ctx;

			ucix_for_each_section_type(ctx, "wireless", "wifi-iface",
				(void *)load_wifi_uci_add_iface, &itr);

			return itr.list;
		}
	}

	return ifs;
}

/* Add tuple */
static void load_watchdog_uci_add_process(const char *section, struct uci_process_itr_ctx *itr)
{
	process_tuple_t *t;
	const char *ucitmp;
	int val = 0;

	if( (t = (process_tuple_t *)malloc(sizeof(process_tuple_t))) != NULL )
	{
		t->restart = 0;

		ucitmp = ucix_get_option(itr->ctx, "freifunk-watchdog", section, "process");
		if(ucitmp)
		{
			strncpy(t->process, ucitmp, sizeof(t->process));
			val++;
		}

		ucitmp = ucix_get_option(itr->ctx, "freifunk-watchdog", section, "initscript");
		if(ucitmp)
		{
			strncpy(t->initscript, ucitmp, sizeof(t->initscript));
			val++;
		}

		if( val == 2 )
		{
			syslog(LOG_INFO, "Monitoring %s: initscript=%s",
				t->process, t->initscript);

				t->next = itr->list;
				itr->list = t;
		}
		else
		{
			free(t);
		}
	}
}

/* Load config */
static process_tuple_t * load_watchdog_uci(process_tuple_t *procs)
{
	struct uci_context *ctx;
	struct uci_process_itr_ctx itr;
	process_tuple_t *cur, *next;

	syslog(LOG_INFO, "Loading watchdog config");

	if( (ctx = ucix_init("freifunk-watchdog")) != NULL )
	{
		if( procs != NULL )
		{
			for(cur = procs; cur; cur = next)
			{
				next = cur->next;
				free(cur);
			}
		}

		itr.list = NULL;
		itr.ctx = ctx;

		ucix_for_each_section_type(ctx, "freifunk-watchdog", "process",
			(void *)load_watchdog_uci_add_process, &itr);

		return itr.list;
	}

	return procs;
}

/* Daemon implementation */
static int do_daemon(void)
{
	static int wdtrigger = 1;
	static int wdtimeout = BASE_INTERVAL * 2;
	static const char wdkeepalive = WATCH_KEEPALIVE;

	int iwfd;
	int channel;
	char bssid[18];
	struct sigaction sa;

	wifi_tuple_t *ifs = NULL, *curr_if;
	process_tuple_t *procs = NULL, *curr_proc;
	time_t wireless_modtime = 0;

	int action_intv = 0;
	int restart_wifi = 0;
	int loadavg_panic = 0;

	openlog(SYSLOG_IDENT, 0, LOG_DAEMON);
	memset(&sa, 0, sizeof(sa));

	if( (iwfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1 )
	{
		syslog(LOG_ERR, "Can not open wireless control socket: %s",
			strerror(errno));

		return 1;
	}

	if( (wdfd = open(WATCH_DEVICE, O_WRONLY)) > -1 )
	{
		syslog(LOG_INFO, "Opened %s - polling every %i seconds",
			WATCH_DEVICE, BASE_INTERVAL);

		/* Install signal handler to halt watchdog on shutdown */
		sa.sa_handler = shutdown_watchdog;
		sa.sa_flags = SA_NOCLDWAIT | SA_RESTART;
		sigaction(SIGHUP,  &sa, NULL);
		sigaction(SIGINT,  &sa, NULL);
		sigaction(SIGPIPE, &sa, NULL);
		sigaction(SIGTERM, &sa, NULL);
		sigaction(SIGUSR1, &sa, NULL);
		sigaction(SIGUSR2, &sa, NULL);

		/* Set watchdog timeout to twice the interval */
		ioctl(wdfd, WDIOC_SETTIMEOUT, &wdtimeout);
	}

	/* Install signal handler to reap childs */
	sa.sa_handler = sigchld_handler;
	sa.sa_flags = 0;
	sigaction(SIGCHLD, &sa, NULL);

	/* Load watchdog configuration only once */
	procs = load_watchdog_uci(procs);

	while( 1 )
	{
		/* Check/increment action interval */
		if( ++action_intv >= ACTION_INTERVAL )
		{
			/* Reset action interval */
			action_intv = 0;

			/* Check average load */
			if( find_loadavg() >= LOAD_TRESHOLD )
				loadavg_panic++;
			else
				loadavg_panic = 0;

			/* Check wireless interfaces */
			ifs = load_wifi_uci(ifs, &wireless_modtime);
			for( curr_if = ifs; curr_if; curr_if = curr_if->next )
			{
				/* Get current channel and bssid */
				if( (iw_get_bssid(iwfd, curr_if->ifname, bssid) == 0) &&
			    (iw_get_channel(iwfd, curr_if->ifname, &channel) == 0) )
				{
					/* Check BSSID */
					if( strcasecmp(bssid, curr_if->bssid) != 0 )
					{
						syslog(LOG_WARNING, "BSSID mismatch on %s: current=%s wanted=%s",
							curr_if->ifname, bssid, curr_if->bssid);

						restart_wifi++;
					}

					/* Check channel */
					else if( channel != curr_if->channel )
					{
						syslog(LOG_WARNING, "Channel mismatch on %s: current=%d wanted=%d",
							curr_if->ifname, channel, curr_if->channel);

						restart_wifi++;
					}
				}
				else
				{
					syslog(LOG_WARNING, "Requested interface %s not present", curr_if->ifname);
				}
			}

			/* Check processes */
			for( curr_proc = procs; curr_proc; curr_proc = curr_proc->next )
			{
				if( find_process(curr_proc->process) < 0 )
					curr_proc->restart++;
				else
					curr_proc->restart = 0;

				/* Process restart required? */
				if( curr_proc->restart >= HYSTERESIS )
				{
					curr_proc->restart = 0;
					syslog(LOG_WARNING, "The %s process died, restarting", curr_proc->process);
					EXEC(PROC_ACTION);
				}
			}


			/* Wifi restart required? */
			if( restart_wifi >= HYSTERESIS )
			{
				restart_wifi = 0;
				syslog(LOG_WARNING, "Channel or BSSID mismatch on wireless interface, restarting");
				EXEC(WIFI_ACTION);
			}

			/* Is there a load problem? */
			if( loadavg_panic >= HYSTERESIS )
			{
				syslog(LOG_EMERG, "Critical system load level, triggering reset!");

				/* Try watchdog, fall back to reboot */
				if( wdfd > -1 )
					ioctl(wdfd, WDIOC_SETTIMEOUT, &wdtrigger);
				else
					EXEC(LOAD_ACTION);
			}
		}


		/* Reset watchdog timer */
		if( wdfd > -1 )
			write(wdfd, &wdkeepalive, 1);

		sleep(BASE_INTERVAL);
	}

	shutdown_watchdog(0);
	closelog();

	return 0;
}


int main(int argc, char *argv[])
{
	/* Check if watchdog is running ... */
	if( (argc > 1) && (strcmp(argv[1], "running") == 0) )
	{
		return (find_process(BINARY) == -1);
	}

	/* Start daemon */
	return do_daemon();
}