/*
 * Tests for tfork
 *
 * Copyright Ralph Boehme <slow@samba.org> 2017
 *
 * 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 <talloc.h>
#include <tevent.h>
#include "system/filesys.h"
#include "system/wait.h"
#include "system/select.h"
#include "libcli/util/ntstatus.h"
#include "torture/torture.h"
#include "lib/util/data_blob.h"
#include "torture/local/proto.h"
#include "lib/util/tfork.h"
#include "lib/util/samba_util.h"
#include "lib/util/sys_rw.h"
#ifdef HAVE_PTHREAD
#include <pthread.h>
#endif

static bool test_tfork_simple(struct torture_context *tctx)
{
        pid_t parent = getpid();
        struct tfork *t = NULL;
        pid_t child;
        int ret;

        t = tfork_create();
        if (t == NULL) {
                torture_fail(tctx, "tfork failed\n");
                return false;
        }
        child = tfork_child_pid(t);
        if (child == 0) {
                torture_comment(tctx, "my parent pid is %d\n", parent);
                torture_assert(tctx, getpid() != parent, "tfork failed\n");
                _exit(0);
        }

        ret = tfork_destroy(&t);
        torture_assert(tctx, ret == 0, "tfork_destroy failed\n");

        return true;
}

static bool test_tfork_status(struct torture_context *tctx)
{
	struct tfork *t = NULL;
	int status;
	pid_t child;
	bool ok = true;

	t = tfork_create();
	if (t == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		_exit(123);
	}

	status = tfork_status(&t, true);
	if (status == -1) {
		torture_fail(tctx, "tfork_status failed\n");
	}

	torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done,
			    "tfork failed\n");
	torture_assert_goto(tctx, WEXITSTATUS(status) == 123, ok, done,
			    "tfork failed\n");

	torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status));

done:
	return ok;
}

static bool test_tfork_sigign(struct torture_context *tctx)
{
	struct tfork *t = NULL;
	struct sigaction act;
	pid_t child;
	int status;
	bool ok = true;
	int ret;

	act = (struct sigaction) {
		.sa_flags = SA_NOCLDWAIT,
		.sa_handler = SIG_IGN,
	};

	ret = sigaction(SIGCHLD, &act, NULL);
	torture_assert_goto(tctx, ret == 0, ok, done, "sigaction failed\n");

	t = tfork_create();
	if (t == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		sleep(1);
		_exit(123);
	}

	child = fork();
	if (child == -1) {
		torture_fail(tctx, "fork failed\n");
		return false;
	}
	if (child == 0) {
		_exit(0);
	}

	status = tfork_status(&t, true);
	if (status == -1) {
		torture_fail(tctx, "tfork_status failed\n");
	}

	torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done,
			    "tfork failed\n");
	torture_assert_goto(tctx, WEXITSTATUS(status) == 123, ok, done,
			    "tfork failed\n");
	torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status));

done:
	return ok;
}

static void sigchld_handler1(int signum, siginfo_t *si, void *u)
{
	pid_t pid;
	int status;

	if (signum != SIGCHLD) {
		abort();
	}

	pid = waitpid(si->si_pid, &status, 0);
	if (pid != si->si_pid) {
		abort();
	}
}

static bool test_tfork_sighandler(struct torture_context *tctx)
{
	struct tfork *t = NULL;
	struct sigaction act;
	struct sigaction oldact;
	pid_t child;
	int status;
	bool ok = true;
	int ret;

	act = (struct sigaction) {
		.sa_flags = SA_SIGINFO,
		.sa_sigaction = sigchld_handler1,
	};

	ret = sigaction(SIGCHLD, &act, &oldact);
	torture_assert_goto(tctx, ret == 0, ok, done, "sigaction failed\n");

	t = tfork_create();
	if (t == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		sleep(1);
		_exit(123);
	}

	child = fork();
	if (child == -1) {
		torture_fail(tctx, "fork failed\n");
		return false;
	}
	if (child == 0) {
		_exit(0);
	}

	status = tfork_status(&t, true);
	if (status == -1) {
		torture_fail(tctx, "tfork_status failed\n");
	}

	torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done,
			    "tfork failed\n");
	torture_assert_goto(tctx, WEXITSTATUS(status) == 123, ok, done,
			    "tfork failed\n");
	torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status));

done:
	sigaction(SIGCHLD, &oldact, NULL);

	return ok;
}

static bool test_tfork_process_hierarchy(struct torture_context *tctx)
{
	struct tfork *t = NULL;
	pid_t pid = getpid();
	pid_t child;
	pid_t pgid = getpgid(0);
	pid_t sid = getsid(0);
	char *procpath = NULL;
	int status;
	struct stat st;
	int ret;
	bool ok = true;

	procpath = talloc_asprintf(tctx, "/proc/%d/status", getpid());
	torture_assert_not_null(tctx, procpath, "talloc_asprintf failed\n");

	ret = stat(procpath, &st);
	TALLOC_FREE(procpath);
	if (ret != 0) {
		if (errno == ENOENT) {
			torture_skip(tctx, "/proc missing\n");
		}
		torture_fail(tctx, "stat failed\n");
	}

	t = tfork_create();
	if (t == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		char *cmd = NULL;
		FILE *fp = NULL;
		char line[64];
		char *p;
		pid_t ppid;

		torture_assert_goto(tctx, pgid == getpgid(0), ok, child_fail, "tfork failed\n");
		torture_assert_goto(tctx, sid == getsid(0), ok, child_fail, "tfork failed\n");

		cmd = talloc_asprintf(tctx, "cat /proc/%d/status | awk '/^PPid:/ {print $2}'", getppid());
		torture_assert_goto(tctx, cmd != NULL, ok, child_fail, "talloc_asprintf failed\n");

		fp = popen(cmd, "r");
		torture_assert_goto(tctx, fp != NULL, ok, child_fail, "popen failed\n");

		p = fgets(line, sizeof(line) - 1, fp);
		pclose(fp);
		torture_assert_goto(tctx, p != NULL, ok, child_fail, "popen failed\n");

		ret = sscanf(line, "%d", &ppid);
		torture_assert_goto(tctx, ret == 1, ok, child_fail, "sscanf failed\n");
		torture_assert_goto(tctx, ppid == pid, ok, child_fail, "process hierachy not rooted at caller\n");

		_exit(0);

	child_fail:
		_exit(1);
	}

	status = tfork_status(&t, true);
	if (status == -1) {
		torture_fail(tctx, "tfork_status failed\n");
	}

	torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done,
			    "tfork failed\n");
	torture_assert_goto(tctx, WEXITSTATUS(status) == 0, ok, done,
			    "tfork failed\n");
	torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status));

done:
	return ok;
}

static bool test_tfork_pipe(struct torture_context *tctx)
{
	struct tfork *t = NULL;
	int status;
	pid_t child;
	int up[2];
	int down[2];
	char c;
	int ret;
	bool ok = true;

	ret = pipe(&up[0]);
	torture_assert(tctx, ret == 0, "pipe failed\n");

	ret = pipe(&down[0]);
	torture_assert(tctx, ret == 0, "pipe failed\n");

	t = tfork_create();
	if (t == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		close(up[0]);
		close(down[1]);

		ret = read(down[0], &c, 1);
		torture_assert_goto(tctx, ret == 1, ok, child_fail, "read failed\n");
		torture_assert_goto(tctx, c == 1, ok, child_fail, "read failed\n");

		ret = write(up[1], &(char){2}, 1);
		torture_assert_goto(tctx, ret == 1, ok, child_fail, "write failed\n");

		_exit(0);

	child_fail:
		_exit(1);
	}

	close(up[1]);
	close(down[0]);

	ret = write(down[1], &(char){1}, 1);
	torture_assert(tctx, ret == 1, "read failed\n");

	ret = read(up[0], &c, 1);
	torture_assert(tctx, ret == 1, "read failed\n");
	torture_assert(tctx, c == 2, "read failed\n");

	status = tfork_status(&t, true);
	if (status == -1) {
		torture_fail(tctx, "tfork_status failed\n");
	}

	torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done,
			    "tfork failed\n");
	torture_assert_goto(tctx, WEXITSTATUS(status) == 0, ok, done,
			    "tfork failed\n");
done:
	return ok;
}

static bool test_tfork_twice(struct torture_context *tctx)
{
	struct tfork *t = NULL;
	int status;
	pid_t child;
	pid_t pid;
	int up[2];
	int ret;
	bool ok = true;

	ret = pipe(&up[0]);
	torture_assert(tctx, ret == 0, "pipe failed\n");

	t = tfork_create();
	if (t == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		close(up[0]);

		t = tfork_create();
		if (t == NULL) {
			torture_fail(tctx, "tfork failed\n");
			return false;
		}
		child = tfork_child_pid(t);
		if (child == 0) {
			sleep(1);
			pid = getpid();
			ret = write(up[1], &pid, sizeof(pid_t));
			torture_assert_goto(tctx, ret == sizeof(pid_t), ok, child_fail, "write failed\n");

			_exit(0);

		child_fail:
			_exit(1);
		}

		_exit(0);
	}

	close(up[1]);

	ret = read(up[0], &pid, sizeof(pid_t));
	torture_assert(tctx, ret == sizeof(pid_t), "read failed\n");

	status = tfork_status(&t, true);
	torture_assert_goto(tctx, status != -1, ok, done, "tfork_status failed\n");

	torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done,
			    "tfork failed\n");
	torture_assert_goto(tctx, WEXITSTATUS(status) == 0, ok, done,
			    "tfork failed\n");
done:
	return ok;
}

static void *tfork_thread(void *p)
{
	struct tfork *t = NULL;
	int status;
	pid_t child;
	uint64_t tid = (uint64_t)pthread_self();
	uint64_t *result = NULL;
	int up[2];
	ssize_t nread;
	int ret;

	ret = pipe(up);
	if (ret != 0) {
		pthread_exit(NULL);
	}

	t = tfork_create();
	if (t == NULL) {
		pthread_exit(NULL);
	}
	child = tfork_child_pid(t);
	if (child == 0) {
		ssize_t nwritten;

		close(up[0]);
		tid++;
		nwritten = sys_write(up[1], &tid, sizeof(uint64_t));
		if (nwritten != sizeof(uint64_t)) {
			_exit(1);
		}
		_exit(0);
	}
	close(up[1]);

	result = malloc(sizeof(uint64_t));
	if (result == NULL) {
		pthread_exit(NULL);
	}

	nread = sys_read(up[0], result, sizeof(uint64_t));
	if (nread != sizeof(uint64_t)) {
		pthread_exit(NULL);
	}

	status = tfork_status(&t, true);
	if (status == -1) {
		pthread_exit(NULL);
	}

	pthread_exit(result);
}

static bool test_tfork_threads(struct torture_context *tctx)
{
	int ret;
	bool ok = true;
	const int num_threads = 64;
	pthread_t threads[num_threads];
	sigset_t set;
	int i;

#ifndef HAVE_PTHREAD
	torture_skip(tctx, "no pthread support\n");
#endif

	/*
	 * Be nasty and taste for the worst case: ensure all threads start with
	 * SIGCHLD unblocked so we have the most fun with SIGCHLD being
	 * delivered to a random thread. :)
	 */
	sigemptyset(&set);
	sigaddset(&set, SIGCHLD);
