/*
 * Copyright (C) 2018-2022 Intel Corporation.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <linux/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <limits.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>

#define LOG_ELEMENT_SIZE        80
#define LOG_MSG_SIZE		480
#define DEFAULT_POLL_INTERVAL	100000
#define LOG_INCOMPLETE_WARNING	"WARNING: logs missing here! "\
				"Try reducing polling interval"

/* Count of /dev/acrn_hvlog_cur_xxx */
static int cur_cnt,last_cnt;
static unsigned long interval = DEFAULT_POLL_INTERVAL;

/* this is for log file */
#define LOG_FILE_SIZE	(1024*1024)
#define LOG_FILE_NUM 	4
static size_t hvlog_log_size = LOG_FILE_SIZE;
static unsigned short hvlog_log_num = LOG_FILE_NUM;

struct hvlog_file {
	const char *path;
	int fd;

	size_t left_space;
	unsigned short index;
	unsigned short num;
};

static struct hvlog_file cur_log = {
	.path = "/var/log/acrnlog/acrnlog_cur",
	.fd = -1,
	.left_space = 0,
	.index = ~0,
	.num = LOG_FILE_NUM
};

static struct hvlog_file last_log = {
	.path = "/var/log/acrnlog/acrnlog_last",
	.fd = -1,
	.left_space = 0,
	.index = ~0,
	.num = LOG_FILE_NUM
};

struct hvlog_msg {
	__u64 usec;		/* timestamp, from tsc reset in usec */
	int cpu;		/* which physical cpu output the log */
	int sev;		/* log severity level */
	__u64 seq;		/* sequence num, used to reorder logs */

	size_t len;		/* length of message raw string */
	char raw[0];		/* raw log string, end with '\0' */
};

/*
 * describe the hvlog device, eg., /dev/acrn_hvlog_*
 * Note: this is thread-unsafe!
 */
struct hvlog_dev {
	int fd;
	struct hvlog_msg *msg;	/* pointer to msg */

	int latched;		/* 1 if an sbuf element latched */
	char entry_latch[LOG_ELEMENT_SIZE];	/* latch for an sbuf element */
	struct hvlog_msg latched_msg;	/* latch for parsed msg */
};

size_t write_log_file(struct hvlog_file * log, const char *buf, size_t len);

static int get_dev_cnt(char *prefix)
{
	struct dirent *pdir;
	int cnt = 0;
	char *ret;
	DIR *dir;

	dir = opendir("/dev");
	if (!dir) {
		printf("Error opening /dev: %s\n", strerror(errno));
		return -1;
	}

	while ((pdir = readdir(dir)) != NULL) {
		ret = strstr(pdir->d_name, prefix);
		if (ret)
			cnt++;
	}

	closedir(dir);

	return cnt;
}

/*
 * The function read a complete msg from acrnlog dev.
 * read one more sbuf entry if read an entry doesn't end with '\0'
 * however, if the new entry contains a new msg - which means the ending char
 * is lost, it will be latched to be process next time.
 */
struct hvlog_msg *hvlog_read_dev(struct hvlog_dev *dev)
{
	char *p = NULL;
	int ret;
	size_t len = 0;
	struct hvlog_msg *msg[2];
	int msg_num;
	char warn_msg[LOG_MSG_SIZE] = {0};

	msg[0] = dev->msg;
	msg[1] = &dev->latched_msg;

	memset(msg[0], 0, sizeof(struct hvlog_msg) + LOG_MSG_SIZE);
	msg_num = 0;

