/*
 * vdevadm settle — wait for vdev input settle / coldplug stamps
 */

#include "vdevadm.h"

#include "libvdev/vdev_ctrl.h"

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

struct settle_paths {
	char ctl[PATH_MAX];
	char rsp[PATH_MAX];
	char retriggered[PATH_MAX];
	char trigger_failed[PATH_MAX];
	char settle_pid[PATH_MAX];
};

static void settle_usage(const char *prog)
{
	fprintf(stderr,
			"Usage: %s settle [OPTIONS]\n"
			"\n"
			"Wait for vdev / udev event queue to drain.\n"
			"\n"
			"Default mode (eudev-compatible): send PING on %s.  vdevd holds\n"
			"the connection until its workqueue is idle.  --input waits for\n"
			"vdev_settle input sync instead.\n"
			"\n"
			"Options:\n"
			"  -t --timeout=SEC       Timeout in seconds (default: 60, 0 = infinite)\n"
			"      --stamp=PATH        Wait for stamp file (e.g. vdev.ready)\n"
			"      --status            Query vdev_settle FIFO / status file only\n"
			"      --list              Print vdev_settle FIFO list (seen devices)\n"
			"      --list-pending      Print vdev_settle FIFO list-pending\n"
			"      --input             Wait for vdev_settle input enumeration\n"
			"      --input-worker      Run input settle worker (vdev-settle-sync.sh logic)\n"
			"      --stamp-early=PATH  With --input-worker: wait for stamp first\n"
			"      --vdevd-delay=SEC   With --input-worker: pause after stamp (default 0.5)\n"
			"      --settle-bin=PATH   vdev_settle helper (default: /lib/vdev/vdev_settle)\n"
			"      --stable-rounds=N   Stable polls before exit (default: 2)\n"
			"      --max-polls=N       Worker poll iterations (default: 30)\n"
			"  -h --help              Show help\n",
			prog, VDEV_CTRL_DEFAULT_PATH);
}

static int settle_fill_paths(struct vdevadm_ctx *ctx, struct settle_paths *p)
{
	if (snprintf(p->ctl, sizeof(p->ctl), "%s/vdev-settle.ctl",
				 ctx->state_dir) >= (int)sizeof(p->ctl) ||
		snprintf(p->rsp, sizeof(p->rsp), "%s/vdev-settle.rsp",
				 ctx->state_dir) >= (int)sizeof(p->rsp) ||
		snprintf(p->retriggered, sizeof(p->retriggered),
				 "%s/vdev-settle.retriggered",
				 ctx->state_dir) >= (int)sizeof(p->retriggered) ||
		snprintf(p->trigger_failed, sizeof(p->trigger_failed),
				 "%s/vdev-settle.trigger-failed",
				 ctx->state_dir) >= (int)sizeof(p->trigger_failed) ||
		snprintf(p->settle_pid, sizeof(p->settle_pid), "%s/vdev-settle.pid",
				 ctx->state_dir) >= (int)sizeof(p->settle_pid))
		return -ENAMETOOLONG;
	return 0;
}

static int settle_read_file_line(const char *path, char *buf, size_t len)
{
	FILE *fp;

	fp = fopen(path, "r");
	if (!fp)
		return -errno;
	if (!fgets(buf, len, fp)) {
		int err = ferror(fp) ? errno : ENOENT;

		fclose(fp);
		return -err;
	}
	fclose(fp);
	buf[strcspn(buf, "\r\n")] = '\0';
	return 0;
}

static void settle_sleep_ms(int ms)
{
	struct timespec ts;

	ts.tv_sec = ms / 1000;
	ts.tv_nsec = (long)(ms % 1000) * 1000000L;
	nanosleep(&ts, NULL);
}

static int settle_wait_udev_queue(struct vdevadm_ctx *ctx, int timeout_sec)
{
	struct vdev_ctrl *uctrl = NULL;
	int ping_timeout;
	int ping_rc;

	/*
	 * vdevd PING now blocks until the workqueue is idle, then closes the
	 * control connection.  Do not wait on /run/udev/queue afterwards:
	 * that path is often a directory via the /run/udev metadata symlink,
	 * so access() stays true and settle looks like a pure timeout.
	 */
	if (getuid() != 0)
		return 0;

	uctrl = vdev_ctrl_new(ctx->ctrl_socket);
	if (!uctrl)
		return 0;

	ping_timeout = timeout_sec > 0 ? timeout_sec : 60;
	if (ping_timeout < 5)
		ping_timeout = 5;
	ping_rc = vdev_ctrl_send_ping(uctrl, ping_timeout);
	vdev_ctrl_unref(uctrl);
	if (ping_rc != 0) {
		if (ping_rc == -ETIMEDOUT)
			return -ETIMEDOUT;
		if (ping_rc == -ENOENT || ping_rc == -ECONNREFUSED)
			return ping_rc;
		return 0;
	}
	return 0;
}