#ifdef HAVE_PTHREAD
	ret = pthread_sigmask(SIG_UNBLOCK, &set, NULL);
#else
	ret = sigprocmask(SIG_UNBLOCK, &set, NULL);
#endif
	if (ret != 0) {
		return -1;
	}

	for (i = 0; i < num_threads; i++) {
		ret = pthread_create(&threads[i], NULL, tfork_thread, NULL);
		torture_assert_goto(tctx, ret == 0, ok, done,
				    "pthread_create failed\n");
	}

	for (i = 0; i < num_threads; i++) {
		void *p;
		uint64_t *result;

		ret = pthread_join(threads[i], &p);
		torture_assert_goto(tctx, ret == 0, ok, done,
				    "pthread_join failed\n");
		result = (uint64_t *)p;
		torture_assert_goto(tctx, *result == (uint64_t)threads[i] + 1,
				    ok, done, "thread failed\n");
		free(p);
	}

done:
	return ok;
}

static bool test_tfork_cmd_send(struct torture_context *tctx)
{
	struct tevent_context *ev = NULL;
	struct tevent_req *req = NULL;
	const char *cmd[2] = { NULL, NULL };
	bool ok = true;

	ev = tevent_context_init(tctx);
	torture_assert_goto(tctx, ev != NULL, ok, done,
			    "tevent_context_init failed\n");

	cmd[0] = talloc_asprintf(tctx, "%s/testprogs/blackbox/tfork.sh", SRCDIR);
	torture_assert_goto(tctx, cmd[0] != NULL, ok, done,
			    "talloc_asprintf failed\n");

	req = samba_runcmd_send(tctx, ev, timeval_zero(), 0, 0,
				cmd, "foo", NULL);
	torture_assert_goto(tctx, req != NULL, ok, done,
			    "samba_runcmd_send failed\n");

	ok = tevent_req_poll(req, ev);
	torture_assert_goto(tctx, ok, ok, done, "tevent_req_poll failed\n");

	torture_comment(tctx, "samba_runcmd_send test finished\n");

done:
	TALLOC_FREE(ev);

	return ok;
}

