/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */

#include "common.h"
#include "ioloop.h"
#include "fdpass.h"
#include "service.h"
#include "service-process.h"
#include "service-process-notify.h"
#include "service-anvil.h"

#include <unistd.h>

#define ANVIL_HANDSHAKE "VERSION\tanvil-client\t2\t0\n"

struct service_anvil_global *service_anvil_global;

static void
service_list_anvil_discard_input_stop(struct service_anvil_global *anvil)
{
	if (anvil->io_blocking != NULL) {
		io_remove(&anvil->io_blocking);
		io_remove(&anvil->io_nonblocking);
	}
}

static void anvil_input_fd_discard(struct service_anvil_global *anvil, int fd)
{
	char buf[1024];
	ssize_t ret;

	ret = read(fd, buf, sizeof(buf));
	if (ret <= 0) {
		i_error("read(anvil fd) failed: %m");
		service_list_anvil_discard_input_stop(anvil);
	}
}

static void anvil_input_blocking_discard(struct service_anvil_global *anvil)
{
	anvil_input_fd_discard(anvil, anvil->blocking_fd[0]);
}

static void anvil_input_nonblocking_discard(struct service_anvil_global *anvil)
{
	anvil_input_fd_discard(anvil, anvil->nonblocking_fd[0]);
}

static void service_list_anvil_discard_input(struct service_anvil_global *anvil)
{
	if (anvil->io_blocking != NULL)
		return;

	anvil->io_blocking = io_add(anvil->blocking_fd[0], IO_READ,
				    anvil_input_blocking_discard, anvil);
	anvil->io_nonblocking = io_add(anvil->nonblocking_fd[0], IO_READ,
				       anvil_input_nonblocking_discard, anvil);
}

static int anvil_send_handshake(int fd, const char **error_r)
{
	ssize_t ret;

	ret = write(fd, ANVIL_HANDSHAKE, strlen(ANVIL_HANDSHAKE));
	if (ret < 0) {
		*error_r = t_strdup_printf("write(anvil) failed: %m");
		return -1;
	}
	if (ret == 0) {
		*error_r = t_strdup_printf("write(anvil) returned EOF");
		return -1;
	}
	/* this is a pipe, it either wrote all of it or nothing */
	i_assert((size_t)ret == strlen(ANVIL_HANDSHAKE));
	return 0;
}

static int
service_process_write_anvil_kill(int fd, struct service_process *process)
{
	const char *data;

	data = t_strdup_printf("KILL\t%s\n", dec2str(process->pid));
	if (write(fd, data, strlen(data)) < 0) {
		if (errno != EAGAIN)
			e_error(process->service->event,
				"write(anvil process) failed: %m");
		return -1;
	}
	return 0;
}

void service_anvil_monitor_start(struct service_list *service_list)
{
	struct service *service;

	if (service_anvil_global->process_count == 0)
		service_list_anvil_discard_input(service_anvil_global);
	else {
		service = service_lookup_type(service_list, SERVICE_TYPE_ANVIL);
		(void)service_process_create(service, -1, NULL);
	}
}

void service_anvil_process_created(struct service_process *process)
{
	struct service_anvil_global *anvil = service_anvil_global;
	const char *error;

	service_anvil_global->pid = process->pid;
	service_anvil_global->uid = process->uid;
	service_anvil_global->process_count++;
	service_list_anvil_discard_input_stop(anvil);

	if (anvil_send_handshake(anvil->blocking_fd[1], &error) < 0 ||
	    anvil_send_handshake(anvil->nonblocking_fd[1], &error) < 0)
		e_error(process->service->event, "%s", error);
}

void service_anvil_process_destroyed(struct service_process *process)
{
	i_assert(service_anvil_global->process_count > 0);
	if (--service_anvil_global->process_count == 0)
		service_list_anvil_discard_input(service_anvil_global);

	if (service_anvil_global->pid == process->pid)
		service_anvil_global->pid = 0;
	service_anvil_global->restarted = TRUE;
}

void service_anvil_send_log_fd(struct service_list *service_list)
{
	ssize_t ret;
	char b = 0;

	if (service_anvil_global->process_count == 0)
		return;

	ret = fd_send(service_anvil_global->log_fdpass_fd[1],
		      service_list->anvil->log_fd[1], &b, 1);
	if (ret < 0)
		i_error("fd_send(anvil log fd) failed: %m");
	else if (ret == 0)
		i_error("fd_send(anvil log fd) failed: disconnected");
}

void service_anvil_global_init(void)
{
	struct service_anvil_global *anvil;

	anvil = i_new(struct service_anvil_global, 1);
	if (pipe(anvil->status_fd) < 0)
		i_fatal("pipe() failed: %m");
	if (pipe(anvil->blocking_fd) < 0)
		i_fatal("pipe() failed: %m");
	if (pipe(anvil->nonblocking_fd) < 0)
		i_fatal("pipe() failed: %m");
	if (socketpair(AF_UNIX, SOCK_STREAM, 0, anvil->log_fdpass_fd) < 0)
		i_fatal("socketpair() failed: %m");
	fd_set_nonblock(anvil->status_fd[0], TRUE);
	fd_set_nonblock(anvil->status_fd[1], TRUE);
	fd_set_nonblock(anvil->nonblocking_fd[1], TRUE);

	fd_close_on_exec(anvil->status_fd[0], TRUE);
	fd_close_on_exec(anvil->status_fd[1], TRUE);
	fd_close_on_exec(anvil->blocking_fd[0], TRUE);
	fd_close_on_exec(anvil->blocking_fd[1], TRUE);
	fd_close_on_exec(anvil->nonblocking_fd[0], TRUE);
	fd_close_on_exec(anvil->nonblocking_fd[1], TRUE);
	fd_close_on_exec(anvil->log_fdpass_fd[0], TRUE);
	fd_close_on_exec(anvil->log_fdpass_fd[1], TRUE);

	anvil->kills =
		service_process_notify_init(anvil->nonblocking_fd[1],
					    service_process_write_anvil_kill);
	service_anvil_global = anvil;
}

void service_anvil_global_deinit(void)
{
	struct service_anvil_global *anvil = service_anvil_global;

	service_list_anvil_discard_input_stop(anvil);
	service_process_notify_deinit(&anvil->kills);
	if (close(anvil->log_fdpass_fd[0]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->log_fdpass_fd[1]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->blocking_fd[0]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->blocking_fd[1]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->nonblocking_fd[0]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->nonblocking_fd[1]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->status_fd[0]) < 0)
		i_error("close(anvil) failed: %m");
	if (close(anvil->status_fd[1]) < 0)
		i_error("close(anvil) failed: %m");
	i_free(anvil);

	service_anvil_global = NULL;
}
