/*
 * Copyright (c) 2009 Red Hat, Inc.
 *
 * All rights reserved.
 *
 * Author: Jan Friesse (jfriesse@redhat.com)
 *
 * This software licensed under BSD license, the text of which follows:
 *
 * 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 Red Hat, Inc. 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.
 */

/*
 * Provides a SAM API
 */

#include <config.h>

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>

#include <corosync/corotypes.h>
#include <corosync/coroipc_types.h>
#include <corosync/coroipcc.h>
#include <corosync/corodefs.h>
#include <corosync/hdb.h>

#include <corosync/sam.h>

#include "util.h"

#include <stdio.h>
#include <sys/wait.h>
#include <signal.h>

enum sam_internal_status_t {
	SAM_INTERNAL_STATUS_NOT_INITIALIZED = 0,
	SAM_INTERNAL_STATUS_INITIALIZED,
	SAM_INTERNAL_STATUS_REGISTERED,
	SAM_INTERNAL_STATUS_STARTED,
	SAM_INTERNAL_STATUS_FINALIZED
};

enum sam_command_t {
	SAM_COMMAND_START,
	SAM_COMMAND_STOP,
	SAM_COMMAND_HB
};

enum sam_parent_action_t {
	SAM_PARENT_ACTION_ERROR,
	SAM_PARENT_ACTION_RECOVERY,
	SAM_PARENT_ACTION_QUIT,
	SAM_PARENT_ACTION_CONTINUE
};

static struct {
	int time_interval;
	sam_recovery_policy_t recovery_policy;
	enum sam_internal_status_t internal_status;
	unsigned int instance_id;
	int parent_fd;
	int term_send;

	sam_hc_callback_t hc_callback;
	pthread_t cb_thread;
	int cb_rpipe_fd, cb_wpipe_fd;
	int cb_registered;
} sam_internal_data;

cs_error_t sam_initialize (
	int time_interval,
	sam_recovery_policy_t recovery_policy)
{
	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_NOT_INITIALIZED) {
		return (CS_ERR_BAD_HANDLE);
	}

	if (recovery_policy != SAM_RECOVERY_POLICY_QUIT && recovery_policy != SAM_RECOVERY_POLICY_RESTART) {
		return (CS_ERR_INVALID_PARAM);
	}

	sam_internal_data.recovery_policy = recovery_policy;

	sam_internal_data.time_interval = time_interval;

	sam_internal_data.internal_status = SAM_INTERNAL_STATUS_INITIALIZED;

	return (CS_OK);
}

/*
 * Wrapper on top of write(2) function. It handles EAGAIN and EINTR states and sends whole buffer if possible.
 */
static size_t sam_safe_write (
	int d,
	const void *buf,
	size_t nbyte)
{
	ssize_t bytes_write;
	ssize_t tmp_bytes_write;

	bytes_write = 0;

	do {
		tmp_bytes_write = write (d, (const char *)buf + bytes_write, nbyte - bytes_write);

		if (tmp_bytes_write == -1) {
			if (!(errno == EAGAIN || errno == EINTR))
				return -1;
		} else {
			bytes_write += tmp_bytes_write;
		}
	} while (bytes_write != nbyte);

	return bytes_write;
}

cs_error_t sam_start (void)
{
	char command;

	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_REGISTERED) {
		return (CS_ERR_BAD_HANDLE);
	}

	command = SAM_COMMAND_START;

	if (sam_safe_write (sam_internal_data.parent_fd, &command, 1) == -1)
		return (CS_ERR_LIBRARY);

	if (sam_internal_data.hc_callback)
		if (sam_safe_write (sam_internal_data.cb_wpipe_fd, &command, 1) == -1)
			return (CS_ERR_LIBRARY);

	sam_internal_data.internal_status = SAM_INTERNAL_STATUS_STARTED;

	return (CS_OK);
}

cs_error_t sam_stop (void)
{
	char command;

	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_STARTED) {
		return (CS_ERR_BAD_HANDLE);
	}

	command = SAM_COMMAND_STOP;

	if (sam_safe_write (sam_internal_data.parent_fd, &command, 1) == -1)
		return (CS_ERR_LIBRARY);

	if (sam_internal_data.hc_callback)
		if (sam_safe_write (sam_internal_data.cb_wpipe_fd, &command, 1) == -1)
			return (CS_ERR_LIBRARY);

	sam_internal_data.internal_status = SAM_INTERNAL_STATUS_REGISTERED;

	return (CS_OK);
}

cs_error_t sam_hc_send (void)
{
	char command;

	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_STARTED) {
		return (CS_ERR_BAD_HANDLE);
	}

	command = SAM_COMMAND_HB;

	if (sam_safe_write (sam_internal_data.parent_fd, &command, 1) == -1)
		return (CS_ERR_LIBRARY);

	return (CS_OK);
}

