// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2017 Red Hat Inc. All Rights Reserved.
 */

/*
 * Fork N children, each child writes to and reads from its own region of the
 * same test file, and check if what it reads is what it writes. The test
 * region is determined by N * blksz. Write and read operation can be either
 * direct or buffered.
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <sys/file.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define DEF_BLKSZ 4096

int verbose = 0;

static void usage(const char *prog)
{
	fprintf(stderr, "Usage: %s [-Fhptrwv] [-b blksz] [-n nr_child] [-i iterations] [-o offset] <-f filename>\n", prog);
	fprintf(stderr, "\t-F\tPreallocate all blocks by writing them before test\n");
	fprintf(stderr, "\t-p\tPreallocate all blocks using fallocate(2) before test\n");
	fprintf(stderr, "\t-t\tTruncate test file to largest size before test\n");
	fprintf(stderr, "\t-r\tDo direct read\n");
	fprintf(stderr, "\t-w\tDo direct write\n");
	fprintf(stderr, "\t-v\tBe verbose\n");
	fprintf(stderr, "\t-h\tshow this help message\n");
	exit(EXIT_FAILURE);
}

static int cmpbuf(char *b1, char *b2, int bsize)
{
	int i;

	for (i = 0; i < bsize; i++) {
		if (b1[i] != b2[i]) {
			fprintf(stderr, "cmpbuf: offset %d: Expected: 0x%x,"
				" got 0x%x\n", i, b1[i], b2[i]);
			return 1;
		}
	}
	return 0;
}

static void kill_children(pid_t *pids, int nr_child)
{
	int i;
	pid_t pid;

	for (i = 0; i < nr_child; i++) {
		pid = pids[i];
		if (pid == 0)
			continue;
		kill(pid, SIGTERM);
	}
}

static int wait_children(pid_t *pids, int nr_child)
{
	int i, status, ret = 0;
	pid_t pid;

	for (i = 0; i < nr_child; i++) {
		pid = pids[i];
		if (pid == 0)
			continue;
		waitpid(pid, &status, 0);
		ret += WEXITSTATUS(status);
	}
	return ret;
}

static void dumpbuf(char *buf, int size, int blksz)
{
	int i;

	printf("dumping buffer content\n");
	for (i = 0; i < size; i++) {
		if (((i % blksz) == 0) || ((i % 64) == 0))
			putchar('\n');
		printf("%x", buf[i]);
	}
	putchar('\n');
}

static int run_test(const char *filename, int n_child, int blksz, off_t offset,
		    int nr_iter, int flag_rd, int flag_wr)
{
	char *buf_rd;
	char *buf_wr;
	off_t seekoff;
	int fd_rd, fd_wr;
	int i, ret;
	long page_size;

	seekoff = offset + blksz * n_child;

	page_size = sysconf(_SC_PAGESIZE);
	ret = posix_memalign((void **)&buf_rd, (size_t)page_size,
		blksz > page_size ? blksz : (size_t)page_size);
	if (ret) {
		fprintf(stderr, "posix_memalign(buf_rd, %d, %d) failed: %d\n",
			blksz, blksz, ret);
		exit(EXIT_FAILURE);
	}
	memset(buf_rd, 0, blksz);
	ret = posix_memalign((void **)&buf_wr, (size_t)page_size,
		blksz > page_size ? blksz : (size_t)page_size);
	if (ret) {
		fprintf(stderr, "posix_memalign(buf_wr, %d, %d) failed: %d\n",
			blksz, blksz, ret);
		exit(EXIT_FAILURE);
	}
	memset(buf_wr, 0, blksz);

	fd_rd = open(filename, flag_rd);
	if (fd_rd < 0) {
		perror("open readonly for read");
		exit(EXIT_FAILURE);
	}

	fd_wr = open(filename, flag_wr);
	if (fd_wr < 0) {
		perror("open writeonly for direct write");
		exit(EXIT_FAILURE);
	}

#define log(format, ...) 			\
	if (verbose) {				\
		printf("[%d:%d] ", n_child, i);	\
		printf(format, __VA_ARGS__);	\
	}


	/* seek, write, read and verify */
	for (i = 0; i < nr_iter; i++) {
		memset(buf_wr, i + 1, blksz);
		log("pwrite(fd_wr, %p, %d, %lld)\n", buf_wr, blksz,
		    (long long) seekoff);
		if (pwrite(fd_wr, buf_wr, blksz, seekoff) != blksz) {
			perror("direct write");
			exit(EXIT_FAILURE);
		}

		/* make sure buffer write hits disk before direct read */
		if (!(flag_wr & O_DIRECT)) {
			if (fsync(fd_wr) < 0) {
				perror("fsync(fd_wr)");
				exit(EXIT_FAILURE);
			}
		}

		log("pread(fd_rd, %p, %d, %lld)\n", buf_rd, blksz,
		    (long long) seekoff);
		if (pread(fd_rd, buf_rd, blksz, seekoff) != blksz) {
			perror("buffer read");
			exit(EXIT_FAILURE);
		}
		if (cmpbuf(buf_wr, buf_rd, blksz) != 0) {
			fprintf(stderr, "[%d:%d] FAIL - comparison failed, "
				"offset %d\n", n_child, i, (int)seekoff);
			if (verbose)
				dumpbuf(buf_rd, blksz, blksz);
			exit(EXIT_FAILURE);
		}
	}
	exit(EXIT_SUCCESS);
}