	do {
		if (dev->latched) {
			/* handle the latched msg first */
			dev->latched = 0;
			memcpy(&msg[0]->raw[msg[0]->len], dev->entry_latch,
			       LOG_ELEMENT_SIZE);
			msg_num++;
			memcpy(msg[0], msg[1], sizeof(struct hvlog_msg));
		} else {
			ret =
			    read(dev->fd, &msg[0]->raw[msg[0]->len],
				 LOG_ELEMENT_SIZE);
			if (!ret)
				break;
			/* do we read a new meaasge?
			 * msg[0]->raw[msg[0]->len format: [%lluus][cpu=%d][sev=%d][seq=%llu]: */
			p = strstr(&msg[0]->raw[msg[0]->len], "][seq=");
			if (p) {
				p = p + strlen("][seq=");
				msg[msg_num]->seq = strtoull(p, NULL, 10);
				if ((errno == ERANGE && (msg[msg_num]->seq == ULLONG_MAX))
						|| (errno != 0 && msg[msg_num]->seq == 0)) {
					if (snprintf(warn_msg, LOG_MSG_SIZE, "\n\n\t%s[invalid seq]\n\n\n",
								LOG_INCOMPLETE_WARNING) >= LOG_MSG_SIZE) {
						printf("WARN: warning message is truncated\n");
					}
					write_log_file(&cur_log, warn_msg, strnlen(warn_msg, LOG_MSG_SIZE));
				} else {
					msg_num++;
					/* if we read another new msg, latch it */
					/* to process next time */
					if (msg_num > 1) {
						dev->latched = 1;
						memcpy(dev->entry_latch,
							   &msg[0]->raw[msg[0]->len],
							LOG_ELEMENT_SIZE);
						break;
					}
				}
			}
		}

		if (msg_num == 0) {
			/* if head of a message lost, continue to read */
			memset(msg[0], 0, sizeof(struct hvlog_msg)
			       + LOG_MSG_SIZE);
			continue;
		}

		len = strnlen(&msg[0]->raw[msg[0]->len], LOG_MSG_SIZE);
		msg[0]->len += len;
	} while (len == LOG_ELEMENT_SIZE &&
		 msg[0]->len < LOG_MSG_SIZE - LOG_ELEMENT_SIZE);

	if (!msg[0]->len)
		return NULL;

	msg[0]->raw[msg[0]->len] = '\n';
	msg[0]->raw[msg[0]->len + 1] = 0;
	msg[0]->len++;

	return msg[0];
}

struct hvlog_dev *hvlog_open_dev(const char *path)
{
	struct hvlog_dev *dev;

	dev = calloc(1, sizeof(struct hvlog_dev));
	if (!dev) {
		printf("%s %d\n", __FUNCTION__, __LINE__);
		goto open_dev;
	}

	dev->fd = open(path, O_RDONLY);
	if (dev->fd < 0) {
		printf("%s %d\n", __FUNCTION__, __LINE__);
		goto open_fd;
	}

	/* actual allocated size is 512B */
	dev->msg = calloc(1, sizeof(struct hvlog_msg) + LOG_MSG_SIZE);
	if (!dev->msg) {
		printf("%s %d\n", __FUNCTION__, __LINE__);
		goto alloc_msg;
	}

	return dev;

 open_fd:
	close(dev->fd);
 alloc_msg:
	free(dev);
 open_dev:
	return NULL;
}

void hvlog_close_dev(struct hvlog_dev *dev)
{
	if (!dev)
		return;

	if (dev->msg)
		free(dev->msg);
	if (dev->fd > 0)
		close(dev->fd);
	free(dev);
	dev = NULL;
}

/* this is for reading msg from hvlog devices array */
static struct hvlog_data {
	struct hvlog_dev *dev;
	struct hvlog_msg *msg;	/* clean it after use */
} *cur, *last;

/*
 * read the earliest msg from each dev, to hvlog_data[].msg
 * hvlog_data[] will be used for reordering
 */
static int hvlog_dev_read_msg(struct hvlog_data *data, int num_dev)
{
	int i, new_read;

	new_read = 0;
	for (i = 0; i < num_dev; i++) {
		if (data[i].msg)
			continue;
		if (!data[i].dev)
			continue;

		data[i].msg = hvlog_read_dev(data[i].dev);
		if (data[i].msg)
			new_read++;
	}

	return new_read;
}

static struct hvlog_msg *get_min_seq_msg(struct hvlog_data *data, int num_dev)
{
	int i, index_min = -1;
	__u64 min_seq = 0;
	struct hvlog_msg *msg;

	for (i = 0; i < num_dev; i++) {
		if (!data[i].msg)
			continue;

		if (index_min == -1) {
			index_min = i;
			min_seq = data[i].msg->seq;
			continue;
		}

		if (data[i].msg->seq > min_seq)
			continue;
		index_min = i;
		min_seq = data[i].msg->seq;
	}

	if (index_min == -1)
		return NULL;

	msg = data[index_min].msg;
	data[index_min].msg = NULL;

	return msg;
}

