/*
 * Copyright (C) 2008-2009 Tobias Brunner
 * Copyright (C) 2007 Martin Willi
 * Hochschule fuer Technik Rapperswil
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <dirent.h>
#include <termios.h>
#include <stdarg.h>

#include <utils/debug.h>
#include <collections/linked_list.h>

#include "dumm.h"
#include "guest.h"
#include "mconsole.h"
#include "cowfs.h"

#define PERME (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
#define PERM (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH)

#define MASTER_DIR "master"
#define DIFF_DIR "diff"
#define UNION_DIR "union"
#define ARGS_FILE "args"
#define PID_FILE "pid"
#define KERNEL_FILE "linux"
#define LOG_FILE "boot.log"
#define NOTIFY_FILE "notify"
#define PTYS 0

typedef struct private_guest_t private_guest_t;

struct private_guest_t {
	/** implemented public interface */
	guest_t public;
	/** name of the guest */
	char *name;
	/** directory of guest */
	int dir;
	/** directory name of guest */
	char *dirname;
	/** additional args to pass to guest */
	char *args;
	/** pid of guest child process */
	int pid;
	/** state of guest */
	guest_state_t state;
	/** FUSE cowfs instance */
	cowfs_t *cowfs;
	/** mconsole to control running UML */
	mconsole_t *mconsole;
	/** list of interfaces attached to the guest */
	linked_list_t *ifaces;
};

ENUM(guest_state_names, GUEST_STOPPED, GUEST_STOPPING,
	"STOPPED",
	"STARTING",
	"RUNNING",
	"PAUSED",
	"STOPPING",
);

METHOD(guest_t, get_name, char*,
	private_guest_t *this)
{
	return this->name;
}

METHOD(guest_t, create_iface, iface_t*,
	private_guest_t *this, char *name)
{
	enumerator_t *enumerator;
	iface_t *iface;

	if (this->state != GUEST_RUNNING)
	{
		DBG1(DBG_LIB, "guest '%s' not running, unable to add interface",
			 this->name);
		return NULL;
	}

	enumerator = this->ifaces->create_enumerator(this->ifaces);
	while (enumerator->enumerate(enumerator, (void**)&iface))
	{
		if (streq(name, iface->get_guestif(iface)))
		{
			DBG1(DBG_LIB, "guest '%s' already has an interface '%s'",
				 this->name, name);
			enumerator->destroy(enumerator);
			return NULL;
		}
	}
	enumerator->destroy(enumerator);

	iface = iface_create(name, &this->public, this->mconsole);
	if (iface)
	{
		this->ifaces->insert_last(this->ifaces, iface);
	}
	return iface;
}

METHOD(guest_t, destroy_iface, void,
	private_guest_t *this, iface_t *iface)
{
	enumerator_t *enumerator;
	iface_t *current;

	enumerator = this->ifaces->create_enumerator(this->ifaces);
	while (enumerator->enumerate(enumerator, (void**)&current))
	{
		if (current == iface)
		{
			this->ifaces->remove_at(this->ifaces, enumerator);
			current->destroy(current);
			break;
		}
	}
	enumerator->destroy(enumerator);
}

METHOD(guest_t, create_iface_enumerator, enumerator_t*,
	private_guest_t *this)
{
	return this->ifaces->create_enumerator(this->ifaces);
}

METHOD(guest_t, get_state, guest_state_t,
	private_guest_t *this)
{
	return this->state;
}

METHOD(guest_t, get_pid, pid_t,
	private_guest_t *this)
{
	return this->pid;
}

/**
 * write format string to a buffer, and advance buffer position
 */
static char* write_arg(char **pos, size_t *left, char *format, ...)
{
	size_t len;
	char *res = NULL;
	va_list args;

	va_start(args, format);
	len = vsnprintf(*pos, *left, format, args);
	va_end(args);
	if (len < *left)
	{
		res = *pos;
		len++;
		*pos += len + 1;
		*left -= len + 1;
	}
	return res;
}

