/*
 * Copyright 2016-2018, Intel Corporation
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *
 *     * 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.
 *
 *     * Neither the name of the copyright holder nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS 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.
 */

/*
 * ctrld.c -- simple application which helps running tests on remote node.
 *
 * XXX - wait_port is not supported on FreeBSD because there are currently
 *       no test cases that require it.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <signal.h>
#include <limits.h>
#include <sys/queue.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdarg.h>

#include "os.h"

#ifdef __FreeBSD__
#include "signals_freebsd.h"
#else
#include "signals_linux.h"
#endif

#define APP_NAME "ctrld"
#define BUFF_SIZE 4096

#define S_MINUTE	(60)		/* seconds in one minute */
#define S_HOUR		(60 * 60)	/* seconds in one hour */
#define S_DAY		(60 * 60 * 24)	/* seconds in one day */

static FILE *log_fh;

static void
log_err(const char *file, int lineno, const char *fmt, ...)
{
	FILE *fh = log_fh ? log_fh : stderr;
	va_list ap;
	fprintf(fh, "[%s:%d] ", file, lineno);

	char *prefix = "";
	char *errstr = "";
	if (*fmt == '!') {
		fmt++;
		prefix = ": ";
		errstr = strerror(errno);
	}

	va_start(ap, fmt);
	vfprintf(fh, fmt, ap);
	va_end(ap);

	fprintf(fh, "%s%s\n", prefix, errstr);
	fflush(fh);
}

#define CTRLD_LOG(...) log_err(__FILE__, __LINE__, __VA_ARGS__)

struct inode_item {
	LIST_ENTRY(inode_item) next;
	unsigned long inode;
};

struct inodes {
	LIST_HEAD(inode_items, inode_item) head;
};

/*
 * usage -- print usage and exit with failure code
 */
static void
usage(void)
{
	CTRLD_LOG("usage: %s <pid file> <cmd> [<arg>]", APP_NAME);
	CTRLD_LOG("commands:");
	CTRLD_LOG("  exe <command> [<args...>] -- "
			"run specified command");
	CTRLD_LOG("  run  <timeout> <command> [<args...>] -- "
			"run specified command with given timeout");
	CTRLD_LOG("  wait [<timeout>]                     -- "
			"wait for command");
#ifndef __FreeBSD_
	CTRLD_LOG("  wait_port <port>                     -- "
			"wait until a port is opened");
#endif
	CTRLD_LOG("  kill <signal>                        -- "
			"send a signal to command");
	exit(EXIT_FAILURE);
}

/*
 * alloc_argv -- allocate NULL terminated list of arguments at specified offset
 */
static char **
alloc_argv(unsigned argc, char *argv[], unsigned off)
{
	if (argc < off)
		return NULL;

	unsigned nargc = argc - off;
	char **nargv = malloc((nargc + 1) * sizeof(char *));
	if (!nargv)
		return NULL;

	for (unsigned i = 0; i < nargc; i++)
		nargv[i] = argv[i + off];

	nargv[nargc] = NULL;

	return nargv;
}

/*
 * do_run_or_exe -- execute the 'run' or the 'exe' command
 *
 * if timeout is equal to 0 cmd will be just executed (the 'exe' command)
 * otherwise it will be run and wait with timeout (the 'run' command)
 */