cs_error_t sam_finalize (void)
{
	cs_error_t error;

	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_INITIALIZED &&
		sam_internal_data.internal_status != SAM_INTERNAL_STATUS_REGISTERED &&
		sam_internal_data.internal_status != SAM_INTERNAL_STATUS_STARTED) {
		return (CS_ERR_BAD_HANDLE);
	}

	if (sam_internal_data.internal_status == SAM_INTERNAL_STATUS_STARTED) {
		error = sam_stop ();
		if (error != CS_OK)
			goto exit_error;
	}

	sam_internal_data.internal_status = SAM_INTERNAL_STATUS_FINALIZED;

exit_error:
	return (CS_OK);
}



static enum sam_parent_action_t sam_parent_handler (int pipe_fd, pid_t child_pid)
{
	int poll_error;
	int action;
	int status;
	ssize_t bytes_read;
	char command;
	int time_interval;
	struct pollfd pfds;

	status = 0;

	action = SAM_PARENT_ACTION_CONTINUE;

	while (action == SAM_PARENT_ACTION_CONTINUE) {
		pfds.fd = pipe_fd;
		pfds.events = POLLIN;
		pfds.revents = 0;

		if (status == 1 && sam_internal_data.time_interval != 0) {
			time_interval = sam_internal_data.time_interval;
		} else {
			time_interval = -1;
		}

		poll_error = poll (&pfds, 1, time_interval);

		if (poll_error == -1) {
			/*
			 *  Error in poll
			 *  If it is EINTR, continue, otherwise QUIT
			 */
			if (errno != EINTR) {
				action = SAM_PARENT_ACTION_ERROR;
			}
		}

		if (poll_error == 0) {
			/*
			 *  Time limit expires
			 */
			if (status == 0) {
				action = SAM_PARENT_ACTION_QUIT;
			} else {
				/*
				 *  Kill child process
				 */
				if (!sam_internal_data.term_send) {
					/*
					 * We didn't send SIGTERM (warning) yet.
					 */

					kill (child_pid, SIGTERM);

					sam_internal_data.term_send = 1;
				} else {
					/*
					 * We sent child warning. Now, we will not be so nice
					 */
					kill (child_pid, SIGKILL);
					action = SAM_PARENT_ACTION_RECOVERY;
				}
			}
		}

		if (poll_error > 0) {
			/*
			 *  We have EOF or command in pipe
			 */
			bytes_read = read (pipe_fd, &command, 1);

			if (bytes_read == 0) {
				/*
				 *  Handle EOF -> Take recovery action or quit if sam_start wasn't called
				 */
				if (status == 0)
					action = SAM_PARENT_ACTION_QUIT;
				else
					action = SAM_PARENT_ACTION_RECOVERY;

				continue;
			}

			if (bytes_read == -1) {
				/*
				 * Something really bad happened in read side
				 */
				if (errno == EAGAIN || errno == EINTR) {
					continue;
				}

				action = SAM_PARENT_ACTION_ERROR;
				goto action_exit;
			}

			/*
			 * We have read command -> take status
			 */
			switch (status) {
			case 0:
				/*
				 *  Not started yet
				 */
				if (command == SAM_COMMAND_START)
					status = 1;
			break;

			case 1:
				/*
				 *  Started
				 */
				if (command == SAM_COMMAND_STOP)
					status = 0;
			break;

			}
		} /* select_error > 0 */
	} /* action == SAM_PARENT_ACTION_CONTINUE */

action_exit:
	return action;
}

cs_error_t sam_register (
	unsigned int *instance_id)
{
	cs_error_t error;
	pid_t pid;
	int pipe_error;
	int pipe_fd[2];
	enum sam_parent_action_t action;
	int child_status;

	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_INITIALIZED) {
		return (CS_ERR_BAD_HANDLE);
	}

	error = CS_OK;

	while (1) {
		pipe_error = pipe (pipe_fd);

		if (pipe_error != 0) {
			/*
			 *  Pipe creation error
			 */
			error = CS_ERR_LIBRARY;
			goto error_exit;
		}

		sam_internal_data.instance_id++;

		sam_internal_data.term_send = 0;

		pid = fork ();

		if (pid == -1) {
			/*
			 *  Fork error
			 */
			sam_internal_data.instance_id--;

			error = CS_ERR_LIBRARY;
			goto error_exit;
		}

		if (pid == 0) {
			/*
			 *  Child process
			 */
			close (pipe_fd[0]);

			sam_internal_data.parent_fd = pipe_fd[1];
			if (instance_id)
				*instance_id = sam_internal_data.instance_id;

			sam_internal_data.internal_status = SAM_INTERNAL_STATUS_REGISTERED;

			goto error_exit;
		} else {
			/*
			 *  Parent process
			 */
			close (pipe_fd[1]);

			action = sam_parent_handler (pipe_fd[0], pid);

			close (pipe_fd[0]);

			if (action == SAM_PARENT_ACTION_ERROR) {
				error = CS_ERR_LIBRARY;
				goto error_exit;
			}

			/*
			 * We really don't like zombies
			 */
			while (waitpid (pid, &child_status, 0) == -1 && errno == EINTR)
				;

			if (action == SAM_PARENT_ACTION_RECOVERY) {
				if (sam_internal_data.recovery_policy == SAM_RECOVERY_POLICY_QUIT)
					action = SAM_PARENT_ACTION_QUIT;
			}

			if (action == SAM_PARENT_ACTION_QUIT) {
				exit (WEXITSTATUS (child_status));
			}

		}
	}

