/*
   daemonlet.c
  
   All modifications to the original source file are:
    - Copyright (C) 2025 Aitor Cuadrado Zubizarreta <aitor_czr@gnuinos.org> 
 
   Original copyright and license text produced below.
*/

/*
   vdev: a virtual device manager for *nix
   Copyright (C) 2015  Jude Nelson

   This program is dual-licensed: you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 3 or later as 
   published by the Free Software Foundation. For the terms of this 
   license, see LICENSE.GPLv3+ or <http://www.gnu.org/licenses/>.

   You are free to use this program under the terms of the GNU General
   Public License, but WITHOUT ANY WARRANTY; without even the implied 
   warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   See the GNU General Public License for more details.

   Alternatively, you are free to use this program under the terms of the 
   Internet Software Consortium License, but WITHOUT ANY WARRANTY; without
   even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   For the terms of this license, see LICENSE.ISC or 
   <http://www.isc.org/downloads/software-support-policy/isc-license/>.
*/


#include "config.h"
#include "util.h"
#include "daemonlet.h"

#include <signal.h>
#include <sys/prctl.h>

#define VDEV_METADATA_PREFIX     "metadata/"

// clean up a daemonlet's state in an action 
// always succeeds 
static int vdev_daemonlet_clean(struct vdev_daemonlet *daemonlet)
{
    /* dead */ 
    daemonlet->pid = -1;
   
    if (daemonlet->stdin >= 0) {
        close(daemonlet->stdin);
        daemonlet->stdin = -1;
    }   
    if (daemonlet->stdout >= 0) {
        close(daemonlet->stdout);
        daemonlet->stdout = -1;
    }   
    return 0;
}

// start up a daemonlet, using the daemonlet helper program at $VDEV_HELPERS/daemonlet.
// stderr will be routed to /dev/null
// return 0 on success, and fill in the relevant information to act.
// return 0 if the daemonlet is already running
// return -errno from stat(2) if we couldn't find the daemonlet runner (i.e. -ENOENT)
// return -EPERM if the daemonlet is not executable
// return -errno from pipe(2) if we could not allocate a pipe
// return -errno from open(2) if we could not open /dev/null
// return -errno from fork(2) if we could not fork
// return -ECHILD if the daemonlet died before it could signal readiness
// NOTE: /dev/null *must* exist already--this should be taken care of by the pre-seed script.
int vdev_daemonlet_start(struct vdev_config *cfg, 
                         struct vdev_daemonlet *daemonlet, 
                         const char *command, 
                         const char *name, 
                         bool async, 
                         int *error_fd)
{
    int rc = 0;
    pid_t pid = 0;
    int daemonlet_pipe_stdin[2];
    int daemonlet_pipe_stdout[2];
    char daemonlet_runner_path[PATH_MAX + 1];
    char vdevd_global_metadata[PATH_MAX + 1];
    long max_open = sysconf(_SC_OPEN_MAX);
    struct stat sb;
    struct vdev_config *config = cfg;
    
    char *daemonlet_argv[] = { "vdevd-daemonlet", (char*)command, NULL };
   
    if (max_open <= 0)
        max_open = 1024;
   
    if (daemonlet->pid > 0)
        return 0;
   
    memset(daemonlet_runner_path, 0, PATH_MAX + 1);
    memset(vdevd_global_metadata, 0, PATH_MAX + 1);
   
    snprintf(daemonlet_runner_path, PATH_MAX, "%s/daemonlet", config->helpers_dir);
    snprintf(vdevd_global_metadata, PATH_MAX, "%s/" VDEV_METADATA_PREFIX, config->mountpoint);
   
    /* the daemonlet runner must exist */ 
    rc = stat(daemonlet_runner_path, &sb);
    if (rc != 0) {
        vdev_error("stat('%s') rc = %d\n", daemonlet_runner_path, rc);
        return rc;
    }
   
    /* the daemonlet runner must be executable */
    if (!S_ISREG(sb.st_mode) || !(((S_IXUSR & sb.st_mode) && sb.st_uid == geteuid()) || 
        ((S_IXGRP & sb.st_mode) && sb.st_gid == getegid()) || (S_IXOTH & sb.st_mode))) {
            vdev_error("%s is not a regular file, or is not executable for vdevd\n", 
                       daemonlet_runner_path);
            return -EPERM;
    }
   
    rc = pipe(daemonlet_pipe_stdin);
    if (rc < 0) {
        rc = -errno;
        vdev_error("pipe rc = %d\n", rc);
        return rc;
    }
   
    rc = pipe(daemonlet_pipe_stdout);
    if (rc < 0) {
        rc = -errno;
        vdev_error("pipe rc = %d\n", rc);
      
        close(daemonlet_pipe_stdin[0]);
        close(daemonlet_pipe_stdin[1]);
        return rc;
    }
  
    pid = fork();
    if (pid == 0) {
        /* child */ 
        {
            sigset_t cleared;
            sigemptyset(&cleared);
            /* vdevd may block signals for signalfd(2); children must accept SIGINT/SIGTERM */
            sigprocmask(SIG_SETMASK, &cleared, NULL);
        }
        close(daemonlet_pipe_stdin[1]);
        close(daemonlet_pipe_stdout[0]);
    
        /* request TERM signal if parent exits */
        prctl(PR_SET_PDEATHSIG, SIGTERM);
      
        /* redirect */ 
        rc = dup2(daemonlet_pipe_stdin[0], STDIN_FILENO);
        if (rc < 0) {
            vdev_error_async_safe("dup2 stdin failed\n");
            exit(1);
        }
      
        rc = dup2(daemonlet_pipe_stdout[1], STDOUT_FILENO);
        if (rc < 0) {
            vdev_error_async_safe("dup2 stdout to vdevd failed\n");
            _exit(1);
        }

        rc = dup2(*error_fd, STDERR_FILENO);
        if (rc < 0) {
            vdev_error_async_safe("dup2 stderr to null failed\n");
            _exit(1);
        }
      
        /* close everything else
         * the daemonlet should capture stderr itself
         */
        for (int i = 3; i < max_open; i++) 
            close(i);
   
        /* set basic environment variables */
        clearenv();
        setenv("VDEV_GLOBAL_METADATA", vdevd_global_metadata, 1);
        setenv("VDEV_MOUNTPOINT", config->mountpoint, 1);
        setenv("VDEV_HELPERS", config->helpers_dir, 1);
        setenv("VDEV_LOGFILE", config->logfile_path, 1);
        setenv("VDEV_CONFIG_FILE", config->config_path, 1);
        setenv("VDEV_INSTANCE", config->instance_str, 1);
      
        /* start the daemonlet */ 
        execv(daemonlet_runner_path, daemonlet_argv);
      
        /* keep gcc happy */
        _exit(0);

    } else if (pid > 0) {

        /* parent vdevd */
        close(daemonlet_pipe_stdin[0]);
        close(daemonlet_pipe_stdout[1]);
      
        /* wait for child to indicate readiness, if running synchronously */
        if (!async) {
            while (1) {
                char tmp = 0;
                ssize_t sz = vdev_read_uninterrupted(daemonlet_pipe_stdout[0], &tmp, 1);
                if (sz <= 0) {
                    /* some fatal error, or process has closed stdout
                     * not much we can safely do at this point (don't want to risk SIGKILL'ing)
                     */
                    close(daemonlet_pipe_stdin[1]);
                    close(daemonlet_pipe_stdout[0]);
                    if (sz == 0) {
                        /* process has closed stdout */
                        vdev_error("vdev_read_uninterrupted(%d PID=%d action='%s') rc = %d\n", 
                                   daemonlet_pipe_stdout[0], pid, name, rc);
                        rc = -ECHILD;
                    }
                    return (int)sz;
                } else {
                    /* got back readiness hint */
                    break;
                }
            }
        }
        
        /* hold onto its runtime state */
        daemonlet->pid = pid;
        daemonlet->stdin = daemonlet_pipe_stdin[1];
        daemonlet->stdout = daemonlet_pipe_stdout[0];
      
        /* daemonlet started! */
        return 0;

    } else {
        /* fork() error */ 
        rc = -errno;
        vdev_error("fork() rc = %d\n", rc);
      
        close(daemonlet_pipe_stdin[0]);
        close(daemonlet_pipe_stdin[1]);

        close(daemonlet_pipe_stdout[0]);
        close(daemonlet_pipe_stdout[1]);
      
        return rc;
    }
}

