// SPDX-License-Identifier: LGPL-2.1
/*
 * Copyright (C) 2009, 2010 Red Hat Inc, Steven Rostedt <srostedt@redhat.com>
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>

#include "tracefs.h"
#include "trace-cmd-private.h"
#include "trace-cmd-local.h"
#include "event-utils.h"

/* F_GETPIPE_SZ was introduced in 2.6.35, older systems don't have it */
#ifndef F_GETPIPE_SZ
# define F_GETPIPE_SZ	1032 /* The Linux number for the option */
#endif

#ifndef SPLICE_F_MOVE
# define SPLICE_F_MOVE		1
# define SPLICE_F_NONBLOCK	2
# define SPLICE_F_MORE		4
# define SPLICE_F_GIFT		8
#endif

#define POLL_TIMEOUT_MS		1000

struct tracecmd_recorder {
	struct tracefs_cpu *tcpu;
	int		fd;
	int		fd1;
	int		fd2;
	int		page_size;
	int		subbuf_size;
	int		cpu;
	int		stop;
	int		max;
	int		pages;
	int		count;
	unsigned	flags;
};

static int append_file(int size, int dst, int src)
{
	char buf[size];
	int r;

	lseek(src, 0, SEEK_SET);

	/* If there's an error, then we are pretty much screwed :-p */
	do {
		r = read(src, buf, size);
		if (r < 0)
			return r;
		r = write(dst, buf, r);
		if (r < 0)
			return r;
	} while (r);
	return 0;
}

void tracecmd_free_recorder(struct tracecmd_recorder *recorder)
{
	if (!recorder)
		return;

	if (recorder->max) {
		/* Need to put everything into fd1 */
		if (recorder->fd == recorder->fd1) {
			int ret;
			/*
			 * Crap, the older data is in fd2, and we need
			 * to append fd1 onto it, and then copy over to fd1
			 */
			ret = append_file(recorder->page_size,
					  recorder->fd2, recorder->fd1);
			/* Error on copying, then just keep fd1 */
			if (ret) {
				lseek(recorder->fd1, 0, SEEK_END);
				goto close;
			}
			lseek(recorder->fd1, 0, SEEK_SET);
			ftruncate(recorder->fd1, 0);
		}
		append_file(recorder->page_size, recorder->fd1, recorder->fd2);
	}
 close:
	tracefs_cpu_close(recorder->tcpu);

	if (recorder->fd1 >= 0)
		close(recorder->fd1);

	if (recorder->fd2 >= 0)
		close(recorder->fd2);

	free(recorder);
}

static int set_nonblock(struct tracecmd_recorder *recorder)
{
	return tracefs_cpu_stop(recorder->tcpu);
}

static struct tracecmd_recorder *
create_buffer_recorder_fd2(int fd, int fd2, int cpu, unsigned flags,
			   struct tracefs_instance *instance, int maxkb, int tfd)
{
	struct tracecmd_recorder *recorder;
	bool nonblock = false;

	recorder = malloc(sizeof(*recorder));
	if (!recorder)
		return NULL;

	recorder->flags = flags;

	recorder->page_size = getpagesize();
	if (maxkb) {
		int kb_per_page = recorder->page_size >> 10;

		if (!kb_per_page)
			kb_per_page = 1;
		recorder->max = maxkb / kb_per_page;
		/* keep max half */
		recorder->max >>= 1;
		if (!recorder->max)
			recorder->max = 1;
	} else
		recorder->max = 0;

	recorder->count = 0;
	recorder->pages = 0;

	/* fd always points to what to write to */
	recorder->fd = fd;
	recorder->fd1 = fd;
	recorder->fd2 = fd2;

	if (recorder->flags & TRACECMD_RECORD_POLL)
		nonblock = true;

	if (tfd >= 0)
		recorder->tcpu = tracefs_cpu_alloc_fd(tfd, recorder->page_size, nonblock);
	else
		recorder->tcpu = tracefs_cpu_open(instance, cpu, nonblock);

	if (!recorder->tcpu)
		goto out_free;

	recorder->subbuf_size = tracefs_cpu_read_size(recorder->tcpu);
	return recorder;

 out_free:
	tracecmd_free_recorder(recorder);
	return NULL;
}