static int new_log_file(struct hvlog_file *log)
{
	char file_name[32] = { };

	if (log->fd >= 0) {
		if (!hvlog_log_size)
			return 0;
		close(log->fd);
		log->fd = -1;
	}

	if (snprintf(file_name, sizeof(file_name), "%s.%hu", log->path,
		 log->index + 1) >= sizeof(file_name)) {
		printf("WARN: log path is truncated\n");
	} else
		remove(file_name);

	log->fd = open(file_name, O_RDWR | O_CREAT | O_TRUNC, 0666);
	if (log->fd < 0) {
		perror(file_name);
		return -1;
	}

	log->left_space = hvlog_log_size;
	log->index++;
	if (snprintf(file_name, sizeof(file_name), "%s.%hu", log->path,
			log->index - hvlog_log_num) >= sizeof(file_name)) {
		printf("WARN: log path is truncated\n");
	} else
		remove(file_name);

	return 0;
}

size_t write_log_file(struct hvlog_file * log, const char *buf, size_t len)
{
	int ret;

	if (len >= log->left_space)
		if (new_log_file(log))
			return 0;

	ret = write(log->fd, buf, len);
	log->left_space -= ret;

	return ret;
}

static void *cur_read_func(void *arg)
{
	struct hvlog_msg *msg;
	__u64 last_seq = 0;
	char warn_msg[LOG_MSG_SIZE] = {0};

	while (1) {
		hvlog_dev_read_msg(cur, cur_cnt);
		msg = get_min_seq_msg(cur, cur_cnt);
		if (!msg) {
			usleep(interval);
			continue;
		}

		/* if msg->seq is not contineous, warn for logs missing */
		if (last_seq + 1 < msg->seq) {
			if (snprintf(warn_msg, LOG_MSG_SIZE,
				 "\n\n\t%s[%lu ms]\n\n\n",
				 LOG_INCOMPLETE_WARNING, interval) >= LOG_MSG_SIZE) {
				printf("WARN: warning message is truncated\n");
			}

			write_log_file(&cur_log, warn_msg, strnlen(warn_msg, LOG_MSG_SIZE));
		}

		last_seq = msg->seq;

		write_log_file(&cur_log, msg->raw, msg->len);
	}

	return NULL;
}

/* If dir *path does't exist, create a new one.
 * Otherwise, remove all the old acrnlog files in the dir.
 */
static int mk_dir(const char *path)
{
	char prefix[32] = "acrnlog_cur."; /* acrnlog file prefix */
	char acrnlog_file[64] = { };
	struct dirent *pdir;
	struct stat st;
	int index = 0;
	char *find;
	DIR *dir;

	if (stat(path, &st)) {
		if (mkdir(path, 0644))
			return -1;
	} else {
		/* Remove all the old acrnlogs */
		dir = opendir(path);
		if (!dir) {
			printf("Error opening directory %s. Error: %s\n",
				path, strerror(errno));
			return -1;
		}
		while ((pdir = readdir(dir)) != NULL) {
			find = strstr(pdir->d_name, prefix);
			if (!find)
				continue;

			if (snprintf(acrnlog_file, sizeof(acrnlog_file), "%s/%s%d",
					path, prefix, index++) >= sizeof(acrnlog_file)) {
				printf("WARN: acrnlog file path is truncated\n");
			} else
				remove(acrnlog_file);
		}
		closedir(dir);
	}

	return 0;
}

/* for user optinal args */
static const char optString[] = "s:n:t:h";

static void display_usage(void)
{
	printf("acrnlog - tool to collect ACRN hypervisor log\n"
	       "[Usage] acrnlog [-s size] [-n number] [-t interval] [-h]\n\n"
	       "[Options]\n"
	       "\t-h: print this message\n"
	       "\t-t: polling interval to collect logs, in ms\n"
	       "\t-s: size limitation for each log file, in MB.\n"
	       "\t    0 means no limitation.\n"
	       "\t-n: how many files you would like to keep on disk\n"
	       "[Output] capatured log files under /var/log/acrnlog/\n");
}