// stop a daemonlet and join with it
// first, ask it by writing "exit" to its stdin pipe.
// wait 30 seconds before SIGTERM'ing the daemonlet.
// return 0 on success, even if the daemonlet is already dead
int vdev_daemonlet_stop(struct vdev_daemonlet *daemonlet, const char *action_name)
{ 
    int rc = 0;
    pid_t child_pid = 0;
   
    if (daemonlet->pid <= 0 || daemonlet->stdin < 0 || daemonlet->stdout < 0)
        /* not running */
        return 0;
   
    /* confirm that it is still running by trying to join with it */ 
    child_pid = waitpid(daemonlet->pid, &rc, WNOHANG);
    if (child_pid == daemonlet->pid || child_pid < 0) {
        /* joined, or not running */
        vdev_debug("Daemonlet %d (%s) dead\n", daemonlet->pid, action_name);
        vdev_daemonlet_clean(daemonlet);
        return 0;
    }
   
    /* ask it to die: close stdin */ 
    rc = close(daemonlet->stdin);
    if (rc < 0) {
        vdev_error("close(%d PID=%d name=%s) rc = %d\n", 
                   daemonlet->stdin, daemonlet->pid, action_name, rc);
        daemonlet->stdin = -1;
      
        /* no choice but to kill this one */ 
        kill(daemonlet->pid, SIGTERM);
        vdev_daemonlet_clean(daemonlet);
      
        /* join with it... */
        rc = 0;
    } else {
        /* tell daemonlet to die */
        rc = kill(daemonlet->pid, SIGINT);
        if (rc < 0)
            vdev_error("kill(PID=%d name=%s) rc = %d\n", daemonlet->pid, action_name, rc);
      
        /* will wait for the child to die */
        daemonlet->stdin = -1;
    }
   
    /* join with it. */
    while (1) {
        /* attempt to join */
        child_pid = waitpid(daemonlet->pid, &rc, 0);
        if (child_pid < 0) {
            rc = -errno;
            if (rc == -ECHILD) {
                /* already dead */ 
                rc = 0;
                vdev_debug("Daemonlet %d (%s) dead\n", daemonlet->pid, action_name);
                vdev_daemonlet_clean(daemonlet);
                break;
            } else if (rc == -EINTR) {
                // unhandled signal *or* SIGCHLD 
                child_pid = waitpid(daemonlet->pid, &rc, WNOHANG);
                if (child_pid > 0)
                    /* child died */
                    break;
                else
                    /* try waiting again */
                    continue;
            }
        
        } else if (child_pid > 0) {
            /* joined! */
            break;
        }
    }

    /* clean this child out (even if we had an error with waitpid) */
    vdev_debug("Daemonlet %d (%s) dead\n", daemonlet->pid, action_name);
    vdev_daemonlet_clean(daemonlet);
   
    return rc;
}