error_exit:
	return (error);
}

static void *hc_callback_thread (void *unused_param)
{
	int poll_error;
	int status;
	ssize_t bytes_readed;
	char command;
	int time_interval, tmp_time_interval;
	int counter;
	struct pollfd pfds;

	status = 0;
	counter = 0;

	time_interval = sam_internal_data.time_interval >> 2;

	while (1) {
		pfds.fd = sam_internal_data.cb_rpipe_fd;
		pfds.events = POLLIN;
		pfds.revents = 0;

		if (status == 1) {
			tmp_time_interval = time_interval;
		} else {
			tmp_time_interval = -1;
		}

		poll_error = poll (&pfds, 1, tmp_time_interval);

		if (poll_error == 0) {
			sam_hc_send ();
			counter++;

			if (counter >= 4) {
				if (sam_internal_data.hc_callback () != 0) {
					status = 3;
				}

				counter = 0;
			}
		}

		if (poll_error > 0) {
			bytes_readed = read (sam_internal_data.cb_rpipe_fd, &command, 1);

			if (bytes_readed > 0) {
				if (status == 0 && command == SAM_COMMAND_START)
					status = 1;

				if (status == 1 && command == SAM_COMMAND_STOP)
					status = 0;

			}
		}
	}

	/*
	 * This makes compiler happy, it's same as return (NULL);
	 */
	return (unused_param);
}

cs_error_t sam_hc_callback_register (sam_hc_callback_t cb)
{
	cs_error_t error = CS_OK;
	pthread_attr_t thread_attr;
	int pipe_error;
	int pipe_fd[2];

	if (sam_internal_data.internal_status != SAM_INTERNAL_STATUS_REGISTERED) {
		return (CS_ERR_BAD_HANDLE);
	}

	if (sam_internal_data.time_interval == 0) {
		return (CS_ERR_INVALID_PARAM);
	}

	if (sam_internal_data.cb_registered) {
		sam_internal_data.hc_callback = cb;

		return (CS_OK);
	}

	/*
	 * We know, this is first registration
	 */

	if (cb == NULL) {
		return (CS_ERR_INVALID_PARAM);
	}

	pipe_error = pipe (pipe_fd);

	if (pipe_error != 0) {
		/*
		 *  Pipe creation error
		 */
		error = CS_ERR_LIBRARY;
		goto error_exit;
	}

	sam_internal_data.cb_rpipe_fd = pipe_fd[0];
	sam_internal_data.cb_wpipe_fd = pipe_fd[1];

	/*
	 * Create thread attributes
	 */
	error = pthread_attr_init (&thread_attr);
	if (error != 0) {
		error = CS_ERR_LIBRARY;
		goto error_close_fd_exit;
	}


	pthread_attr_setdetachstate (&thread_attr, PTHREAD_CREATE_DETACHED);
	pthread_attr_setstacksize (&thread_attr, 32768);

	/*
	 * Create thread
	 */
	error = pthread_create (&sam_internal_data.cb_thread, &thread_attr, hc_callback_thread, NULL);

	if (error != 0) {
		error = CS_ERR_LIBRARY;
		goto error_attr_destroy_exit;
	}

	/*
	 * Cleanup
	 */
	pthread_attr_destroy(&thread_attr);

	sam_internal_data.cb_registered = 1;
	sam_internal_data.hc_callback = cb;

	return (CS_OK);

error_attr_destroy_exit:
	pthread_attr_destroy(&thread_attr);
error_close_fd_exit:
	sam_internal_data.cb_rpipe_fd = sam_internal_data.cb_wpipe_fd = 0;
	close (pipe_fd[0]);
	close (pipe_fd[1]);
error_exit:
	return (error);
}