METHOD(guest_t, stop, void,
	private_guest_t *this, idle_function_t idle)
{
	if (this->state != GUEST_STOPPED)
	{
		this->state = GUEST_STOPPING;
		this->ifaces->destroy_offset(this->ifaces, offsetof(iface_t, destroy));
		this->ifaces = linked_list_create();
		kill(this->pid, SIGINT);
		while (this->state != GUEST_STOPPED)
		{
			if (idle)
			{
				idle();
			}
			else
			{
				usleep(50000);
			}
		}
		unlinkat(this->dir, PID_FILE, 0);
		this->pid = 0;
	}
}

/**
 * save pid in file
 */
void savepid(private_guest_t *this)
{
	FILE *file;

	file = fdopen(openat(this->dir, PID_FILE, O_RDWR | O_CREAT | O_TRUNC,
						 PERM), "w");
	if (file)
	{
		fprintf(file, "%d", this->pid);
		fclose(file);
	}
}

METHOD(guest_t, start, bool,
	private_guest_t *this, invoke_function_t invoke, void* data,
	idle_function_t idle)
{
	char buf[2048];
	char *notify;
	char *pos = buf;
	char *args[32];
	int i = 0;
	size_t left = sizeof(buf);

	memset(args, 0, sizeof(args));

	if (this->state != GUEST_STOPPED)
	{
		DBG1(DBG_LIB, "unable to start guest in state %N", guest_state_names,
			 this->state);
		return FALSE;
	}
	this->state = GUEST_STARTING;

	notify = write_arg(&pos, &left, "%s/%s", this->dirname, NOTIFY_FILE);

	args[i++] = write_arg(&pos, &left, "nice");
	args[i++] = write_arg(&pos, &left, "%s/%s", this->dirname, KERNEL_FILE);
	args[i++] = write_arg(&pos, &left, "root=/dev/root");
	args[i++] = write_arg(&pos, &left, "rootfstype=hostfs");
	args[i++] = write_arg(&pos, &left, "rootflags=%s/%s", this->dirname, UNION_DIR);
	args[i++] = write_arg(&pos, &left, "uml_dir=%s", this->dirname);
	args[i++] = write_arg(&pos, &left, "umid=%s", this->name);
	args[i++] = write_arg(&pos, &left, "mconsole=notify:%s", notify);
	args[i++] = write_arg(&pos, &left, "con=null");
	if (this->args)
	{
		args[i++] = this->args;
	}

	this->pid = invoke(data, &this->public, args, i);
	if (!this->pid)
	{
		this->state = GUEST_STOPPED;
		return FALSE;
	}
	savepid(this);

	/* open mconsole */
	this->mconsole = mconsole_create(notify, idle);
	if (this->mconsole == NULL)
	{
		DBG1(DBG_LIB, "opening mconsole at '%s' failed, stopping guest", buf);
		stop(this, NULL);
		return FALSE;
	}

	this->state = GUEST_RUNNING;
	return TRUE;
}

METHOD(guest_t, add_overlay, bool,
	private_guest_t *this, char *path)
{
	if (path == NULL)
	{
		return FALSE;
	}

	if (access(path, F_OK) != 0)
	{
		if (!mkdir_p(path, PERME))
		{
			DBG1(DBG_LIB, "creating overlay for guest '%s' failed: %m",
				 this->name);
			return FALSE;
		}
	}

	return this->cowfs->add_overlay(this->cowfs, path);
}

METHOD(guest_t, del_overlay, bool,
	private_guest_t *this, char *path)
{
	return this->cowfs->del_overlay(this->cowfs, path);
}

METHOD(guest_t, pop_overlay, bool,
	private_guest_t *this)
{
	return this->cowfs->pop_overlay(this->cowfs);
}

/**
 * Variadic version of the exec function
 */
static int vexec(private_guest_t *this, void(*cb)(void*,char*,size_t), void *data,
				 char *cmd, va_list args)
{
	char buf[1024];
	size_t len;

	if (this->mconsole)
	{
		len = vsnprintf(buf, sizeof(buf), cmd, args);

		if (len > 0 && len < sizeof(buf))
		{
			return this->mconsole->exec(this->mconsole, cb, data, buf);
		}
	}
	return -1;
}

