/*-
 * SPDX-License-Identifier: LicenseRef-scancode-bsd-unchanged
 *
 * Copyright (c) 2011-2026 Baptiste Daroussin <bapt@FreeBSD.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer
 *    in this position and unchanged.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/stat.h>
#include <sys/wait.h>

#include <errno.h>
#include <fcntl.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "pkg.h"
#include "private/pkg.h"
#include "private/event.h"
#include "xmalloc.h"
#include "pkghash.h"

extern char **environ;

/*
 * Run a command and return its exit status, or -1 on error.
 * If quiet is true, stdout and stderr are redirected to /dev/null.
 */
static int
run_cmd(const char *program, const char **argv, bool quiet)
{
	posix_spawn_file_actions_t actions, *actionsp = NULL;
	int error, pstat;
	pid_t pid;

	if (quiet) {
		if ((error = posix_spawn_file_actions_init(&actions)) != 0 ||
		    (error = posix_spawn_file_actions_addopen(&actions,
		    STDOUT_FILENO, "/dev/null", O_RDONLY, 0)) != 0 ||
		    (error = posix_spawn_file_actions_addopen(&actions,
		    STDERR_FILENO, "/dev/null", O_RDONLY, 0)) != 0) {
			posix_spawn_file_actions_destroy(&actions);
			errno = error;
			return (-1);
		}
		actionsp = &actions;
	}

	error = posix_spawn(&pid, program, actionsp, NULL,
	    __DECONST(char **, argv), environ);
	if (actionsp != NULL)
		posix_spawn_file_actions_destroy(actionsp);
	if (error != 0) {
		errno = error;
		return (-1);
	}

	while (waitpid(pid, &pstat, 0) == -1) {
		if (errno != EINTR)
			return (-1);
	}

	return (WEXITSTATUS(pstat));
}

/*
 * Run "service <name> <cmd>" quietly and return exit status.
 */
static int
service_cmd(const char *name, const char *cmd)
{
	const char *argv[4];

	argv[0] = "service";
	argv[1] = name;
	argv[2] = cmd;
	argv[3] = NULL;
	return (run_cmd("/usr/sbin/service", argv, true));
}

/*
 * Run "<script_path> <cmd>" quietly and return exit status.
 */
static int
script_cmd(const char *script_path, const char *cmd)
{
	const char *argv[3];

	argv[0] = script_path;
	argv[1] = cmd;
	argv[2] = NULL;
	return (run_cmd(script_path, argv, true));
}

static int
rc_stop(const char *rc_file)
{
	if (rc_file == NULL)
		return (0);

	/* use faststop to avoid checking if the service was running */
	return (service_cmd(rc_file, "faststop"));
}

static int
rc_stop_with_script(const char *script_path)
{
	if (script_path == NULL)
		return (0);

	/* use faststop to avoid checking if the service was running */
	return (script_cmd(script_path, "faststop"));
}

static int
rc_start(const char *rc_file)
{
	if (rc_file == NULL)
		return (0);

	return (service_cmd(rc_file, "start"));
}

int
pkg_start_stop_rc_scripts(struct pkg *pkg, pkg_rc_attr attr)
{
	struct pkg_file *file = NULL;
	char rc_d_path[PATH_MAX];
	const char *rcfile;
	size_t len = 0;
	int ret = 0;
	bool handle_rc;

	handle_rc = pkg_object_bool(pkg_config_get("HANDLE_RC_SCRIPTS"));
	if (!handle_rc)
		return (ret);

	/* Do not manage rc scripts when operating on an alternate rootdir */
	if (ctx.pkg_rootdir != NULL)
		return (ret);

	snprintf(rc_d_path, sizeof(rc_d_path), "%s/etc/rc.d/", pkg->prefix);
	len = strlen(rc_d_path);

	while (pkg_files(pkg, &file) == EPKG_OK) {
		if (strncmp(rc_d_path, file->path, len) == 0) {
			rcfile = file->path;
			rcfile += len;
			switch (attr) {
			case PKG_RC_START:
				ret += rc_start(rcfile);
				break;
			case PKG_RC_STOP:
				ret += rc_stop(rcfile);
				break;
			}
		}
	}

	return (ret);
}

static int
copy_file(const char *src, const char *dst)
{
	int sfd, dfd;
	char buf[BUFSIZ];
	ssize_t n;
	struct stat sb;

	sfd = open(src, O_RDONLY);
	if (sfd == -1)
		return (-1);

	dfd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0700);
	if (dfd == -1) {
		close(sfd);
		return (-1);
	}

	for (;;) {
		n = read(sfd, buf, sizeof(buf));
		if (n == -1) {
			if (errno == EINTR)
				continue;
			close(sfd);
			close(dfd);
			return (-1);
		}
		if (n == 0)
			break;
		const char *p = buf;
		ssize_t remaining = n;
		while (remaining > 0) {
			ssize_t w = write(dfd, p, remaining);
			if (w == -1) {
				if (errno == EINTR)
					continue;
				close(sfd);
				close(dfd);
				return (-1);
			}
			p += w;
			remaining -= w;
		}
	}

	if (fstat(sfd, &sb) == 0)
		fchmod(dfd, sb.st_mode);

	close(sfd);
	close(dfd);
	return (n < 0 ? -1 : 0);
}

