/*
 * FUSE: Filesystem in Userspace
 * Copyright (C) 2026 Bernd Schubert <bsbernd.com>
 *
 * This program can be distributed under the terms of the GNU LGPLv2.
 * See the file COPYING.LIB.
 */

#define _GNU_SOURCE

#include "fuse_daemonize.h"
#include "fuse_daemonize_i.h"

#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#include <err.h>

/**
 * Status values for fuse_daemonize_success() and fuse_daemonize_fail()
 */
#define FUSE_DAEMONIZE_SUCCESS 0
#define FUSE_DAEMONIZE_FAILURE 1

/* Private/internal data  */
struct fuse_daemonize {
	unsigned int flags;
	int signal_pipe_wr;	/* write end for signaling parent */
	int death_pipe_rd;	/* read end, POLLHUP when parent dies */
	int stop_pipe_rd;	/* read end for stop signal */
	int stop_pipe_wr;	/* write end for stop signal */
	pthread_t watcher;
	bool watcher_started;
	_Atomic bool active;     /* daemonization in progress */
	_Atomic bool daemonized; /* daemonized successfully */
	_Atomic bool mounted;  /* fuse_session_mount() completed */
	_Atomic bool got_init; /* got FUSE_INIT */
};

/* Global daemonization object pointer */
static struct fuse_daemonize daemonize = {
	.signal_pipe_wr = -1,
	.death_pipe_rd = -1,
	.stop_pipe_rd = -1,
	.stop_pipe_wr = -1,
};

/* Watcher thread: polls for parent death or stop signal */
static void *parent_watcher_thread(void *arg)
{
	const struct fuse_daemonize *di = arg;
	struct pollfd pfd[2];

	pfd[0].fd = di->death_pipe_rd;
	pfd[0].events = POLLIN;
	pfd[1].fd = di->stop_pipe_rd;
	pfd[1].events = POLLIN;

	while (1) {
		int rc = poll(pfd, 2, -1);

		if (rc < 0)
			continue;

		/* Parent died - death pipe write end closed */
		if (pfd[0].revents & (POLLHUP | POLLERR))
			_exit(EXIT_FAILURE);

		/* Stop signal received */
		if (pfd[1].revents & POLLIN)
			break;
	}
	return NULL;
}

static int start_parent_watcher(struct fuse_daemonize *dm)
{
	int rc;

	rc = pthread_create(&dm->watcher, NULL, parent_watcher_thread,
			    dm);
	if (rc != 0) {
		fprintf(stderr, "fuse_daemonize: pthread_create: %s\n",
			strerror(rc));
		return -rc;
	}
	dm->watcher_started = true;
	return 0;
}

static void stop_parent_watcher(struct fuse_daemonize *dm)
{
	char byte = 0;

	if (dm && dm->watcher_started) {
		/* Signal watcher to stop */
		if (write(dm->stop_pipe_wr, &byte, 1) != 1)
			perror("fuse_daemonize: stop write");
		pthread_join(dm->watcher, NULL);
		dm->watcher_started = false;
	}
}

static int daemonize_child(struct fuse_daemonize *dm)
{
	int stop_pipe[2], err = 0;

	if (pipe(stop_pipe) == -1) {
		err = -errno;
		perror("fuse_daemonize_start: stop pipe");
		return err;
	}
	dm->stop_pipe_rd = stop_pipe[0];
	dm->stop_pipe_wr = stop_pipe[1];

	if (setsid() == -1) {
		err = -errno;
		perror("fuse_daemonize_start: setsid");
		goto err_close_stop;
	}

	/* Close stdin immediately */
	int nullfd = open("/dev/null", O_RDWR, 0);

	if (nullfd != -1) {
		(void)dup2(nullfd, 0);
		if (nullfd > 0)
			close(nullfd);
	}

	/* Start watcher thread to detect parent death */
	err = start_parent_watcher(dm);
	if (err)
		goto err_close_stop;

	dm->daemonized = true;
	return 0;

err_close_stop:
	close(dm->stop_pipe_rd);
	close(dm->stop_pipe_wr);
	return err;
}

/* Fork and daemonize. Returns 0 in child, never returns in parent. */
static int do_daemonize(struct fuse_daemonize *dm)
{
	int signal_pipe[2], death_pipe[2], err;

	if (pipe(signal_pipe) == -1) {
		err = -errno;
		perror("fuse_daemonize_start: signal pipe");
		return err;
	}

	if (pipe(death_pipe) == -1) {
		err = -errno;
		perror("fuse_daemonize_start: death pipe");
		close(signal_pipe[0]);
		close(signal_pipe[1]);
		return err;
	}

	switch (fork()) {
	case -1:
		err = -errno;
		perror("fuse_daemonize_start: fork");
		close(signal_pipe[0]);
		close(signal_pipe[1]);
		close(death_pipe[0]);
		close(death_pipe[1]);
		return err;

	case 0:
		/* Child: signal write end, death read end */
		close(signal_pipe[0]);
		close(death_pipe[1]);
		dm->signal_pipe_wr = signal_pipe[1];
		dm->death_pipe_rd = death_pipe[0];
		return daemonize_child(dm);

	default: {
		/* Parent: signal read end, death write end (kept open) */
		int status;
		ssize_t res;

		close(signal_pipe[1]);
		close(death_pipe[0]);

		res = read(signal_pipe[0], &status, sizeof(status));
		close(signal_pipe[0]);
		close(death_pipe[1]);

		if (res != sizeof(status))
			_exit(EXIT_FAILURE);
		_exit(status);
	}
	}
}