METHOD(guest_t, exec, int,
	private_guest_t *this, void(*cb)(void*,char*,size_t), void *data,
	char *cmd, ...)
{
	int res;
	va_list args;
	va_start(args, cmd);
	res = vexec(this, cb, data, cmd, args);
	va_end(args);
	return res;
}

typedef struct {
	chunk_t buf;
	void (*cb)(void*,char*);
	void *data;
} exec_str_t;

/**
 * callback that combines chunks to a string. if a callback is given, the string
 * is split at newlines and the callback is called for each line.
 */
static void exec_str_cb(exec_str_t *data, char *buf, size_t len)
{
	if (!data->buf.ptr)
	{
		data->buf = chunk_alloc(len + 1);
		memcpy(data->buf.ptr, buf, len);
		data->buf.ptr[len] = '\0';
	}
	else
	{
		size_t newlen = strlen(data->buf.ptr) + len + 1;
		if (newlen > data->buf.len)
		{
			data->buf.ptr = realloc(data->buf.ptr, newlen);
			data->buf.len = newlen;
		}
		strncat(data->buf.ptr, buf, len);
	}

	if (data->cb)
	{
		char *nl;
		while ((nl = strchr(data->buf.ptr, '\n')) != NULL)
		{
			*nl++ = '\0';
			data->cb(data->data, data->buf.ptr);
			memmove(data->buf.ptr, nl, strlen(nl) + 1);
		}
	}
}

METHOD(guest_t, exec_str, int,
	private_guest_t *this, void(*cb)(void*,char*), bool lines, void *data,
	char *cmd, ...)
{
	int res;
	va_list args;
	va_start(args, cmd);
	if (cb)
	{
		exec_str_t exec = { chunk_empty, NULL, NULL };
		if (lines)
		{
			exec.cb = cb;
			exec.data = data;
		}
		res = vexec(this, (void(*)(void*,char*,size_t))exec_str_cb, &exec, cmd, args);
		if (exec.buf.ptr)
		{
			if (!lines || strlen(exec.buf.ptr) > 0)
			{
				/* return the complete string or the remaining stuff in the
				 * buffer (i.e. when there was no newline at the end) */
				cb(data, exec.buf.ptr);
			}
			chunk_free(&exec.buf);
		}
	}
	else
	{
		res = vexec(this, NULL, NULL, cmd, args);
	}
	va_end(args);
	return res;
}

METHOD(guest_t, sigchild, void,
	private_guest_t *this)
{
	DESTROY_IF(this->mconsole);
	this->mconsole = NULL;
	this->state = GUEST_STOPPED;
}

/**
 * umount the union filesystem
 */
static bool umount_unionfs(private_guest_t *this)
{
	if (this->cowfs)
	{
		this->cowfs->destroy(this->cowfs);
		this->cowfs = NULL;
		return TRUE;
	}
	return FALSE;
}

/**
 * mount the union filesystem
 */
static bool mount_unionfs(private_guest_t *this)
{
	char master[PATH_MAX];
	char diff[PATH_MAX];
	char mount[PATH_MAX];

	if (this->cowfs == NULL)
	{
		snprintf(master, sizeof(master), "%s/%s", this->dirname, MASTER_DIR);
		snprintf(diff, sizeof(diff), "%s/%s", this->dirname, DIFF_DIR);
		snprintf(mount, sizeof(mount), "%s/%s", this->dirname, UNION_DIR);

		this->cowfs = cowfs_create(master, diff, mount);
		if (this->cowfs)
		{
			return TRUE;
		}
	}
	return FALSE;
}

/**
 * load args configuration from file
 */
char *loadargs(private_guest_t *this)
{
	FILE *file;
	char buf[512], *args = NULL;

	file = fdopen(openat(this->dir, ARGS_FILE, O_RDONLY, PERM), "r");
	if (file)
	{
		if (fgets(buf, sizeof(buf), file))
		{
			args = strdup(buf);
		}
		fclose(file);
	}
	return args;
}

/**
 * save args configuration to file
 */
bool saveargs(private_guest_t *this, char *args)
{
	FILE *file;
	bool retval = FALSE;

	file = fdopen(openat(this->dir, ARGS_FILE, O_RDWR | O_CREAT | O_TRUNC,
						 PERM), "w");
	if (file)
	{
		if (fprintf(file, "%s", args) > 0)
		{
			retval = TRUE;
		}
		fclose(file);
	}
	return retval;
}