// read a string-ified int64_t from a file descriptor, followed by a newline.
// this is used to get a daemonlet return code 
// return 0 on success, and set *ret 
// return -errno on I/O error (such as -EPIPE)
// return -EAGAIN if we got EOF
int vdev_daemonlet_read_int64(int fd, int64_t *ret)
{
    int64_t value = 0;
    int cnt = 0; 
    int c = 0;
    int rc = 0;
    int s = 1;
   
    while (1) {
        /* next character */ 
        ssize_t sz = vdev_read_uninterrupted(fd, (char*)&c, 1);
        if (sz < 0)
            return sz;
        if (sz == 0)
            return -EAGAIN;
        if (c == '\n')
            break;
        if (cnt == 0 && c == '-') {
            s = -1;
        } else if (c < '0' || c > '9') {
            /* invalid */ 
            rc = -EINVAL;
            break;
        }
      
        value *= 10;
        value += (c - '0');

        cnt++;
    }
   
    *ret = value * s;
    return rc;
}

// issue a command to a daemonlet, and get back its return code.
// the daemonlet should already be running (or thought to be running) before calling this method.
// if this method fails, the caller should consider restarting the daemonlet.
// return 0 on success, and set *daemonlet_rc
// return -ENOMEM on OOM
// return -EAGAIN if the daemonlet needs to be restarted and the request retried.
// return -EPERM on permanent daemonlet failure
int vdev_daemonlet_send_command(char **env, 
                                int num_env, 
                                const char *name, 
                                bool async, 
                                int64_t *daemonlet_rc, 
                                struct vdev_daemonlet *daemonlet)
{
    int rc = 0;
    char env_buf[PATH_MAX + 1];

    memset(env_buf, 0, PATH_MAX + 1);
   
    vdev_debug("run daemonlet (async=%d): '%s'\n", async, name);
   
    for (int i = 0; i < num_env; i++)
        vdev_debug("daemonlet env: '%s'\n", env[i]);
   
    /* feed environment variables */
    for (int i = 0; i < num_env; i++) {
        snprintf(env_buf, PATH_MAX, "%s\n", env[i]);
        rc = vdev_write_uninterrupted(daemonlet->stdin, env_buf, strlen(env_buf));
        if (rc < 0) {
            // failed to write! 
            vdev_error("vdev_write_uninterrupted(%d) to daemonlet '%s' rc = %d\n", 
                       daemonlet->stdin, name, rc);
            break;
        }
    }
   
    if (rc > 0) {
        /* feed end-of-environment flag */
        rc = vdev_write_uninterrupted(daemonlet->stdin, "done\n", strlen("done\n"));
        if (rc < 0)
            vdev_error("vdev_write_uninterrupted(%d) to daemonlet '%s' rc = %d\n", 
                       daemonlet->stdin, name, rc);
    }
   
    if (rc < 0) {
        /* -EPIPE means the daemonlet is dead, and the caller should restart it */
        if (rc == -EPIPE)
            return -EAGAIN;
        else
            return -EPERM;
    }
   
    /* if we're running asynchronously, then we don't care about getting back the reply 
     * (stdout is routed to /dev/null anyway)
     */
    if (async) {
        *daemonlet_rc = 0;
        return 0;
    }
   
    /* wait for a status code reply */
    rc = vdev_daemonlet_read_int64(daemonlet->stdout, daemonlet_rc);
    if (rc < 0) {
        vdev_error("vdev_action_daemonlet_read_int64('%s') rc = %d\n", name, rc);
        /* -EPIPE means the daemonlet is dead, and the caller should restart it */ 
        if (rc == -EPIPE)
            return -EAGAIN;
        else
            return -EPERM;
    }
   
    if (*daemonlet_rc < 0 || *daemonlet_rc > 255) {
        /* invalid daemonlet return code 
         * caller should consider restarting the daemonlet and trying again
         */
        vdev_error("vdev_daemonlet_read_int64('%s', PID=%d) exit status %d\n", 
                   name, daemonlet->pid, (int)*daemonlet_rc);
        return -EAGAIN;
    }
   
    return 0;
}