static int settle_wait_stamp(const char *path, int timeout_sec)
{
	int elapsed_ms = 0;
	const int step_ms = 200;

	while (timeout_sec == 0 || elapsed_ms < timeout_sec * 1000) {
		if (access(path, F_OK) == 0)
			return 0;
		settle_sleep_ms(step_ms);
		elapsed_ms += step_ms;
	}
	return -ETIMEDOUT;
}

static int settle_fifo_query(struct vdevadm_ctx *ctx, const char *command,
							 char *buf, size_t buflen)
{
	struct settle_paths paths;
	int rc;

	rc = settle_fill_paths(ctx, &paths);
	if (rc != 0)
		return rc;

	if (access(paths.ctl, F_OK) != 0 || access(paths.rsp, F_OK) != 0)
		return -ENOENT;

	return vdevadm_fifo_cmd(paths.ctl, paths.rsp, command, buf, buflen);
}

static int settle_count_sysfs_events(void)
{
	DIR *d = opendir("/sys/class/input");
	struct dirent *ent;
	int count = 0;

	if (!d)
		return 0;
	while ((ent = readdir(d)) != NULL) {
		if (strncmp(ent->d_name, "event", 5) == 0)
			count++;
	}
	closedir(d);
	return count;
}

static int settle_count_seen_lines(const char *text)
{
	const char *p = text;
	int n = 0;

	if (!text || !text[0])
		return 0;
	while (*p) {
		if (*p == '\n' && p > text && *(p - 1) != '\n')
			n++;
		p++;
	}
	if (text[strlen(text) - 1] != '\n')
		n++;
	return n;
}

static bool settle_event_in_list(const char *list_text, const char *event)
{
	const char *p = list_text;
	size_t elen = strlen(event);

	if (!list_text || !event[0])
		return false;

	while (*p) {
		const char *eol = strchr(p, '\n');
		size_t len = eol ? (size_t)(eol - p) : strlen(p);
		const char *tab = memchr(p, '\t', len);
		size_t field = tab ? (size_t)(tab - p) : len;

		if (field == elen && strncmp(p, event, elen) == 0)
			return true;
		if (!eol)
			break;
		p = eol + 1;
	}
	return false;
}

static int settle_input_ready(struct vdevadm_ctx *ctx, char *status_buf,
							  size_t status_len)
{
	char list[65536];
	int sysfs_n;
	int seen_n;
	int rc;

	rc = settle_fifo_query(ctx, "status", status_buf, status_len);
	if (rc < 0) {
		char file[PATH_MAX];

		if (snprintf(file, sizeof(file), "%s/vdev-settle.status",
					 ctx->state_dir) >= (int)sizeof(file))
			return rc;
		rc = settle_read_file_line(file, status_buf, status_len);
		if (rc != 0)
			return rc;
	}

	rc = settle_fifo_query(ctx, "list", list, sizeof(list));
	if (rc < 0) {
		char file[PATH_MAX];
		FILE *fp;

		if (snprintf(file, sizeof(file), "%s/vdev-settle.seen",
					 ctx->state_dir) >= (int)sizeof(file))
			return rc;
		fp = fopen(file, "r");
		if (!fp)
			return -errno;
		if (!fgets(list, sizeof(list), fp) && ferror(fp)) {
			int err = errno;

			fclose(fp);
			return -err;
		}
		fclose(fp);
	}

	sysfs_n = settle_count_sysfs_events();
	seen_n = settle_count_seen_lines(list);

	if (sysfs_n > 0 && seen_n >= sysfs_n)
		return 0;
	if (strncmp(status_buf, "ready", 5) == 0)
		return 0;
	return -EAGAIN;
}

static int settle_write_uevent_add(const char *event_name)
{
	char path[PATH_MAX];
	FILE *fp;

	if (snprintf(path, sizeof(path), "/sys/class/input/%s/uevent",
				 event_name) >= (int)sizeof(path))
		return -ENAMETOOLONG;
	fp = fopen(path, "we");
	if (!fp)
		return -errno;
	if (fprintf(fp, "add\n") < 0) {
		int err = errno;

		fclose(fp);
		return -err;
	}
	fclose(fp);
	return 0;
}