static int
do_run_or_exe(const char *pid_file, char *cmd, char *argv[], unsigned timeout)
{
	int rv = -1;

	FILE *fh = os_fopen(pid_file, "w+");
	if (!fh) {
		CTRLD_LOG("!%s", pid_file);
		return -1;
	}

	int fd = fileno(fh);
	if (fd == -1) {
		CTRLD_LOG("!fileno");
		goto err;
	}

	if (os_flock(fd, LOCK_EX | LOCK_NB)) {
		CTRLD_LOG("!flock");
		goto err;
	}

	if (timeout != 0) {
		if (daemon(1, 0)) {
			CTRLD_LOG("!daemon");
			goto err;
		}
	}

	int child = fork();
	switch (child) {
	case -1:
		CTRLD_LOG("!fork");
		fprintf(fh, "-1r%d", errno);
		goto err;
	case 0:
		execvp(cmd, argv);
		CTRLD_LOG("!execvp(%s)", cmd);
		goto err;
	default:
		break;
	}

	if (fprintf(fh, "%d", child) < 0) {
		CTRLD_LOG("!fprintf");
		goto err;
	}

	if (fflush(fh)) {
		CTRLD_LOG("!fflush");
		goto err;
	}

	int child_timeout = -1;
	if (timeout != 0) {
		child_timeout = fork();
		switch (child_timeout) {
		case -1:
			CTRLD_LOG("!fork");
			fprintf(fh, "-1r%d", errno);
			goto err;
		case 0:
			fclose(fh);
			sleep(timeout);
			return 0;
		default:
			break;
		}
	}

	int ret = 0;
	int pid = wait(&ret);
	if (pid == child) {
		if (timeout != 0) {
			/* kill the timeout child */
			kill(child_timeout, SIGTERM);
		}

		if (WIFSIGNALED(ret)) {
			ret = 128 + WTERMSIG(ret);
		} else {
			ret = WEXITSTATUS(ret);
		}

		if (fseek(fh, 0, SEEK_SET)) {
			CTRLD_LOG("!fseek");
			goto err;
		}

		if (os_ftruncate(fileno(fh), 0)) {
			CTRLD_LOG("!ftruncate");
			goto err;
		}

		fprintf(fh, "%dr%d", child, ret);

	} else if (timeout != 0 && pid == child_timeout) {
		CTRLD_LOG("run: timeout");
		if (kill(child, SIGTERM) && errno != ESRCH) {
			CTRLD_LOG("!kill");
			goto err;
		}
		CTRLD_LOG("run: process '%s' killed (PID %i)", cmd, child);
	} else {
		CTRLD_LOG("!wait");
		goto err;
	}

	rv = 0;
err:
	fclose(fh);
	return rv;
}

/*
 * do_wait -- execute the 'wait' command
 */
static int
do_wait(char *pid_file, int timeout)
{
	int fd = os_open(pid_file, O_RDONLY);
	if (fd < 0) {
		perror(pid_file);
		return 1;
	}

	int ret;

	int t = 0;
	while ((timeout == -1 || t < timeout) &&
		os_flock(fd, LOCK_EX | LOCK_NB)) {
		sleep(1);
		t++;
	}

	FILE *fh = os_fdopen(fd, "r");
	if (!fh) {
		CTRLD_LOG("!fdopen");
		ret = 1;
		goto err;
	}

	pid_t pid;
	char r;
	int n = fscanf(fh, "%d%c%d", &pid, &r, &ret);
	if (n < 0) {
		CTRLD_LOG("!fscanf");
		ret = 1;
		goto err;
	}

	if (n == 2 || (n == 3 && r != 'r')) {
		CTRLD_LOG("invalid format of PID file");
		ret = 1;
		goto err;
	}

	if (n == 1) {
		if (timeout >= 0) {
			ret = -1;
			goto err;
		} else {
			CTRLD_LOG("missing return value");
			ret = 1;
			goto err;
		}
	}

err:
	os_close(fd);
	fclose(fh);
	return ret;
}

/*
 * do_kill -- execute the 'kill' command
 */
static int
do_kill(char *pid_file, int signo)
{
	FILE *fh = os_fopen(pid_file, "r");
	if (!fh) {
		CTRLD_LOG("!%s", pid_file);
		return 1;
	}

	int ret;
	pid_t pid;
	int n = fscanf(fh, "%d", &pid);
	if (n == 0) {
		ret = 0;
		goto out;
	}

	/* do not fail if such process already does not exist */
	if (kill(pid, signo) && errno != ESRCH) {
		CTRLD_LOG("!kill");
		ret = 1;
		goto out;
	}

	ret = 0;
out:
	fclose(fh);
	return ret;
}

#ifndef __FreeBSD__	/* XXX wait_port support */
/*
 * contains_inode -- check if list contains specified inode
 */
static int
contains_inode(struct inodes *inodes, unsigned long inode)
{
	struct inode_item *inode_item;
	LIST_FOREACH(inode_item, &inodes->head, next) {
		if (inode_item->inode == inode)
			return 1;
	}

	return 0;
}

/*
 * has_port_inode -- check if /proc/net/tcp has an entry with specified
 * port and inode
 */