/*
 * Test to ensure that the event_fd becomes readable after
 * a tfork_process terminates.
 */
static bool test_tfork_event_file_handle(struct torture_context *tctx)
{
	bool ok = true;

	struct tfork *t1 = NULL;
	pid_t child1;
	struct pollfd poll1[] = { {-1, POLLIN} };

	struct tfork *t2 = NULL;
	pid_t child2;
	struct pollfd poll2[] = { {-1, POLLIN} };


	t1 = tfork_create();
	if (t1 == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}

	child1 = tfork_child_pid(t1);
	if (child1 == 0) {
		/*
		 * Parent process will kill this with a SIGTERM
		 * so 10 seconds should be plenty
		 */
		sleep(10);
		exit(1);
	}
	poll1[0].fd = tfork_event_fd(t1);

	t2 = tfork_create();
	if (t2 == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child2 = tfork_child_pid(t2);
	if (child2 == 0) {
		/*
		 * Parent process will kill this with a SIGTERM
		 * so 10 seconds should be plenty
		 */
		sleep(10);
		exit(2);
	}
	poll2[0].fd = tfork_event_fd(t2);

	/*
	 * Have forked two process and are in the master process
	 * Expect that both event_fds are unreadable
	 */
	poll(poll1, 1, 0);
	ok = !(poll1[0].revents & POLLIN);
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd readable\n");
	poll(poll2, 1, 0);
	ok = !(poll2[0].revents & POLLIN);
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd readable\n");

	/* Kill the first child process */
	kill(child1, SIGKILL);
	sleep(1);

	/*
	 * Have killed the first child, so expect it's event_fd to have gone
	 * readable.
	 *
	 */
	poll(poll1, 1, 0);
	ok = (poll1[0].revents & POLLIN);
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd not readable\n");
	poll(poll2, 1, 0);
	ok = !(poll2[0].revents & POLLIN);
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 2 event fd readable\n");

	/* Kill the secind child process */
	kill(child2, SIGKILL);
	sleep(1);
	/*
	 * Have killed the children, so expect their event_fd's to have gone
	 * readable.
	 *
	 */
	poll(poll1, 1, 0);
	ok = (poll1[0].revents & POLLIN);
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd not readable\n");
	poll(poll2, 1, 0);
	ok = (poll2[0].revents & POLLIN);
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 2 event fd not readable\n");

done:
	return ok;
}

/*
 * Test to ensure that the status calls behave as expected after a process
 * terminates.
 *
 * As the parent process owns the status fd's they get passed to all
 * subsequent children after a tfork.  So it's possible for another
 * child process to hold the status pipe open.
 *
 * The event fd needs to be left open by tfork, as a close in the status
 * code can cause issues in tevent code.
 *
 */
static bool test_tfork_status_handle(struct torture_context *tctx)
{
	bool ok = true;

	struct tfork *t1 = NULL;
	pid_t child1;

	struct tfork *t2 = NULL;
	pid_t child2;

	int status;
	int fd;
	int ev1_fd;
	int ev2_fd;


	t1 = tfork_create();
	if (t1 == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}

	child1 = tfork_child_pid(t1);
	if (child1 == 0) {
		/*
		 * Parent process will kill this with a SIGTERM
		 * so 10 seconds should be plenty
		 */
		sleep(10);
		exit(1);
	}
	ev1_fd = tfork_event_fd(t1);

	t2 = tfork_create();
	if (t2 == NULL) {
		torture_fail(tctx, "tfork failed\n");
		return false;
	}
	child2 = tfork_child_pid(t2);
	if (child2 == 0) {
		/*
		 * Parent process will kill this with a SIGTERM
		 * so 10 seconds should be plenty
		 */
		sleep(10);
		exit(2);
	}
	ev2_fd = tfork_event_fd(t2);

	/*
	 * Have forked two process and are in the master process
	 * expect that the status call will block, and hence return -1
	 * as the processes are still running
	 * The event fd's should be open.
	 */
	status = tfork_status(&t1, false);
	ok = status == -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork status available for non terminated "
			    "process 1\n");
	/* Is the event fd open? */
	fd = dup(ev1_fd);
	ok = fd != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd is not open");

	status = tfork_status(&t2, false);
	ok = status == -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork status avaiable for non terminated "
			    "process 2\n");
	/* Is the event fd open? */
	fd = dup(ev2_fd);
	ok = fd != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 2 event fd is not open");

	/*
	 * Kill the first process, it's status should be readable
	 * and it's event_fd should be open
	 * The second process's status should be unreadable.
	 */
	kill(child1, SIGTERM);
	sleep(1);
	status = tfork_status(&t1, false);
	ok = status != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork status for child 1 not available after "
			    "termination\n");
	/* Is the event fd open? */
	fd = dup(ev2_fd);
	ok = fd != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd is not open");

	status = tfork_status(&t2, false);
	ok = status == -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork status available for child 2 after "
			    "termination of child 1\n");

	/*
	 * Kill the second process, it's status should be readable
	 */
	kill(child2, SIGTERM);
	sleep(1);
	status = tfork_status(&t2, false);
	ok = status != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork status for child 2 not available after "
			    "termination\n");

	/* Check that the event fd's are still open */
	/* Is the event fd open? */
	fd = dup(ev1_fd);
	ok = fd != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 1 event fd is not open");
	/* Is the event fd open? */
	fd = dup(ev2_fd);
	ok = fd != -1;
	torture_assert_goto(tctx, ok, ok, done,
			    "tfork process 2 event fd is not open");