void
pkg_deferred_rc_init(struct deferred_rc *rc)
{
	memset(rc, 0, sizeof(*rc));
}

static void
deferred_rc_cleanup_cb(void *data)
{
	struct deferred_rc *rc = data;

	pkg_deferred_rc_free(rc);
}

static void
deferred_rc_stop_free(struct deferred_rc_stop *s)
{
	if (s->oldpath != NULL) {
		unlink(s->oldpath);
		free(s->oldpath);
	}
	free(s->name);
}

void
pkg_deferred_rc_free(struct deferred_rc *rc)
{
	if (rc == NULL)
		return;

	vec_foreach(rc->to_stop, i)
		deferred_rc_stop_free(&rc->to_stop.d[i]);
	vec_free(&rc->to_stop);

	vec_foreach(rc->to_start, i)
		free(rc->to_start.d[i]);
	vec_free(&rc->to_start);

	pkghash_destroy(rc->seen_stop);
	rc->seen_stop = NULL;
	pkghash_destroy(rc->seen_start);
	rc->seen_start = NULL;

	if (rc->tmpdir != NULL) {
		rmdir(rc->tmpdir);
		free(rc->tmpdir);
		rc->tmpdir = NULL;
	}
}

static int
deferred_rc_ensure_tmpdir(struct deferred_rc *rc)
{
	if (rc->tmpdir != NULL)
		return (0);

	const char *tmpdir = getenv("TMPDIR");
	if (tmpdir == NULL)
		tmpdir = "/tmp";

	char template[PATH_MAX];
	snprintf(template, sizeof(template), "%s/pkg-rc.XXXXXX", tmpdir);
	if (mkdtemp(template) == NULL) {
		pkg_errno("Cannot create temporary directory '%s'", template);
		return (-1);
	}
	rc->tmpdir = xstrdup(template);

	pkg_register_cleanup_callback(deferred_rc_cleanup_cb, rc);

	return (0);
}

void
pkg_deferred_rc_add(struct deferred_rc *rc, struct pkg *pkg, pkg_rc_attr attr)
{
	struct pkg_file *file = NULL;
	char rc_d_path[PATH_MAX];
	size_t len;
	bool handle_rc;

	handle_rc = pkg_object_bool(pkg_config_get("HANDLE_RC_SCRIPTS"));
	if (!handle_rc)
		return;

	/* Do not manage rc scripts when operating on an alternate rootdir */
	if (ctx.pkg_rootdir != NULL)
		return;

	snprintf(rc_d_path, sizeof(rc_d_path), "%s/etc/rc.d/", pkg->prefix);
	len = strlen(rc_d_path);

	while (pkg_files(pkg, &file) == EPKG_OK) {
		if (strncmp(rc_d_path, file->path, len) != 0)
			continue;

		const char *rcname = file->path + len;

		switch (attr) {
		case PKG_RC_STOP:
			if (pkghash_get(rc->seen_stop, rcname) != NULL)
				break;
			pkghash_safe_add(rc->seen_stop, rcname, NULL, NULL);

			struct deferred_rc_stop entry = { .name = xstrdup(rcname) };
			if (deferred_rc_ensure_tmpdir(rc) == 0) {
				char saved[PATH_MAX];
				snprintf(saved, sizeof(saved), "%s/%s",
				    rc->tmpdir, rcname);
				if (copy_file(file->path, saved) == 0)
					entry.oldpath = xstrdup(saved);
				else
					pkg_debug(1,
					    "Failed to save rc script %s, "
					    "will use service(8) to stop",
					    file->path);
			}
			vec_push(&rc->to_stop, entry);
			break;
		case PKG_RC_START:
			if (pkghash_get(rc->seen_start, rcname) != NULL)
				break;
			pkghash_safe_add(rc->seen_start, rcname, NULL, NULL);
			vec_push(&rc->to_start, xstrdup(rcname));
			break;
		}
	}
}

int
pkg_deferred_rc_execute(struct deferred_rc *rc)
{
	int ret = 0;

	/*
	 * Upgrades (in both stop and start sets): "service <name> restart"
	 * so the rc script can handle the transition gracefully.
	 * Deletions (stop only): stop using the saved old script.
	 * New installs (start only): start if enabled.
	 */
	vec_foreach(rc->to_stop, i) {
		struct deferred_rc_stop *s = &rc->to_stop.d[i];
		if (pkghash_get(rc->seen_start, s->name) != NULL) {
			ret += service_cmd(s->name, "restart");
		} else {
			if (s->oldpath != NULL) {
				ret += rc_stop_with_script(s->oldpath);
			} else {
				ret += rc_stop(s->name);
			}
		}
	}

	/* Start only services that were not already restarted above */
	vec_foreach(rc->to_start, i) {
		char *name = rc->to_start.d[i];
		if (pkghash_get(rc->seen_stop, name) != NULL)
			continue;
		pkg_emit_notice("Starting %s", name);
		ret += rc_start(name);
	}

	if (rc->tmpdir != NULL)
		pkg_unregister_cleanup_callback(deferred_rc_cleanup_cb, rc);
	pkg_deferred_rc_free(rc);

	return (ret);
}
