/*
   CTDB cluster mutex handling

   Copyright (C) Andrew Tridgell  2007
   Copyright (C) Ronnie Sahlberg  2007
   Copyright (C) Martin Schwenke  2016

   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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include "replace.h"
#include "system/network.h"

#include <tevent.h>

#include "lib/util/debug.h"
#include "lib/util/time.h"
#include "lib/util/strv.h"
#include "lib/util/strv_util.h"
#include "lib/util/sys_rw.h"
#include "lib/util/blocking.h"

#include "ctdb_private.h"
#include "common/common.h"
#include "common/logging.h"

#include "ctdb_cluster_mutex.h"

struct ctdb_cluster_mutex_handle {
	struct ctdb_context *ctdb;
	cluster_mutex_handler_t handler;
	void *private_data;
	cluster_mutex_lost_handler_t lost_handler;
	void *lost_data;
	int fd[2];
	struct tevent_timer *te;
	struct tevent_fd *fde;
	pid_t child;
	struct timeval start_time;
	bool have_response;
};

static void cluster_mutex_timeout(struct tevent_context *ev,
				  struct tevent_timer *te,
				  struct timeval t, void *private_data)
{
	struct ctdb_cluster_mutex_handle *h =
		talloc_get_type(private_data, struct ctdb_cluster_mutex_handle);
	double latency = timeval_elapsed(&h->start_time);

	if (h->handler != NULL) {
		h->handler('2', latency, h->private_data);
	}
}


/* When the handle is freed it causes any child holding the mutex to
 * be killed, thus freeing the mutex */
static int cluster_mutex_destructor(struct ctdb_cluster_mutex_handle *h)
{
	if (h->fd[0] != -1) {
		h->fd[0] = -1;
	}
	ctdb_kill(h->ctdb, h->child, SIGTERM);
	return 0;
}

/* this is called when the client process has completed ctdb_recovery_lock()
   and has written data back to us through the pipe.
*/
static void cluster_mutex_handler(struct tevent_context *ev,
				  struct tevent_fd *fde,
				  uint16_t flags, void *private_data)
{
	struct ctdb_cluster_mutex_handle *h=
		talloc_get_type(private_data, struct ctdb_cluster_mutex_handle);
	double latency = timeval_elapsed(&h->start_time);
	char c = '0';
	int ret;

	/* Got response from child process so abort timeout */
	TALLOC_FREE(h->te);

	ret = sys_read(h->fd[0], &c, 1);

	/* Don't call the handler more than once.  It only exists to
	 * process the initial response from the helper. */
	if (h->have_response) {
		/* Only deal with EOF due to process exit.  Silently
		 * ignore any other output. */
		if (ret == 0) {
			if (h->lost_handler != NULL) {
				h->lost_handler(h->lost_data);
			}
		}
		return;
	}
	h->have_response = true;

	/* If the child wrote status then just pass it to the handler.
	 * If no status was written then this is an unexpected error
	 * so pass generic error code to handler. */
	if (h->handler != NULL) {
		h->handler(ret == 1 ? c : '3', latency, h->private_data);
	}
}

static char cluster_mutex_helper[PATH_MAX+1] = "";

static bool cluster_mutex_helper_args_file(TALLOC_CTX *mem_ctx,
					   const char *argstring,
					   char ***argv)
{
	bool ok;
	char **args = NULL;

	ok = ctdb_set_helper("cluster mutex helper",
			     cluster_mutex_helper,
			     sizeof(cluster_mutex_helper),
			     "CTDB_CLUSTER_MUTEX_HELPER",
			     CTDB_HELPER_BINDIR,
			     "ctdb_mutex_fcntl_helper");
	if (! ok) {
		DBG_ERR("ctdb exiting with error: "
			"Unable to set cluster mutex helper\n");
		exit(1);
	}


	/* Array includes default helper, file and NULL */
	args = talloc_array(mem_ctx, char *, 3);
	if (args == NULL) {
		DBG_ERR("Memory allocation error\n");
		return false;
	}

	args[0] = cluster_mutex_helper;

	args[1] = talloc_strdup(args, argstring);
	if (args[1] == NULL) {
		DBG_ERR("Memory allocation error\n");
		return false;
	}

	args[2] = NULL;

	*argv = args;
	return true;
}

static bool cluster_mutex_helper_args_cmd(TALLOC_CTX *mem_ctx,
					  const char *argstring,
					  char ***argv)
{
	int i, ret, n;
	char **args = NULL;
	char *strv = NULL;
	char *t = NULL;

	ret = strv_split(mem_ctx, &strv, argstring, " \t");
	if (ret != 0) {
		D_ERR("Unable to parse mutex helper command \"%s\" (%s)\n",
		      argstring,
		      strerror(ret));
		return false;
	}
	n = strv_count(strv);

	/* Extra slot for NULL */
	args = talloc_array(mem_ctx, char *, n + 1);
	if (args == NULL) {
		DBG_ERR("Memory allocation error\n");
		return false;
	}

	talloc_steal(args, strv);

	t = NULL;
	for (i = 0 ; i < n; i++) {
		t = strv_next(strv, t);
		args[i] = t;
	}

	args[n] = NULL;

	*argv = args;
	return true;
}