static int
has_port_inode(unsigned short port, struct inodes *inodes)
{
	/* format of /proc/net/tcp entries */
	const char * const tcp_fmt =
		"%*d: "
		"%*64[0-9A-Fa-f]:%X "
		"%*64[0-9A-Fa-f]:%*X "
		"%*X %*X:%*X %*X:%*X "
		"%*X %*d %*d %lu %*s\n";

	char buff[BUFF_SIZE];

	FILE *fh = os_fopen("/proc/net/tcp", "r");
	if (!fh) {
		CTRLD_LOG("!%s", "/proc/net/tcp");
		return -1;
	}

	int ret;
	/* read heading */
	char *s = fgets(buff, 4096, fh);
	if (!s) {
		ret = -1;
		goto out;
	}

	while (1) {
		s = fgets(buff, 4096, fh);
		if (!s)
			break;

		/* read port number and inode number */
		unsigned p;
		unsigned long inode;
		if (sscanf(s, tcp_fmt, &p, &inode) != 2) {
			ret = -1;
			goto out;
		}

		/*
		 * if port matches and inode is on a list
		 * the process has this port opened
		 */
		if (p == port && contains_inode(inodes, inode)) {
			ret = 1;
			goto out;
		}
	}

	ret = 0;

out:
	fclose(fh);
	return ret;
}

/*
 * get_inodes -- get list of inodes
 */
static int
get_inodes(pid_t pid, struct inodes *inodes)
{
	char path[PATH_MAX];
	char link[PATH_MAX];
	int ret;

	/* set a path to opened files of specified process */
	if ((ret = snprintf(path, PATH_MAX, "/proc/%d/fd", pid)) < 0) {
		CTRLD_LOG("snprintf: %d", ret);
		return -1;
	}

	/* open dir with all opened files */
	DIR *d = opendir(path);
	if (!d) {
		CTRLD_LOG("!%s", path);
		ret = -1;
		goto out_dir;
	}

	/* read all directory entries */
	struct dirent *dent;
	while ((dent = readdir(d)) != NULL) {
		/* create a full path to file */
		if ((ret = snprintf(path, PATH_MAX,
			"/proc/%d/fd/%s", pid, dent->d_name)) < 0) {
			CTRLD_LOG("snprintf: %d", ret);
			ret = -1;
			goto out_dir;
		}

		/* read symbolic link */
		ssize_t sret = readlink(path, link, PATH_MAX - 1);
		if (sret <= 0)
			continue;
		link[sret] = '\0';

		/* check if this is a socket, read inode number if so */
		unsigned long inode;
		if (sscanf(link, "socket:[%lu]", &inode) != 1)
			continue;

		/* add inode to a list */
		struct inode_item *inode_item = malloc(sizeof(*inode_item));
		if (!inode_item) {
			CTRLD_LOG("!malloc inode item");
			exit(1);
		}

		inode_item->inode = inode;
		LIST_INSERT_HEAD(&inodes->head, inode_item, next);

	}

	ret = 0;
out_dir:
	closedir(d);
	return ret;
}

/*
 * clear_inodes -- clear list of inodes
 */
static void
clear_inodes(struct inodes *inodes)
{
	while (!LIST_EMPTY(&inodes->head)) {
		struct inode_item *inode_item = LIST_FIRST(&inodes->head);
		LIST_REMOVE(inode_item, next);
		free(inode_item);
	}
}

/*
 * has_port -- check if process has the specified tcp port opened
 */
static int
has_port(pid_t pid, unsigned short port)
{
	struct inodes inodes;
	memset(&inodes, 0, sizeof(inodes));

	int ret = get_inodes(pid, &inodes);
	if (ret < 0)
		return -1;

	if (!LIST_EMPTY(&inodes.head)) {
		ret = has_port_inode(port, &inodes);
		clear_inodes(&inodes);
	}

	return ret;
}

/*
 * do_wait_port -- wait until process opens a specified tcp port
 */
static int
do_wait_port(char *pid_file, unsigned short port)
{
	FILE *fh = os_fopen(pid_file, "r");
	if (!fh) {
		CTRLD_LOG("!%s", pid_file);
		return 1;
	}

	int ret;

	pid_t pid;
	char r;
	int n = fscanf(fh, "%d%c%d", &pid, &r, &ret);
	if (n < 0) {
		CTRLD_LOG("!fscanf");
		ret = 1;
		goto err;
	}

	if (n == 2 || (n == 3 && r != 'r')) {
		CTRLD_LOG("invalid format of PID file");
		ret = 1;
		goto err;
	}

	if (n == 3) {
		CTRLD_LOG("process already terminated");
		ret = 1;
		goto err;
	}

	int hp;
	do {
		hp = has_port(pid, port);
		if (hp < 0) {
			ret = 1;
			goto err;
		}
	} while (!hp);

	return 0;
err:
	fclose(fh);
	return -1;
}
#endif	/* __FreeBSD__ wait_port support */