static bool settle_event_was_triggered(const struct settle_paths *paths,
									   const char *event)
{
	FILE *fp;
	char line[128];

	fp = fopen(paths->retriggered, "r");
	if (!fp)
		return false;
	while (fgets(line, sizeof(line), fp)) {
		line[strcspn(line, "\r\n")] = '\0';
		if (strcmp(line, event) == 0) {
			fclose(fp);
			return true;
		}
	}
	fclose(fp);
	return false;
}

static void settle_mark_triggered(const struct settle_paths *paths,
								  const char *event)
{
	FILE *fp;

	fp = fopen(paths->retriggered, "a");
	if (!fp)
		return;
	fprintf(fp, "%s\n", event);
	fclose(fp);
}

static void settle_log_trigger_failed(const struct settle_paths *paths,
									  const char *event)
{
	FILE *fp;
	time_t now;
	struct tm tm;
	char tbuf[64];

	fp = fopen(paths->trigger_failed, "a");
	if (!fp)
		return;
	now = time(NULL);
	localtime_r(&now, &tm);
	strftime(tbuf, sizeof(tbuf), "%Y-%m-%dT%H:%M:%S%z", &tm);
	fprintf(fp, "%s %s\n", tbuf, event);
	fclose(fp);
}

static int settle_trigger_event_add(struct vdevadm_ctx *ctx,
									const struct settle_paths *paths,
									const char *event)
{
	char syspath[PATH_MAX];
	int rc;

	(void)ctx;
	if (snprintf(syspath, sizeof(syspath), "/sys/class/input/%s", event) >=
		(int)sizeof(syspath))
		return -ENAMETOOLONG;
	if (access(syspath, F_OK) != 0)
		return -ENOENT;

	rc = settle_write_uevent_add(event);
	if (rc != 0) {
		settle_log_trigger_failed(paths, event);
		return rc;
	}
	if (!settle_event_was_triggered(paths, event))
		settle_mark_triggered(paths, event);
	return 0;
}

static int settle_foreach_input_event(
	int (*fn)(struct vdevadm_ctx *ctx, const struct settle_paths *paths,
			  const char *event, void *userdata),
	struct vdevadm_ctx *ctx, const struct settle_paths *paths, void *userdata)
{
	DIR *d = opendir("/sys/class/input");
	struct dirent *ent;
	int rc = 0;

	if (!d)
		return 0;
	while ((ent = readdir(d)) != NULL) {
		if (strncmp(ent->d_name, "event", 5) != 0)
			continue;
		rc = fn(ctx, paths, ent->d_name, userdata);
		if (rc != 0)
			break;
	}
	closedir(d);
	return rc;
}

static int settle_trigger_all_cb(struct vdevadm_ctx *ctx,
								 const struct settle_paths *paths,
								 const char *event, void *userdata)
{
	(void)userdata;
	settle_trigger_event_add(ctx, paths, event);
	return 0;
}

static int settle_stop_daemon(const struct settle_paths *paths)
{
	FILE *fp;
	long pid;

	fp = fopen(paths->settle_pid, "r");
	if (!fp)
		return 0;
	if (fscanf(fp, "%ld", &pid) != 1) {
		fclose(fp);
		return 0;
	}
	fclose(fp);
	if (pid > 0)
		kill((pid_t)pid, SIGTERM);
	unlink(paths->settle_pid);
	return 0;
}

static int settle_wait_fifos(const struct settle_paths *paths, int max_ms)
{
	int elapsed = 0;

	while (elapsed < max_ms) {
		if (access(paths->ctl, F_OK) == 0 && access(paths->rsp, F_OK) == 0)
			return 0;
		settle_sleep_ms(100);
		elapsed += 100;
	}
	return -ETIMEDOUT;
}

static int settle_start_daemon(const char *settle_bin, const char *state_dir)
{
	pid_t pid;

	if (!settle_bin || access(settle_bin, X_OK) != 0)
		return -ENOENT;

	pid = fork();
	if (pid < 0)
		return -errno;
	if (pid == 0) {
		int nullfd = open("/dev/null", O_RDWR);

		if (nullfd >= 0) {
			dup2(nullfd, STDIN_FILENO);
			dup2(nullfd, STDOUT_FILENO);
			dup2(nullfd, STDERR_FILENO);
			if (nullfd > STDERR_FILENO)
				close(nullfd);
		}
		execl(settle_bin, "vdev_settle", "--state-dir", state_dir, NULL);
		_exit(127);
	}
	return 0;
}

