/*
 * Copyright (C) 2013-2021 Canonical, Ltd.
 * Copyright (C) 2022-2023 Colin Ian King.
 *
 * 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.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */
#include "stress-ng.h"

#define MIN_POLL_FDS	(1)
#define MAX_POLL_FDS	(8192)	/* Must be never larger than 65535 */

#if defined(HAVE_SYS_SELECT_H)
#include <sys/select.h>
#endif

#if defined(HAVE_POLL_H)
#include <poll.h>
#endif

static const stress_help_t help[] = {
	{ "P N", "poll N",	"start N workers exercising zero timeout polling" },
	{ NULL, "poll-fds N",	"use N file descriptors" },
	{ NULL,	"poll-ops N",	"stop after N poll bogo operations" },
	{ NULL,	NULL,		NULL }
};

static int stress_set_poll_fds(const char *opt)
{
	size_t max_fds;

        max_fds = (size_t)stress_get_uint32(opt);
        stress_check_range("poll-fds", (uint64_t)max_fds, MIN_POLL_FDS, MAX_POLL_FDS);
        return stress_set_setting("poll-fds", TYPE_ID_SIZE_T, &max_fds);
}

static const stress_opt_set_func_t opt_set_funcs[] = {
	{ OPT_poll_fds,		stress_set_poll_fds },
	{ 0,			NULL },
};

#if defined(HAVE_POLL_H)

#define MAX_PIPES	(5)

typedef struct {
	int fd[2];
} pipe_fds_t;

/*
 *  pipe_read()
 *	read a pipe with some verification and checking
 */
static ssize_t OPTIMIZE3 pipe_read(const stress_args_t *args, const int fd, const size_t n)
{
	const bool verify = !!(g_opt_flags & OPT_FLAGS_VERIFY);

	while (stress_continue_flag()) {
		ssize_t ret;
		uint16_t buf ALIGN64 = ~0;

		ret = read(fd, &buf, sizeof(buf));
		if (UNLIKELY(verify)) {
			if (UNLIKELY(ret < 0)) {
				if ((errno == EAGAIN) || (errno == EINTR))
					continue;
				pr_fail("%s: pipe read error detected, errno=%d (%s)\n",
					args->name, errno, strerror(errno));
				return ret;
			} else if (LIKELY(ret > 0)) {
				if (UNLIKELY(buf != (uint16_t)n)) {
					pr_fail("%s: pipe read error, "
						"expecting different data on "
						"pipe\n", args->name);
					return ret;
				}
			}
		}
		return ret;
	}
	return -1;
}

/*
 *  stress_poll()
 *	stress system by rapid polling system calls
 */