/*
 * convert_signal_name -- convert a signal name to a signal number
 */
static int
convert_signal_name(const char *signal_name)
{
	for (int sig = SIGHUP; sig <= SIGNALMAX; sig++)
		if (strcmp(signal_name, signal2str[sig]) == 0)
			return sig;
	return -1;
}

/*
 * log_run -- print run command with arguments
 */
static void
log_run(const char *pid_file, char *cmd, char *argv[])
{
	char buff[BUFF_SIZE];
	buff[0] = '\0';
	size_t cnt = 0;
	size_t i = 0;
	char *arg = argv[0];
	while (arg) {
		int ret = snprintf(&buff[cnt], BUFF_SIZE - cnt,
				" %s", arg);
		if (ret < 0) {
			CTRLD_LOG("snprintf: %d", ret);
			exit(EXIT_FAILURE);
		}

		cnt += (size_t)ret;

		i++;
		arg = argv[i];
	}

	CTRLD_LOG("run %s%s", pid_file, buff);
}

/*
 * convert_timeout -- convert a floating point number with an optional suffix
 *                    to unsigned integer: 's' for seconds (the default),
 *                    'm' for minutes, 'h' for hours or 'd' for days.
 */
static unsigned
convert_timeout(char *str)
{
	char *endptr;
	float ftimeout = strtof(str, &endptr);
	switch (*endptr) {
	case 'm':
		ftimeout *= S_MINUTE;
		break;
	case 'h':
		ftimeout *= S_HOUR;
		break;
	case 'd':
		ftimeout *= S_DAY;
		break;
	default:
		break;
	}
	return (unsigned)ftimeout;
}

int
main(int argc, char *argv[])
{
	if (argc < 3)
		usage();

	int ret = 0;
	char *pid_file = argv[1];
	char *cmd = argv[2];

	char buff[BUFF_SIZE];
	if (snprintf(buff, BUFF_SIZE, "%s.%s.%s.log",
			pid_file, cmd, APP_NAME) < 0) {
		perror("snprintf");
		return -1;
	}

	log_fh = os_fopen(buff, "a");
	if (!log_fh) {
		perror(buff);
		return -1;
	}

	if (strcmp(cmd, "exe") == 0) {
		if (argc < 4)
			usage();

		char *command = argv[3];
		char **nargv = alloc_argv((unsigned)argc, argv, 3);
		if (!nargv) {
			CTRLD_LOG("!get_argv");
			return 1;
		}

		log_run(pid_file, command, nargv);
		ret = do_run_or_exe(pid_file, command, nargv, 0 /* timeout */);

		free(nargv);
	} else if (strcmp(cmd, "run") == 0) {
		if (argc < 5)
			usage();

		unsigned timeout = convert_timeout(argv[3]);
		char *command = argv[4];
		char **nargv = alloc_argv((unsigned)argc, argv, 4);
		if (!nargv) {
			CTRLD_LOG("!get_argv");
			return 1;
		}

		log_run(pid_file, command, nargv);
		ret = do_run_or_exe(pid_file, command, nargv, timeout);

		free(nargv);
	} else if (strcmp(cmd, "wait") == 0) {
		if (argc != 3 && argc != 4)
			usage();

		int timeout = -1;
		if (argc == 4)
			timeout = atoi(argv[3]);

		CTRLD_LOG("wait %s %d", pid_file, timeout);
		ret = do_wait(pid_file, timeout);
	} else if (strcmp(cmd, "kill") == 0) {
		if (argc != 4)
			usage();

		int signo = atoi(argv[3]);
		if (signo == 0) {
			signo = convert_signal_name(argv[3]);
			if (signo == -1) {
				CTRLD_LOG("Invalid signal name or number"
						" (%s)", argv[3]);
				return 1;
			}
		}

		CTRLD_LOG("kill %s %s", pid_file, argv[3]);
		ret = do_kill(pid_file, signo);
#ifndef __FreeBSD__
	} else if (strcmp(cmd, "wait_port") == 0) {
		if (argc != 4)
			usage();

		unsigned short port = (unsigned short)atoi(argv[3]);

		CTRLD_LOG("wait_port %s %u", pid_file, port);
		ret = do_wait_port(pid_file, port);
#endif
	} else {
		usage();
	}

	return ret;
}