static int settle_worker_poll(struct vdevadm_ctx *ctx,
							  const struct settle_paths *paths,
							  int stable_rounds)
{
	char list[65536];
	int stable = 0;
	int pending;

	if (settle_fifo_query(ctx, "list", list, sizeof(list)) < 0)
		list[0] = '\0';

	pending = 0;
	{
		DIR *d = opendir("/sys/class/input");
		struct dirent *ent;

		if (d) {
			while ((ent = readdir(d)) != NULL) {
				if (strncmp(ent->d_name, "event", 5) != 0)
					continue;
				if (!settle_event_was_triggered(paths, ent->d_name))
					settle_trigger_event_add(ctx, paths, ent->d_name);
				if (!settle_event_in_list(list, ent->d_name))
					pending = 1;
			}
			closedir(d);
		}
	}

	if (pending == 0) {
		stable++;
		return stable >= stable_rounds ? 0 : 1;
	}
	return 1;
}

static int settle_run_input_worker(struct vdevadm_ctx *ctx, int timeout_sec,
								   const char *stamp_early, double vdevd_delay,
								   const char *settle_bin, int stable_rounds,
								   int max_polls)
{
	struct settle_paths paths;
	char status[128];
	int poll_ms = 500;
	int polls = 0;
	int rc;

	rc = settle_fill_paths(ctx, &paths);
	if (rc != 0)
		return 1;

	mkdir(ctx->state_dir, 0755);
	{
		FILE *fp = fopen(paths.retriggered, "w");

		if (fp)
			fclose(fp);
		fp = fopen(paths.trigger_failed, "w");
		if (fp)
			fclose(fp);
	}

	{
		int mrc = system("modprobe -q evdev >/dev/null 2>&1");

		(void)mrc;
	}

	if (stamp_early && stamp_early[0]) {
		if (settle_wait_stamp(stamp_early, timeout_sec > 0 ? timeout_sec : 25) !=
			0)
			fprintf(stderr,
					"vdevadm settle: warning: stamp %s not seen\n",
					stamp_early);
	}

	if (settle_wait_stamp(VDEV_CTRL_DEFAULT_PATH,
						  timeout_sec > 0 ? timeout_sec : 25) != 0)
		fprintf(stderr, "vdevadm settle: warning: %s not seen\n",
				VDEV_CTRL_DEFAULT_PATH);

	if (vdevd_delay > 0)
		settle_sleep_ms((int)(vdevd_delay * 1000.0));

	settle_foreach_input_event(settle_trigger_all_cb, ctx, &paths, NULL);
	settle_start_daemon(settle_bin, ctx->state_dir);
	settle_wait_fifos(&paths, 5000);

	while (polls < max_polls &&
		   (timeout_sec == 0 || polls * poll_ms < timeout_sec * 1000)) {
		rc = settle_worker_poll(ctx, &paths, stable_rounds);
		if (rc == 0)
			break;
		settle_sleep_ms(poll_ms);
		polls++;
	}

	settle_stop_daemon(&paths);

	if (settle_input_ready(ctx, status, sizeof(status)) == 0)
		return 0;

	fprintf(stderr, "vdevadm settle: input-worker timeout (%s)\n",
			status[0] ? status : "pending");
	return 1;
}

