
/*
 * libfiu remote control API
 */

#include <errno.h>     /* errno and friends */
#include <fcntl.h>     /* open() and friends */
#include <pthread.h>   /* pthread_create() and friends */
#include <stdio.h>     /* snprintf() */
#include <stdlib.h>    /* malloc()/free() */
#include <string.h>    /* strncpy() */
#include <sys/stat.h>  /* mkfifo() */
#include <sys/types.h> /* getpid(), mkfifo() */
#include <unistd.h>    /* getpid() */

/* Enable us, so we get the real prototypes from the headers */
#define FIU_ENABLE 1

#include "fiu-control.h"
#include "internal.h"

/* Max length of a line containing a control directive */
#define MAX_LINE 512

/*
 * Generic remote control
 */

/* Reads a line from the given fd, assumes the buffer can hold MAX_LINE
 * characters. Returns the number of bytes read, or -1 on error. Inefficient,
 * but we don't really care. The final '\n' will not be included. */
static int read_line(int fd, char *buf)
{
	int r;
	char c;
	unsigned int len;

	c = '\0';
	len = 0;
	memset(buf, 0, MAX_LINE);

	do {
		r = read(fd, &c, 1);
		if (r < 0)
			return -1;
		if (r == 0)
			break;

		len += r;

		*buf = c;
		buf++;

	} while (c != '\n' && c != '\0' && len < MAX_LINE);

	if (len > 0 && c == '\n') {
		*(buf - 1) = '\0';
		len--;
	}

	return len;
}

/* Remote control command processing.
 *
 * Supported commands:
 *  - disable name=N
 *  - enable name=N,failnum=F,failinfo=I
 *  - enable_random <same as enable>,probability=P
 *  - enable_stack_by_name <same as enable>,func_name=F,pos_in_stack=P
 *
 * All enable* commands can also take an additional "onetime" parameter,
 * indicating that this should only fail once (analogous to the FIU_ONETIME
 * flag).
 *
 * This function is ugly, but we aim for simplicity and ease to extend for
 * future commands.
 */
int fiu_rc_string(const char *cmd, char **const error)
{
	char m_cmd[MAX_LINE] = {0};
	char command[MAX_LINE] = {0};
	char parameters[MAX_LINE] = {0};

	/* We need a version of cmd we can write to for parsing */
	strncpy(m_cmd, cmd, MAX_LINE - 1);

	/* Separate command and parameters */
	{
		char *tok = NULL, *state = NULL;

		tok = strtok_r(m_cmd, " \t", &state);
		if (tok == NULL) {
			*error = "Cannot get command";
			return -1;
		}
		strncpy(command, tok, MAX_LINE - 1);

		tok = strtok_r(NULL, " \t", &state);
		if (tok == NULL) {
			*error = "Cannot get parameters";
			return -1;
		}
		strncpy(parameters, tok, MAX_LINE - 1);
	}

	/* Parsing of parameters.
	 *
	 * To simplify the code, we parse the command parameters here. Not all
	 * commands use all the parameters, but since they're not ambiguous it
	 * makes it easier to do it this way. */
	char *fp_name = NULL;
	int failnum = 1;
	void *failinfo = NULL;
	unsigned int flags = 0;
	double probability = -1;
	char *func_name = NULL;
	int func_pos_in_stack = -1;

	{
		/* Different tokens that we accept as parameters */
		enum {
			OPT_NAME = 0,
			OPT_FAILNUM,
			OPT_FAILINFO,
			OPT_PROBABILITY,
			OPT_FUNC_NAME,
			OPT_POS_IN_STACK,
			FLAG_ONETIME,
		};
		char *const token[] = {[OPT_NAME] = "name",
		                       [OPT_FAILNUM] = "failnum",
		                       [OPT_FAILINFO] = "failinfo",
		                       [OPT_PROBABILITY] = "probability",
		                       [OPT_FUNC_NAME] = "func_name",
		                       [OPT_POS_IN_STACK] = "pos_in_stack",
		                       [FLAG_ONETIME] = "onetime",
		                       NULL};

		char *value;
		char *opts = parameters;
		while (*opts != '\0') {
			switch (getsubopt(&opts, token, &value)) {
			case OPT_NAME:
				fp_name = value;
				break;
			case OPT_FAILNUM:
				failnum = atoi(value);
				break;
			case OPT_FAILINFO:
				failinfo = (void *)strtoul(value, NULL, 10);
				break;
			case OPT_PROBABILITY:
				probability = strtod(value, NULL);
				break;
			case OPT_FUNC_NAME:
				func_name = value;
				break;
			case OPT_POS_IN_STACK:
				func_pos_in_stack = atoi(value);
				break;
			case FLAG_ONETIME:
				flags |= FIU_ONETIME;
				break;
			default:
				*error = "Unknown parameter";
				return -1;
			}
		}
	}

	/* Excecute the command */
	if (strcmp(command, "disable") == 0) {
		*error = "Error in disable";
		return fiu_disable(fp_name);
	} else if (strcmp(command, "enable") == 0) {
		*error = "Error in enable";
		return fiu_enable(fp_name, failnum, failinfo, flags);
	} else if (strcmp(command, "enable_random") == 0) {
		*error = "Error in enable_random";
		return fiu_enable_random(fp_name, failnum, failinfo, flags,
		                         probability);
	} else if (strcmp(command, "enable_stack_by_name") == 0) {
		*error = "Error in enable_stack_by_name";
		return fiu_enable_stack_by_name(fp_name, failnum, failinfo,
		                                flags, func_name,
		                                func_pos_in_stack);
	} else {
		*error = "Unknown command";
		return -1;
	}
}