done:
	return ok;
}

struct torture_suite *torture_local_tfork(TALLOC_CTX *mem_ctx)
{
	struct torture_suite *suite =
		torture_suite_create(mem_ctx, "tfork");

	torture_suite_add_simple_test(suite,
				      "tfork_simple",
				      test_tfork_simple);

	torture_suite_add_simple_test(suite,
				      "tfork_status",
				      test_tfork_status);

	torture_suite_add_simple_test(suite,
				      "tfork_sigign",
				      test_tfork_sigign);

	torture_suite_add_simple_test(suite,
				      "tfork_sighandler",
				      test_tfork_sighandler);

	torture_suite_add_simple_test(suite,
				      "tfork_process_hierarchy",
				      test_tfork_process_hierarchy);

	torture_suite_add_simple_test(suite,
				      "tfork_pipe",
				      test_tfork_pipe);

	torture_suite_add_simple_test(suite,
				      "tfork_twice",
				      test_tfork_twice);

	torture_suite_add_simple_test(suite,
				      "tfork_threads",
				      test_tfork_threads);

	torture_suite_add_simple_test(suite,
				      "tfork_cmd_send",
				      test_tfork_cmd_send);

	torture_suite_add_simple_test(suite,
				      "tfork_event_file_handle",
				      test_tfork_event_file_handle);

	torture_suite_add_simple_test(suite,
				      "tfork_status_handle",
				      test_tfork_status_handle);

	return suite;
}