METHOD(guest_t, destroy, void,
	private_guest_t *this)
{
	stop(this, NULL);
	umount_unionfs(this);
	if (this->dir > 0)
	{
		close(this->dir);
	}
	this->ifaces->destroy(this->ifaces);
	free(this->dirname);
	free(this->args);
	free(this->name);
	free(this);
}

/**
 * generic guest constructor
 */
static private_guest_t *guest_create_generic(char *parent, char *name,
											 bool create)
{
	char cwd[PATH_MAX];
	private_guest_t *this;

	INIT(this,
		.public = {
			.get_name = _get_name,
			.get_pid = _get_pid,
			.get_state = _get_state,
			.create_iface = _create_iface,
			.destroy_iface = _destroy_iface,
			.create_iface_enumerator = _create_iface_enumerator,
			.start = _start,
			.stop = _stop,
			.add_overlay = _add_overlay,
			.del_overlay = _del_overlay,
			.pop_overlay = _pop_overlay,
			.exec = _exec,
			.exec_str = _exec_str,
			.sigchild = _sigchild,
			.destroy = _destroy,
		}
	);

	if (*parent == '/' || getcwd(cwd, sizeof(cwd)) == NULL)
	{
		if (asprintf(&this->dirname, "%s/%s", parent, name) < 0)
		{
			this->dirname = NULL;
		}
	}
	else
	{
		if (asprintf(&this->dirname, "%s/%s/%s", cwd, parent, name) < 0)
		{
			this->dirname = NULL;
		}
	}
	if (this->dirname == NULL)
	{
		free(this);
		return NULL;
	}
	if (create)
	{
		mkdir(this->dirname, PERME);
	}
	this->dir = open(this->dirname, O_DIRECTORY, PERME);
	if (this->dir < 0)
	{
		DBG1(DBG_LIB, "opening guest directory '%s' failed: %m", this->dirname);
		free(this->dirname);
		free(this);
		return NULL;
	}
	this->state = GUEST_STOPPED;
	this->ifaces = linked_list_create();
	this->name = strdup(name);

	return this;
}

/**
 * create a symlink to old called new in our working dir
 */
static bool make_symlink(private_guest_t *this, char *old, char *new)
{
	char cwd[PATH_MAX];
	char buf[PATH_MAX];

	if (*old == '/' || getcwd(cwd, sizeof(cwd)) == NULL)
	{
		snprintf(buf, sizeof(buf), "%s", old);
	}
	else
	{
		snprintf(buf, sizeof(buf), "%s/%s", cwd, old);
	}
	return symlinkat(buf, this->dir, new) == 0;
}


/**
 * create the guest instance, including required dirs and mounts
 */
guest_t *guest_create(char *parent, char *name, char *kernel,
					  char *master, char *args)
{
	private_guest_t *this = guest_create_generic(parent, name, TRUE);

	if (this == NULL)
	{
		return NULL;
	}

	if (!make_symlink(this, master, MASTER_DIR) ||
		!make_symlink(this, kernel, KERNEL_FILE))
	{
		DBG1(DBG_LIB, "creating master/kernel symlink failed: %m");
		destroy(this);
		return NULL;
	}

	if (mkdirat(this->dir, UNION_DIR, PERME) != 0 ||
		mkdirat(this->dir, DIFF_DIR, PERME) != 0)
	{
		DBG1(DBG_LIB, "unable to create directories for '%s': %m", name);
		destroy(this);
		return NULL;
	}

	this->args = args;
	if (args && !saveargs(this, args))
	{
		destroy(this);
		return NULL;
	}

	if (!mount_unionfs(this))
	{
		destroy(this);
		return NULL;
	}

	return &this->public;
}

/**
 * load an already created guest
 */
guest_t *guest_load(char *parent, char *name)
{
	private_guest_t *this = guest_create_generic(parent, name, FALSE);

	if (this == NULL)
	{
		return NULL;
	}

	this->args = loadargs(this);

	if (!mount_unionfs(this))
	{
		destroy(this);
		return NULL;
	}

	return &this->public;
}