static bool cluster_mutex_helper_args(TALLOC_CTX *mem_ctx,
				      const char *argstring,
				      char ***argv)
{
	bool ok;

	if (argstring != NULL && argstring[0] == '!') {
		ok = cluster_mutex_helper_args_cmd(mem_ctx, &argstring[1], argv);
	} else {
		ok = cluster_mutex_helper_args_file(mem_ctx, argstring, argv);
	}

	return ok;
}

struct ctdb_cluster_mutex_handle *
ctdb_cluster_mutex(TALLOC_CTX *mem_ctx,
		   struct ctdb_context *ctdb,
		   const char *argstring,
		   int timeout,
		   cluster_mutex_handler_t handler,
		   void *private_data,
		   cluster_mutex_lost_handler_t lost_handler,
		   void *lost_data)
{
	struct ctdb_cluster_mutex_handle *h;
	char **args;
	sigset_t sigset_term;
	int ret;

	h = talloc(mem_ctx, struct ctdb_cluster_mutex_handle);
	if (h == NULL) {
		DEBUG(DEBUG_ERR, (__location__ " out of memory\n"));
		return NULL;
	}

	h->start_time = timeval_current();
	h->fd[0] = -1;
	h->fd[1] = -1;
	h->have_response = false;

	ret = pipe(h->fd);
	if (ret != 0) {
		talloc_free(h);
		DEBUG(DEBUG_ERR, (__location__ " Failed to open pipe\n"));
		return NULL;
	}
	set_close_on_exec(h->fd[0]);

	/* Create arguments for lock helper */
	if (!cluster_mutex_helper_args(h, argstring, &args)) {
		close(h->fd[0]);
		close(h->fd[1]);
		talloc_free(h);
		return NULL;
	}

	sigemptyset(&sigset_term);
	sigaddset(&sigset_term, SIGTERM);
	ret = sigprocmask(SIG_BLOCK, &sigset_term, NULL);
	if (ret != 0) {
		DBG_WARNING("Failed to block SIGTERM (%d)\n", errno);
	}

	h->child = ctdb_fork(ctdb);
	if (h->child == (pid_t)-1) {
		close(h->fd[0]);
		close(h->fd[1]);
		talloc_free(h);
		ret = sigprocmask(SIG_UNBLOCK, &sigset_term, NULL);
		if (ret != 0) {
			DBG_WARNING("Failed to unblock SIGTERM (%d)\n", errno);
		}
		return NULL;
	}

	if (h->child == 0) {
		struct sigaction sa = {
			.sa_handler = SIG_DFL,
		};

		ret = sigaction(SIGTERM, &sa, NULL);
		if (ret != 0) {
			DBG_WARNING("Failed to reset signal handler (%d)\n",
				    errno);
		}

		ret = sigprocmask(SIG_UNBLOCK, &sigset_term, NULL);
		if (ret != 0) {
			DBG_WARNING("Failed to unblock SIGTERM (%d)\n", errno);
		}

		/* Make stdout point to the pipe */
		close(STDOUT_FILENO);
		dup2(h->fd[1], STDOUT_FILENO);
		close(h->fd[1]);

		execv(args[0], args);

		/* Only happens on error */
		DEBUG(DEBUG_ERR, (__location__ "execv() failed\n"));
		_exit(1);
	}

	/* Parent */

	ret = sigprocmask(SIG_UNBLOCK, &sigset_term, NULL);
	if (ret != 0) {
		DBG_WARNING("Failed to unblock SIGTERM (%d)\n", errno);
	}

	DEBUG(DEBUG_DEBUG, (__location__ " Created PIPE FD:%d\n", h->fd[0]));
	set_close_on_exec(h->fd[0]);

	close(h->fd[1]);
	h->fd[1] = -1;

	talloc_set_destructor(h, cluster_mutex_destructor);

	if (timeout != 0) {
		h->te = tevent_add_timer(ctdb->ev, h,
					 timeval_current_ofs(timeout, 0),
					 cluster_mutex_timeout, h);
	} else {
		h->te = NULL;
	}

	h->fde = tevent_add_fd(ctdb->ev, h, h->fd[0], TEVENT_FD_READ,
			       cluster_mutex_handler, (void *)h);

	if (h->fde == NULL) {
		talloc_free(h);
		return NULL;
	}
	tevent_fd_set_auto_close(h->fde);

	h->ctdb = ctdb;
	h->handler = handler;
	h->private_data = private_data;
	h->lost_handler = lost_handler;
	h->lost_data = lost_data;

	return h;
}