/* Read remote control directives from fdr and process them, writing the
 * results in fdw. Returns the length of the line read, 0 if EOF, or < 0 on
 * error. */
static int rc_do_command(int fdr, int fdw)
{
	int len, r, reply_len;
	char buf[MAX_LINE], reply[MAX_LINE];
	char *error = NULL;

	len = read_line(fdr, buf);
	if (len <= 0)
		return len;

	r = fiu_rc_string(buf, &error);
	if (r < 0)
		fprintf(stderr, "libfiu: rc parsing error: %s\n", error);

	reply_len = snprintf(reply, MAX_LINE, "%d\n", r);
	r = write(fdw, reply, reply_len);
	if (r <= 0)
		return r;

	return len;
}

/*
 * Remote control via named pipes
 *
 * Enables remote control over a named pipe that begins with the given
 * basename. "-$PID.in" will be appended to it to form the final path to read
 * commands from, and "-$PID.out" will be appended to it to form the final
 * path to write the replies to. After the process dies, the pipe will be
 * removed. If the process forks, a new pipe will be created.
 */

static char *npipe_basename = NULL;
static char *npipe_path_in = NULL;
static char *npipe_path_out = NULL;

static void *rc_fifo_thread(void *unused)
{
	int fdr, fdw, r, errcount;

	/* increment the recursion count so we're not affected by libfiu,
	 * otherwise we could make the remote control useless by enabling all
	 * failure points */
	rec_count++;

	errcount = 0;

reopen:
	if (errcount > 10) {
		fprintf(stderr, "libfiu: Too many errors in remote control "
		                "thread, shutting down\n");
		return NULL;
	}

	fdr = open(npipe_path_in, O_RDONLY);
	if (fdr < 0)
		return NULL;

	fdw = open(npipe_path_out, O_WRONLY);
	if (fdw < 0) {
		close(fdr);
		return NULL;
	}

	for (;;) {
		r = rc_do_command(fdr, fdw);
		if (r < 0 && errno != EPIPE) {
			perror("libfiu: Error reading from remote control");
			errcount++;
			close(fdr);
			close(fdw);
			goto reopen;
		} else if (r == 0 || (r < 0 && errno == EPIPE)) {
			/* one of the ends of the pipe was closed */
			close(fdr);
			close(fdw);
			goto reopen;
		}
	}

	/* we never get here */
}

static void fifo_atexit(void)
{
	unlink(npipe_path_in);
	unlink(npipe_path_out);
}

static int _fiu_rc_fifo(const char *basename)
{
	pthread_t thread;

	/* see rc_fifo_thread() */
	rec_count++;

	/* Allocate the pipe paths. Will be composed of
	 * "<basename>-<pid>.[in|out]", so leave plenty of room for the last
	 * part, to handle potentially long pids.
	 * These live through the entire life of the binary and are never
	 * freed. */
	int path_len = strlen(basename) + 40;
	npipe_path_in = malloc(path_len);
	npipe_path_out = malloc(path_len);

	snprintf(npipe_path_in, path_len, "%s-%d.in", basename, getpid());
	snprintf(npipe_path_out, path_len, "%s-%d.out", basename, getpid());

	if (mkfifo(npipe_path_in, 0600) != 0 && errno != EEXIST) {
		rec_count--;
		return -1;
	}

	if (mkfifo(npipe_path_out, 0600) != 0 && errno != EEXIST) {
		unlink(npipe_path_in);
		rec_count--;
		return -1;
	}

	if (pthread_create(&thread, NULL, rc_fifo_thread, NULL) != 0) {
		unlink(npipe_path_in);
		unlink(npipe_path_out);
		rec_count--;
		return -1;
	}

	atexit(fifo_atexit);

	rec_count--;
	return 0;
}

static void fifo_atfork_child(void)
{
	_fiu_rc_fifo(npipe_basename);
}

int fiu_rc_fifo(const char *basename)
{
	int r;

	r = _fiu_rc_fifo(basename);
	if (r < 0)
		return r;

	npipe_basename = strdup(basename);
	pthread_atfork(NULL, NULL, fifo_atfork_child);

	return r;
}