struct tracecmd_recorder *
tracecmd_create_buffer_recorder_fd2(int fd, int fd2, int cpu, unsigned flags,
				    struct tracefs_instance *instance, int maxkb)
{
	return create_buffer_recorder_fd2(fd, fd2, cpu, flags, instance, maxkb, -1);
}

struct tracecmd_recorder *
tracecmd_create_buffer_recorder_fd(int fd, int cpu, unsigned flags, struct tracefs_instance *instance)
{
	return tracecmd_create_buffer_recorder_fd2(fd, -1, cpu, flags, instance, 0);
}

static struct tracecmd_recorder *
__tracecmd_create_buffer_recorder(const char *file, int cpu, unsigned flags,
				  struct tracefs_instance *instance, int tfd, int maxkb)
{
	struct tracecmd_recorder *recorder;
	int fd, fd2 = -1;
	char *file2;

	fd = open(file, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, 0644);
	if (fd < 0)
		return NULL;

	if (maxkb) {
		int len = strlen(file);

		file2 = malloc(len + 3);
		if (!file2)
			return NULL;

		sprintf(file2, "%s.1", file);

		fd2 = open(file2, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, 0644);
		if (fd2 < 0) {
			close(fd);
			unlink(file);
			free(file2);
			return NULL;
		}
	}

	recorder = create_buffer_recorder_fd2(fd, fd2, cpu, flags, instance, maxkb, tfd);
	if (!recorder) {
		close(fd);
		unlink(file);
		if (fd2 != -1)
			close(fd2);
	}

	if (fd2 != -1) {
		/* Unlink file2, we need to add everything to file at the end */
		unlink(file2);
		free(file2);
	}

	return recorder;
}

struct tracecmd_recorder *
tracecmd_create_buffer_recorder_maxkb(const char *file, int cpu, unsigned flags,
				      struct tracefs_instance *instance, int maxkb)
{
	struct tracecmd_recorder *recorder = NULL;
	char *file2;
	int len;
	int fd;
	int fd2;

	if (!maxkb)
		return tracecmd_create_buffer_recorder(file, cpu, flags, instance);

	len = strlen(file);
	file2 = malloc(len + 3);
	if (!file2)
		return NULL;

	sprintf(file2, "%s.1", file);

	fd = open(file, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, 0644);
	if (fd < 0)
		goto out;

	fd2 = open(file2, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, 0644);
	if (fd2 < 0)
		goto err;

	recorder = tracecmd_create_buffer_recorder_fd2(fd, fd2, cpu, flags, instance, maxkb);
	if (!recorder)
		goto err2;
 out:
	/* Unlink file2, we need to add everything to file at the end */
	unlink(file2);
	free(file2);

	return recorder;
 err2:
	close(fd2);
 err:
	close(fd);
	unlink(file);
	goto out;
}

struct tracecmd_recorder *
tracecmd_create_buffer_recorder(const char *file, int cpu, unsigned flags,
				struct tracefs_instance *instance)
{
	return __tracecmd_create_buffer_recorder(file, cpu, flags, instance, -1, 0);
}

/**
 * tracecmd_create_recorder_virt - Create a recorder reading tracing data
 * from the trace_fd file descriptor instead of from the local tracefs
 * @file: output filename where tracing data will be written
 * @cpu: which CPU is being traced
 * @flags: flags configuring the recorder (see TRACECMD_RECORDER_* enums)
 * @trace_fd: file descriptor from where tracing data will be read
 */
struct tracecmd_recorder *
tracecmd_create_recorder_virt(const char *file, int cpu, unsigned flags,
			      int trace_fd, int maxkb)
{
	return __tracecmd_create_buffer_recorder(file, cpu, flags, NULL, trace_fd, maxkb);
}

struct tracecmd_recorder *tracecmd_create_recorder_fd(int fd, int cpu, unsigned flags)
{
	return tracecmd_create_buffer_recorder_fd(fd, cpu, flags, NULL);
}

struct tracecmd_recorder *tracecmd_create_recorder(const char *file, int cpu, unsigned flags)
{
	return tracecmd_create_buffer_recorder(file, cpu, flags, NULL);
}

struct tracecmd_recorder *
tracecmd_create_recorder_maxkb(const char *file, int cpu, unsigned flags, int maxkb)
{
	return tracecmd_create_buffer_recorder_maxkb(file, cpu, flags, NULL, maxkb);
}