static int parse_opt(int argc, char *argv[])
{
	int opt, ret;

	while ((opt = getopt(argc, argv, optString)) != -1) {
		switch (opt) {
		case 's':
			ret = strtoll(optarg, NULL, 10);
			if ((errno == ERANGE && (ret == LLONG_MAX || ret == LLONG_MIN))
					|| (errno != 0 && ret == 0)) {
				printf("'-s' invalid parameter: %s\n", optarg);
				return -EINVAL;
			}

			hvlog_log_size = ret * 1024;
			break;
		case 'n':
			ret = strtol(optarg, NULL, 10);
			if ((errno == ERANGE && (ret == LONG_MAX || ret == LONG_MIN))
					|| (errno != 0 && ret == 0)) {
				printf("'-n' invalid parameter: %s\n", optarg);
				return -EINVAL;
			}
			if (ret > 3)
				hvlog_log_num = ret;
			break;
		case 't':
			ret = strtol(optarg, NULL, 10);
			if (ret <= 0 || ret >=1000) {
				printf("'-t' require integer between [1-999]\n");
				return -EINVAL;
			}
			interval = ret * 1000;
			printf("Polling interval is %u ms\n", ret);
			break;
		case 'h':
			display_usage();
			return -EINVAL;
		default:
			/* Undefined operation. */
			display_usage();
			return -EINVAL;
		}
	}
	return 0;
}

static pthread_t cur_thread;

int main(int argc, char *argv[])
{
	char name[32];
	int i, ret;
	int num_cur, num_last;
	struct hvlog_msg *msg;

	if (parse_opt(argc, argv))
		return -1;

	ret = mk_dir("/var/log/acrnlog");
	if (ret) {
		printf("Cannot create /var/log/acrnlog. Error: %s\n",
			strerror(errno));
		return ret;
	}

	cur_cnt = get_dev_cnt("acrn_hvlog_cur_");
	last_cnt = get_dev_cnt("acrn_hvlog_last_");

	if (cur_cnt == 0) {
		printf("Failed to find acrn hvlog devices, please check whether module acrn_hvlog is inserted\n");
		return -1;
	} else if (cur_cnt == -1)
		return -1;

	cur = calloc(cur_cnt, sizeof(struct hvlog_data));
	if (!cur) {
		printf("Failed to allocate buf for cur log buf\n");
		return -1;
	}

	if (last_cnt) {
		last = calloc(cur_cnt, sizeof(struct hvlog_data));
		if (!last) {
			printf("Failed to allocate buf for last log buf\n");
			free(cur);
			return -1;
		}
	}

	num_cur = 0;
	for (i = 0; i < cur_cnt; i++) {
		if (snprintf(name, sizeof(name), "/dev/acrn_hvlog_cur_%d", i) >= sizeof(name)) {
			printf("ERROR: cur hvlog path is truncated\n");
			return -1;
		}
		cur[i].dev = hvlog_open_dev(name);
		if (!cur[i].dev)
			perror(name);
		else
			num_cur++;
		cur[i].msg = NULL;
	}

	num_last = 0;
	if (last_cnt) {
		for (i = 0; i < cur_cnt; i++) {
			if (snprintf(name, sizeof(name), "/dev/acrn_hvlog_last_%d", i) >= sizeof(name)) {
				printf("ERROR: last hvlog path is truncated\n");
				return -1;
			}
			last[i].dev = hvlog_open_dev(name);
			if (!last[i].dev)
				perror(name);
			else
				num_last++;
			last[i].msg = NULL;
		}
	}

	printf("open cur:%d last:%d\n", num_cur, num_last);

	/* create thread to read cur log */
	if (num_cur) {
		ret = pthread_create(&cur_thread, NULL, cur_read_func, cur);
		if (ret) {
			printf("%s %d\n", __FUNCTION__, __LINE__);
			cur_thread = 0;
		}
	}

	if (num_last) {
		while (1) {
			hvlog_dev_read_msg(last, cur_cnt);
			msg = get_min_seq_msg(last, cur_cnt);
			if (!msg)
				break;
			write_log_file(&last_log, msg->raw, msg->len);
		}
	}

	if (cur_thread)
		pthread_join(cur_thread, NULL);

	for (i = 0; i < cur_cnt; i++) {
		hvlog_close_dev(cur[i].dev);
	}

	for (i = 0; i < last_cnt; i++) {
		hvlog_close_dev(last[i].dev);
	}

	free(cur);
	if (last_cnt)
		free(last);

	return 0;
}