int vdevadm_settle_main(struct vdevadm_ctx *ctx, int argc, char *argv[])
{
	static const struct option opts[] = {
		{ "timeout", required_argument, NULL, 't' },
		{ "stamp", required_argument, NULL, 'S' },
		{ "stamp-early", required_argument, NULL, 'E' },
		{ "status", no_argument, NULL, 's' },
		{ "list", no_argument, NULL, 'l' },
		{ "list-pending", no_argument, NULL, 'L' },
		{ "input", no_argument, NULL, 'i' },
		{ "input-worker", no_argument, NULL, 'w' },
		{ "vdevd-delay", required_argument, NULL, 'd' },
		{ "settle-bin", required_argument, NULL, 'b' },
		{ "stable-rounds", required_argument, NULL, 'r' },
		{ "max-polls", required_argument, NULL, 'm' },
		{ "help", no_argument, NULL, 'h' },
		{}
	};
	int timeout_sec = 60;
	const char *stamp = NULL;
	const char *stamp_early = NULL;
	const char *settle_bin = NULL;
	double vdevd_delay = 0.5;
	int stable_rounds = 2;
	int max_polls = 30;
	bool status_only = false;
	bool list_only = false;
	bool list_pending = false;
	bool input_settle = false;
	bool input_worker = false;
	int c;
	int elapsed_ms = 0;
	const int step_ms = 500;
	char status[128];
	char list[65536];
	const char *env;

	while ((c = getopt_long(argc, argv, "t:S:E:slLiwrd:b:m:h", opts,
							NULL)) >= 0) {
		switch (c) {
		case 't':
			timeout_sec = atoi(optarg);
			break;
		case 'S':
			stamp = optarg;
			break;
		case 'E':
			stamp_early = optarg;
			break;
		case 's':
			status_only = true;
			break;
		case 'l':
			list_only = true;
			break;
		case 'L':
			list_pending = true;
			break;
		case 'i':
			input_settle = true;
			break;
		case 'w':
			input_worker = true;
			break;
		case 'd':
			vdevd_delay = atof(optarg);
			break;
		case 'b':
			settle_bin = optarg;
			break;
		case 'r':
			stable_rounds = atoi(optarg);
			if (stable_rounds < 1)
				stable_rounds = 1;
			break;
		case 'm':
			max_polls = atoi(optarg);
			if (max_polls < 1)
				max_polls = 1;
			break;
		case 'h':
			settle_usage(argv[0]);
			return 0;
		default:
			settle_usage(argv[0]);
			return 1;
		}
	}

	if (!settle_bin) {
		env = getenv("VDEV_HELPERS_DIR");
		if (env && env[0]) {
			static char binbuf[PATH_MAX];

			if (snprintf(binbuf, sizeof(binbuf), "%s/vdev_settle", env) <
				(int)sizeof(binbuf))
				settle_bin = binbuf;
		}
	}
	if (!settle_bin)
		settle_bin = "/lib/vdev/vdev_settle";

	env = getenv("VDEV_SETTLE_VDEVD_DELAY");
	if (env && env[0])
		vdevd_delay = atof(env);

	if (input_worker)
		return settle_run_input_worker(ctx, timeout_sec, stamp_early,
									   vdevd_delay, settle_bin, stable_rounds,
									   max_polls);

	if (stamp) {
		int rc = settle_wait_stamp(stamp, timeout_sec);

		if (rc != 0) {
			fprintf(stderr, "vdevadm settle: timeout waiting for %s\n",
					stamp);
			return 1;
		}
		return 0;
	}

	if (list_pending) {
		int rc = settle_fifo_query(ctx, "list-pending", list, sizeof(list));

		if (rc < 0) {
			fprintf(stderr, "vdevadm settle: list-pending: %s\n",
					strerror(-rc));
			return 1;
		}
		fputs(list, stdout);
		return 0;
	}

	if (list_only) {
		int rc = settle_fifo_query(ctx, "list", list, sizeof(list));

		if (rc < 0) {
			fprintf(stderr, "vdevadm settle: list: %s\n", strerror(-rc));
			return 1;
		}
		fputs(list, stdout);
		return 0;
	}

	if (status_only || input_settle) {
		if (settle_input_ready(ctx, status, sizeof(status)) == 0 ||
			status_only) {
			printf("%s\n", status[0] ? status : "(no status)");
			return status_only ? 0 : 0;
		}
	}

	if (!input_settle) {
		int rc = settle_wait_udev_queue(ctx, timeout_sec);

		if (rc != 0) {
			fprintf(stderr,
					"vdevadm settle: timeout waiting for vdevd workqueue to drain\n");
			return 1;
		}
		if (!status_only)
			return 0;
	}

	if (settle_input_ready(ctx, status, sizeof(status)) == 0) {
		printf("%s\n", status[0] ? status : "ready");
		return 0;
	}

	while (timeout_sec == 0 || elapsed_ms < timeout_sec * 1000) {
		int rc = settle_input_ready(ctx, status, sizeof(status));

		if (rc == 0)
			return 0;
		settle_sleep_ms(step_ms);
		elapsed_ms += step_ms;
	}

	fprintf(stderr, "vdevadm settle: timeout (%s)\n",
			status[0] ? status : "pending");
	return 1;
}