int fuse_daemonize_early_start(unsigned int flags)
{
	struct fuse_daemonize *dm = &daemonize;
	int err = 0;

	if (dm->active)
		return 0; /* already called, no need for another fork */

	dm->flags = flags;
	dm->signal_pipe_wr = -1;
	dm->death_pipe_rd = -1;
	dm->stop_pipe_rd = -1;
	dm->stop_pipe_wr = -1;

	if (!(flags & FUSE_DAEMONIZE_NO_CHDIR))
		(void)chdir("/");

	if (!(flags & FUSE_DAEMONIZE_NO_BACKGROUND)) {
		dm->active = true;
		err = do_daemonize(dm);
	}

	return err;
}

static void close_if_valid(int *fd)
{
	if (*fd != -1) {
		close(*fd);
		*fd = -1;
	}
}

static void fuse_daemonize_early_signal(int status)
{
	struct fuse_daemonize *dm = &daemonize;

	if (!dm->active)
		errx(EINVAL, "%s: not active and cannot signal status", __func__);

	/* The file system is not mounted yet - don't signal parent */
	if (status == FUSE_DAEMONIZE_SUCCESS && (!dm->mounted || !dm->got_init))
		return;

	dm->active = false;

	/* Stop watcher before signaling - parent will exit after this */
	stop_parent_watcher(dm);

	/* Signal status to parent */
	if (dm->signal_pipe_wr != -1) {
		int rc = write(dm->signal_pipe_wr, &status, sizeof(status));

		if (rc != sizeof(status))
			fprintf(stderr, "%s: write failed\n", __func__);
	}

	/* Redirect stdout/stderr to /dev/null on success */
	if (status == FUSE_DAEMONIZE_SUCCESS && dm->daemonized) {
		int nullfd = open("/dev/null", O_RDWR, 0);

		if (nullfd != -1) {
			(void)dup2(nullfd, 1);
			(void)dup2(nullfd, 2);
			if (nullfd > 2)
				close(nullfd);
		}
	}

	close_if_valid(&dm->signal_pipe_wr);
	close_if_valid(&dm->death_pipe_rd);
	close_if_valid(&dm->stop_pipe_rd);
	close_if_valid(&dm->stop_pipe_wr);
}

void fuse_daemonize_early_success(void)
{
	/*
	 * Needs to be gracefully handled as automatically called libfuse
	 * internal from FUSE_INIT handler
	 */
	if (!fuse_daemonize_early_is_used())
		return;

	fuse_daemonize_early_signal(FUSE_DAEMONIZE_SUCCESS);
}

void fuse_daemonize_early_fail(int err)
{
	fuse_daemonize_early_signal(err);
}

bool fuse_daemonize_early_is_used(void)
{
	return daemonize.daemonized || daemonize.active;
}

bool fuse_daemonize_early_is_active(void)
{
	return daemonize.active;
}

void fuse_daemonize_early_set_mounted(void)
{
	daemonize.mounted = true;
}

void fuse_daemonize_set_got_init(void)
{
	daemonize.got_init = true;
}

/*
 * defined in fuse_common.h, but fuse_common.h is outside of the scope
 * of this file - duplicated definition is better here.
 */
int fuse_daemonize(int foreground);

int fuse_daemonize(int foreground)
{
	/* Check if the NEW API is used */
	if (fuse_daemonize_early_is_used()) {
		perror("Newer API fuse_daemonize_start() already used\n");
		return -1;
	}

	if (!foreground) {
		int nullfd;
		int waiter[2];
		char completed;

		if (pipe(waiter)) {
			err(errno, "%s: pipe\n", __func__);
			return -1;
		}

		/*
		 * demonize current process by forking it and killing the
		 * parent.  This makes current process as a child of 'init'.
		 */
		switch (fork()) {
		case -1:
			err(errno, "%s: fork\n", __func__);
			close(waiter[0]);
			close(waiter[1]);
			return -1;
		case 0:
			/* child */
			close(waiter[0]);
			waiter[0] = -1;
			break;
		default:
			/* parent */
			(void)read(waiter[0], &completed, sizeof(completed));
			close(waiter[0]);
			close(waiter[1]);
			_exit(0);
		}

		if (setsid() == -1) {
			err(errno, "%s: setsid", __func__);
			close(waiter[1]);
			return -1;
		}

		(void)chdir("/");

		nullfd = open("/dev/null", O_RDWR, 0);
		if (nullfd != -1) {
			(void)dup2(nullfd, 0);
			(void)dup2(nullfd, 1);
			(void)dup2(nullfd, 2);
			if (nullfd > 2)
				close(nullfd);
		}

		/* Propagate completion of daemon initialization */
		completed = 1;
		(void)write(waiter[1], &completed, sizeof(completed));
		close(waiter[1]);
		waiter[1] = -1;
	} else {
		(void)chdir("/");
	}

	return 0;
}