int main(int argc, char *argv[])
{
	int nr_iter = 1;
	int nr_child = 1;
	int blksz = DEF_BLKSZ;
	int fd, i, ret = 0;
	int flag_rd = O_RDONLY;
	int flag_wr = O_WRONLY;
	int do_trunc = 0;
	int pre_fill = 0;
	int pre_alloc = 0;
	pid_t pid;
	pid_t *pids;
	off_t offset = 0;
	char *filename = NULL;

	while ((i = getopt(argc, argv, "b:i:n:f:Fpo:tvrw")) != -1) {
		switch (i) {
		case 'b':
			if ((blksz = atoi(optarg)) <= 0) {
				fprintf(stderr, "blksz must be > 0\n");
				exit(EXIT_FAILURE);
			}
			if (blksz % 512 != 0) {
				fprintf(stderr, "blksz must be multiple of 512\n");
				exit(EXIT_FAILURE);
			}
			break;
		case 'i':
			if ((nr_iter = atoi(optarg)) <= 0) {
				fprintf(stderr, "iterations must be > 0\n");
				exit(EXIT_FAILURE);
			}
			break;
		case 'n':
			if ((nr_child = atoi(optarg)) <= 0) {
				fprintf(stderr, "no of children must be > 0\n");
				exit(EXIT_FAILURE);
			}
			break;
		case 'f':
			filename = optarg;
			break;
		case 'F':
			pre_fill = 1;
			break;
		case 'p':
			pre_alloc = 1;
			break;
		case 'r':
			flag_rd |= O_DIRECT;
			break;
		case 'w':
			flag_wr |= O_DIRECT;
			break;
		case 't':
			do_trunc = 1;
			break;
		case 'o':
			if ((offset = atol(optarg)) < 0) {
				fprintf(stderr, "offset must be >= 0\n");
				exit(EXIT_FAILURE);
			}
			break;
		case 'v':
			verbose = 1;
			break;
		case 'h':	/* fall through */
		default:
			usage(argv[0]);
		}
	}

	if (filename == NULL)
		usage(argv[0]);
	if (pre_fill && pre_alloc) {
		fprintf(stderr, "Error: -F and -p are both specified\n");
		exit(EXIT_FAILURE);
	}

	pids = malloc(nr_child * sizeof(pid_t));
	if (!pids) {
		fprintf(stderr, "failed to malloc memory for pids\n");
		exit(EXIT_FAILURE);
	}
	memset(pids, 0, nr_child * sizeof(pid_t));

	/* create & truncate testfile first */
	fd = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0600);
	if (fd < 0) {
		perror("create & truncate testfile");
		free(pids);
		exit(EXIT_FAILURE);
	}
	if (do_trunc && (ftruncate(fd, blksz * nr_child) < 0)) {
		perror("ftruncate failed");
		free(pids);
		exit(EXIT_FAILURE);
	}
	if (pre_fill) {
		char *buf;
		buf = malloc(blksz * nr_child);
		memset(buf, 's', blksz * nr_child);
		write(fd, buf, blksz * nr_child);
		free(buf);
	}
	if (pre_alloc) {
		fallocate(fd, 0, 0, blksz * nr_child);
	}
	fsync(fd);
	close(fd);

	/* fork workers */
	for (i = 0; i < nr_child; i++) {
		pid = fork();
		if (pid < 0) {
			perror("fork");
			kill_children(pids, nr_child);
			free(pids);
			exit(EXIT_FAILURE);
		} else if (pid == 0) {
			/* never returns */
			run_test(filename, i, blksz, offset, nr_iter,
				 flag_rd, flag_wr);
		} else {
			pids[i] = pid;
		}
	}

	ret = wait_children(pids, nr_child);
	free(pids);
	exit(ret);
}