static inline void update_fd(struct tracecmd_recorder *recorder, int size)
{
	int fd;

	if (!recorder->max)
		return;

	recorder->count += size;

	if (recorder->count >= recorder->page_size) {
		recorder->pages += recorder->count / recorder->page_size;
		recorder->count = 0;
	}

	if (recorder->pages < recorder->max)
		return;

	recorder->pages = 0;

	fd = recorder->fd;

	/* Swap fd to next file. */
	if (fd == recorder->fd1)
		fd = recorder->fd2;
	else
		fd = recorder->fd1;

	/* Zero out the new file we are writing to */
	lseek(fd, 0, SEEK_SET);
	ftruncate(fd, 0);

	recorder->fd = fd;
}

/*
 * Returns -1 on error.
 *          or bytes of data read.
 */
static long read_data(struct tracecmd_recorder *recorder)
{
	bool nonblock = recorder->stop;
	char buf[recorder->subbuf_size];
	long left;
	long r, w;

	r = tracefs_cpu_read(recorder->tcpu, buf, nonblock);
	if (r < 0)
		return r;

	left = r;
	do {
		w = write(recorder->fd, buf + (r - left), left);
		if (w > 0) {
			left -= w;
			update_fd(recorder, w);
		}
	} while (w >= 0 && left);

	if (w < 0)
		r = w;

	return r;
}

/*
 * Returns -1 on error.
 *          or bytes of data read.
 */
static long direct_splice_data(struct tracecmd_recorder *recorder)
{
	bool nonblock = recorder->stop;
	return tracefs_cpu_pipe(recorder->tcpu, recorder->fd, nonblock);
}

static long move_data(struct tracecmd_recorder *recorder)
{
	bool nonblock = recorder->stop;
	long ret;

	if (recorder->flags & TRACECMD_RECORD_NOSPLICE)
		return read_data(recorder);

	if (recorder->flags & TRACECMD_RECORD_NOBRASS)
		return direct_splice_data(recorder);

	ret = tracefs_cpu_write(recorder->tcpu, recorder->fd, nonblock);
	if (ret > 0)
		update_fd(recorder, ret);
	return ret;
}

long tracecmd_flush_recording(struct tracecmd_recorder *recorder, bool finish)
{
	char buf[recorder->subbuf_size];
	long total = 0;
	long wrote = 0;
	long ret;

	if (!recorder)
		return 0;

	if (!finish)
		return tracefs_cpu_flush_write(recorder->tcpu, recorder->fd);

	set_nonblock(recorder);

	do {
		ret = tracefs_cpu_flush_write(recorder->tcpu, recorder->fd);
		if (ret > 0)
			wrote += ret;
	} while (ret > 0);

	/* Make sure we finish off with a page size boundary */
	wrote &= recorder->subbuf_size - 1;
	if (wrote) {
		memset(buf, 0, recorder->subbuf_size);
		write(recorder->fd, buf, recorder->subbuf_size - wrote);
		total += recorder->subbuf_size;
	}

	return total;
}

int tracecmd_start_recording(struct tracecmd_recorder *recorder, unsigned long sleep)
{
	struct timespec req = {
		.tv_sec = sleep / 1000000,
		.tv_nsec = (sleep % 1000000) * 1000,
	};
	long read = 1;
	long ret;

	recorder->stop = 0;

	do {
		/* Only sleep if we did not read anything last time */
		if (!read && sleep)
			nanosleep(&req, NULL);

		read = 0;
		do {
			ret = move_data(recorder);
			if (ret < 0) {
				if (errno == EINTR)
					continue;
				if ((recorder->flags & TRACECMD_RECORD_POLL) &&
				    errno == EAGAIN)
					continue;
				return ret;
			}
			read += ret;
		} while (ret > 0 && !recorder->stop);
	} while (!recorder->stop);

	/* Flush out the rest */
	ret = tracecmd_flush_recording(recorder, true);

	if (ret < 0)
		return ret;

	return 0;
}

int tracecmd_stop_recording(struct tracecmd_recorder *recorder)
{
	if (!recorder)
		return -1;

	recorder->stop = 1;

	return set_nonblock(recorder);
}