static int OPTIMIZE3 stress_poll(const stress_args_t *args)
{
	pid_t pid;
	int rc = EXIT_SUCCESS, parent_cpu;
	register size_t i;
	size_t max_fds = MAX_PIPES, max_rnd_fds;
	pipe_fds_t *pipe_fds;
	struct pollfd *poll_fds;
	int *rnd_fds_index;

	(void)stress_get_setting("poll-fds", &max_fds);

	pipe_fds = calloc(max_fds, sizeof(*pipe_fds));
	if (!pipe_fds) {
		pr_inf_skip("%s: out of memory allocating %zd pipe file descriptors, "
			"skipping stressor\n", args->name, max_fds);
		return EXIT_NO_RESOURCE;
	}
	poll_fds = calloc(max_fds, sizeof(*poll_fds));
	if (!poll_fds) {
		pr_inf("%s: out of memory allocating %zd poll file descriptors, "
			"skipping stressor\n", args->name, max_fds);
		free(pipe_fds);
		return EXIT_NO_RESOURCE;
	}

	max_rnd_fds = max_fds;
	if (max_rnd_fds < MAX_POLL_FDS)
		max_rnd_fds = MAX_POLL_FDS - (MAX_POLL_FDS % max_fds);

	rnd_fds_index = calloc(max_rnd_fds, sizeof(*rnd_fds_index));
	if (!rnd_fds_index) {
		pr_inf("%s: out of memory allocating %zd randomized poll indices, "
			"skipping stressor\n", args->name, max_fds);
		free(poll_fds);
		free(pipe_fds);
		return EXIT_NO_RESOURCE;
	}

	/* randomize: shuffle rnd_fds_index */
	for (i = 0; i < max_rnd_fds; i++) {
		rnd_fds_index[i] = i % max_fds;
	}
	for (i = 0; i < max_rnd_fds; i++) {
		register size_t tmp, j = stress_mwc32modn(max_rnd_fds);

		tmp = rnd_fds_index[i];
		rnd_fds_index[i] = rnd_fds_index[j];
		rnd_fds_index[j] = tmp;
	}

	for (i = 0; i < max_fds; i++) {
		if (pipe(pipe_fds[i].fd) < 0) {
			size_t j;

			pr_fail("%s: pipe failed, errno=%d (%s)\n",
				args->name, errno, strerror(errno));

			for (j = 0; j < i; j++) {
				(void)close(pipe_fds[i].fd[0]);
				(void)close(pipe_fds[i].fd[1]);
			}
			free(rnd_fds_index);
			free(poll_fds);
			free(pipe_fds);
			return EXIT_NO_RESOURCE;
		}
	}

	stress_set_proc_state(args->name, STRESS_STATE_RUN);
again:
	parent_cpu = stress_get_cpu();
	pid = fork();
	if (pid < 0) {
		if (stress_redo_fork(errno))
			goto again;
		if (!stress_continue(args))
			goto tidy;
		pr_fail("%s: fork failed, errno=%d (%s)\n",
			args->name, errno, strerror(errno));
		rc = EXIT_FAILURE;
		goto tidy;
	} else if (pid == 0) {
		/* Child writer */

		(void)stress_change_cpu(args, parent_cpu);
		stress_parent_died_alarm();
		(void)sched_settings_apply(true);

		for (i = 0; i < max_fds; i++)
			(void)close(pipe_fds[i].fd[0]);

		i = 0;
		do {
			register size_t j = rnd_fds_index[i];
			uint16_t buf ALIGN64 = (uint16_t)j;
			register ssize_t ret;

			i++;
			if (UNLIKELY(i >= max_rnd_fds))
				i = 0;

			/* Write on a randomly chosen pipe */
			ret = write(pipe_fds[j].fd[1], &buf, sizeof(buf));
			if (UNLIKELY(ret < (ssize_t)sizeof(buf))) {
				if ((errno == EAGAIN) || (errno == EINTR))
					continue;
				pr_fail("%s: write failed, errno=%d (%s)\n",
					args->name, errno, strerror(errno));
				goto abort;
			}
		 } while (stress_continue(args));
abort:
		for (i = 0; i < max_fds; i++)
			(void)close(pipe_fds[i].fd[1]);
		exit(EXIT_SUCCESS);
	} else {
		/* Parent read */

		int status;
#if defined(HAVE_SYS_SELECT_H) &&	\
    defined(HAVE_SELECT)
		int maxfd = 0;
		fd_set rfds;
#endif
#if defined(HAVE_PPOLL) ||	\
    defined(HAVE_PSELECT)
		struct timespec ts;
		sigset_t sigmask;
#endif

		for (i = 0; i < max_fds; i++) {
			poll_fds[i].fd = pipe_fds[i].fd[0];
			poll_fds[i].events = POLLIN;
			poll_fds[i].revents = 0;
		}

		do {
#if defined(HAVE_SYS_SELECT_H) &&	\
    defined(HAVE_SELECT)
			struct timeval tv;
#endif
			int ret;

			if (!stress_continue(args))
				break;

			/* stress out poll */
			ret = poll(poll_fds, max_fds, 1);
			if ((g_opt_flags & OPT_FLAGS_VERIFY) &&
			    (ret < 0) && (errno != EINTR)) {
				pr_fail("%s: poll failed, errno=%d (%s)\n",
					args->name, errno, strerror(errno));
			}
			if (LIKELY(ret > 0)) {
				for (i = 0; i < max_fds; i++) {
					if (poll_fds[i].revents == POLLIN) {
						if (UNLIKELY(pipe_read(args, poll_fds[i].fd, i) < 0))
							break;
					}
				}
				stress_bogo_inc(args);
			}

			if (!stress_continue(args))
				break;

#if defined(HAVE_PPOLL)
			/* stress out ppoll */

			ts.tv_sec = 0;
			ts.tv_nsec = 20000000;

			(void)sigemptyset(&sigmask);
			(void)sigaddset(&sigmask, SIGPIPE);

			ret = ppoll(poll_fds, max_fds, &ts, &sigmask);
			if ((g_opt_flags & OPT_FLAGS_VERIFY) &&
			    (ret < 0) && (errno != EINTR)) {
				pr_fail("%s: ppoll failed, errno=%d (%s)\n",
					args->name, errno, strerror(errno));
			}
			if (LIKELY(ret > 0)) {
				for (i = 0; i < max_fds; i++) {
					if (poll_fds[i].revents == POLLIN) {
						if (UNLIKELY(pipe_read(args, poll_fds[i].fd, i) < 0))
							break;
					}
				}
				stress_bogo_inc(args);
			}
			if (!stress_continue(args))
				break;

			/* Exercise illegal poll timeout */
			ts.tv_sec = 0;
			ts.tv_nsec = 1999999999;
			VOID_RET(int, ppoll(poll_fds, max_fds, &ts, &sigmask));
			if (!stress_continue(args))
				break;

#if defined(RLIMIT_NOFILE)
			/*
			 *  Exercise ppoll with more fds than rlimit soft
			 *  limit. This should fail with EINVAL on Linux.
			 */
			{
				struct rlimit old_rlim, new_rlim;

				if (LIKELY(getrlimit(RLIMIT_NOFILE, &old_rlim) == 0)) {
					new_rlim.rlim_cur = max_fds - 1;
					new_rlim.rlim_max = old_rlim.rlim_max;

					if (LIKELY(setrlimit(RLIMIT_NOFILE, &new_rlim) == 0)) {
						ts.tv_sec = 0;
						ts.tv_nsec = 0;
						VOID_RET(int, ppoll(poll_fds, max_fds, &ts, &sigmask));

						(void)setrlimit(RLIMIT_NOFILE, &old_rlim);
						if (!stress_continue(args))
							break;
					}
				}
			}
#endif

#endif

#if defined(HAVE_SYS_SELECT_H) &&	\
    defined(HAVE_SELECT)
			/* stress out select */
			FD_ZERO(&rfds);
			for (i = 0; i < max_fds; i++) {
				if (pipe_fds[i].fd[0] < FD_SETSIZE) {
					FD_SET(pipe_fds[i].fd[0], &rfds);
					if (pipe_fds[i].fd[0] > maxfd)
						maxfd = pipe_fds[i].fd[0];
				}
			}
			tv.tv_sec = 0;
			tv.tv_usec = 20000;
			ret = select(maxfd + 1, &rfds, NULL, NULL, &tv);
			if ((g_opt_flags & OPT_FLAGS_VERIFY) &&
			    (ret < 0) && (errno != EINTR)) {
				pr_fail("%s: select failed, errno=%d (%s)\n",
					args->name, errno, strerror(errno));
			}
			if (LIKELY(ret > 0)) {
				for (i = 0; i < max_fds; i++) {
					if ((pipe_fds[i].fd[0] < FD_SETSIZE) &&
					     FD_ISSET(pipe_fds[i].fd[0], &rfds)) {
						if (UNLIKELY(pipe_read(args, pipe_fds[i].fd[0], i) < 0))
							break;
					}
				}
				stress_bogo_inc(args);
			}
			if (!stress_continue(args))
				break;
#endif

#if defined(HAVE_PSELECT)
			/* stress out pselect */
			ts.tv_sec = 0;
			ts.tv_nsec = 20000000;

			(void)sigemptyset(&sigmask);
			(void)sigaddset(&sigmask, SIGPIPE);

			FD_ZERO(&rfds);
			for (i = 0; i < max_fds; i++) {
				if (pipe_fds[i].fd[0] < FD_SETSIZE) {
					FD_SET(pipe_fds[i].fd[0], &rfds);
					if (pipe_fds[i].fd[0] > maxfd)
						maxfd = pipe_fds[i].fd[0];
				}
			}
			ret = pselect(maxfd + 1, &rfds, NULL, NULL, &ts, &sigmask);
			if ((g_opt_flags & OPT_FLAGS_VERIFY) &&
			    (ret < 0) && (errno != EINTR)) {
				pr_fail("%s: pselect failed, errno=%d (%s)\n",
					args->name, errno, strerror(errno));
			}
			if (LIKELY(ret > 0)) {
				for (i = 0; i < max_fds; i++) {
					if ((pipe_fds[i].fd[0] < FD_SETSIZE) &&
					    (FD_ISSET(pipe_fds[i].fd[0], &rfds))) {
						if (UNLIKELY(pipe_read(args, pipe_fds[i].fd[0], i) < 0))
							break;
					}
				}
				stress_bogo_inc(args);
			}
#endif
			/*
			 * stress zero sleep, this is like
			 * a select zero timeout
			 */
			(void)sleep(0);
		} while (stress_continue(args));

		(void)shim_kill(pid, SIGKILL);
		(void)shim_waitpid(pid, &status, 0);
	}

tidy:
	stress_set_proc_state(args->name, STRESS_STATE_DEINIT);

	for (i = 0; i < max_fds; i++) {
		(void)close(pipe_fds[i].fd[0]);
		(void)close(pipe_fds[i].fd[1]);
	}

	free(rnd_fds_index);
	free(poll_fds);
	free(pipe_fds);

	return rc;
}


stressor_info_t stress_poll_info = {
	.stressor = stress_poll,
	.class = CLASS_SCHEDULER | CLASS_OS,
	.opt_set_funcs = opt_set_funcs,
	.verify = VERIFY_OPTIONAL,
	.help = help
};
#else
stressor_info_t stress_poll_info = {
	.stressor = stress_unimplemented,
	.class = CLASS_SCHEDULER | CLASS_OS,
	.opt_set_funcs = opt_set_funcs,
	.verify = VERIFY_OPTIONAL,
	.help = help,
	.unimplemented_reason = "built without poll.h"
};
#endif
