/*
 * Copyright 2017-2025 the Pacemaker project contributors
 *
 * The version control history for this file may have further details.
 *
 * This source code is licensed under the GNU Lesser General Public License
 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
 */

#include <crm_internal.h>

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <crm/crm.h>
#include "pacemaker-execd.h"

static pid_t main_pid = 0;

static void
sigdone(void)
{
    crm_exit(CRM_EX_OK);
}

static void
sigreap(void)
{
    pid_t pid = 0;
    int status;

    do {
        /*
         * Opinions seem to differ as to what to put here:
         *  -1, any child process
         *  0,  any child process whose process group ID is equal to that of the calling process
         */
        pid = waitpid(-1, &status, WNOHANG);
        if (pid == main_pid) {
            /* Exit when pacemaker-remote exits and use the same return code */
            if (WIFEXITED(status)) {
                crm_exit(WEXITSTATUS(status));
            }
            crm_exit(CRM_EX_ERROR);
        }
    } while (pid > 0);
}

static struct {
    int sig;
    void (*handler)(void);
} sigmap[] = {
    { SIGCHLD, sigreap },
    { SIGINT,  sigdone },
};

/*!
 * \internal
 * \brief Check a line of text for a valid environment variable name
 *
 * \param[in]  line  Text to check
 * \param[out] first  First character of valid name if found, NULL otherwise
 * \param[out] last   Last character of valid name if found, NULL otherwise
 *
 * \return TRUE if valid name found, FALSE otherwise
 * \note It's reasonable to impose limitations on environment variable names
 *       beyond what C or setenv() does: We only allow names that contain only
 *       [a-zA-Z0-9_] characters and do not start with a digit.
 */
static bool
find_env_var_name(char *line, char **first, char **last)
{
    // Skip leading whitespace
    *first = line;
    while (isspace(**first)) {
        ++*first;
    }

    if (isalpha(**first) || (**first == '_')) { // Valid first character
        *last = *first;
        while (isalnum(*(*last + 1)) || (*(*last + 1) == '_')) {
            ++*last;
        }
        return TRUE;
    }

    *first = *last = NULL;
    return FALSE;
}

static void
load_env_vars(const char *filename)
{
    /* We haven't forked or initialized logging yet, so don't leave any file
     * descriptors open, and don't log -- silently ignore errors.
     */
    FILE *fp = fopen(filename, "r");

    if (fp != NULL) {
        char line[LINE_MAX] = { '\0', };

        while (fgets(line, LINE_MAX, fp) != NULL) {
            char *name = NULL;
            char *end = NULL;
            char *value = NULL;
            char *quote = NULL;

            // Look for valid name immediately followed by equals sign
            if (find_env_var_name(line, &name, &end) && (*++end == '=')) {

                // Null-terminate name, and advance beyond equals sign
                *end++ = '\0';

                // Check whether value is quoted
                if ((*end == '\'') || (*end == '"')) {
                    quote = end++;
                }
                value = end;

                if (quote) {
                    /* Value is remaining characters up to next non-backslashed
                     * matching quote character.
                     */
                    while (((*end != *quote) || (*(end - 1) == '\\'))
                           && (*end != '\0')) {
                        end++;
                    }
                    if (*end == *quote) {
                        // Null-terminate value, and advance beyond close quote
                        *end++ = '\0';
                    } else {
                        // Matching closing quote wasn't found
                        value = NULL;
                    }

                } else {
                    /* Value is remaining characters up to next non-backslashed
                     * whitespace.
                     */
                    while ((!isspace(*end) || (*(end - 1) == '\\'))
                           && (*end != '\0')) {
                        ++end;
                    }

                    if (end == (line + LINE_MAX - 1)) {
                        // Line was too long
                        value = NULL;
                    }
                    // Do NOT null-terminate value (yet)
                }

                /* We have a valid name and value, and end is now the character
                 * after the closing quote or the first whitespace after the
                 * unquoted value. Make sure the rest of the line is just
                 * whitespace or a comment.
                 */
                if (value) {
                    char *value_end = end;

                    while (isspace(*end) && (*end != '\n')) {
                        ++end;
                    }
                    if ((*end == '\n') || (*end == '#')) {
                        if (quote == NULL) {
                            // Now we can null-terminate an unquoted value
                            *value_end = '\0';
                        }

                        // Don't overwrite (bundle options take precedence)
                        // coverity[tainted_string] This can't easily be changed right now
                        setenv(name, value, 0);

                    } else {
                        value = NULL;
                    }
                }
            }

            if ((value == NULL) && (strchr(line, '\n') == NULL)) {
                // Eat remainder of line beyond LINE_MAX
                if (fscanf(fp, "%*[^\n]\n") == EOF) {
                    value = NULL; // Don't care, make compiler happy
                }
            }
        }
        fclose(fp);
    }
}

void
remoted_spawn_pidone(int argc, char **argv, char **envp)
{
    sigset_t set;

    /* This environment variable exists for two purposes:
     * - For testing, setting it to "full" enables full PID 1 behavior even
     *   when PID is not 1
     * - Setting to "vars" enables just the loading of environment variables
     *   from /etc/pacemaker/pcmk-init.env, which could be useful for testing or
     *   containers with a custom PID 1 script that launches the remote
     *   executor.
     */
    const char *pid1 = PCMK_VALUE_DEFAULT;

    if (getpid() != 1) {
        pid1 = pcmk__env_option(PCMK__ENV_REMOTE_PID1);
        if (!pcmk__str_any_of(pid1, "full", "vars", NULL)) {
            // Default, unset, or invalid
            return;
        }
    }

    /* When a container is launched, it may be given specific environment
     * variables, which for Pacemaker bundles are given in the bundle
     * configuration. However, that does not allow for host-specific values.
     * To allow for that, look for a special file containing a shell-like syntax
     * of name/value pairs, and export those into the environment.
     */
    load_env_vars("/etc/pacemaker/pcmk-init.env");

    if (strcmp(pid1, "vars") == 0) {
        return;
    }

    /* Containers can be expected to have /var/log, but they may not have
     * /var/log/pacemaker, so use a different default if no value has been
     * explicitly configured in the container's environment.
     */
    if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) {
        pcmk__set_env_option(PCMK__ENV_LOGFILE, "/var/log/pcmk-init.log", true);
    }

    sigfillset(&set);
    sigprocmask(SIG_BLOCK, &set, 0);

    main_pid = fork();
    switch (main_pid) {
        case 0:
            sigprocmask(SIG_UNBLOCK, &set, NULL);
            setsid();
            setpgid(0, 0);

            // Child remains as pacemaker-remoted
            return;
        case -1:
            crm_err("fork failed: %s", pcmk_rc_str(errno));
    }

    /* Parent becomes the reaper of zombie processes */
    /* Safe to initialize logging now if needed */

#  ifdef HAVE_PROGNAME
    /* Differentiate ourselves in the 'ps' output */
    {
        char *p;
        int i, maxlen;
        char *LastArgv = NULL;
        const char *name = "pcmk-init";

        for (i = 0; i < argc; i++) {
            if (!i || (LastArgv + 1 == argv[i]))
                LastArgv = argv[i] + strlen(argv[i]);
        }

        for (i = 0; envp[i] != NULL; i++) {
            if ((LastArgv + 1) == envp[i]) {
                LastArgv = envp[i] + strlen(envp[i]);
            }
        }

        maxlen = (LastArgv - argv[0]) - 2;

        i = strlen(name);

        /* We can overwrite individual argv[] arguments */
        snprintf(argv[0], maxlen, "%s", name);

        /* Now zero out everything else */
        p = &argv[0][i];
        while (p < LastArgv) {
            *p++ = '\0';
        }
        argv[1] = NULL;
    }
#  endif // HAVE_PROGNAME

    while (1) {
        int sig;
        size_t i;

        sigwait(&set, &sig);
        for (i = 0; i < PCMK__NELEM(sigmap); i++) {
            if (sigmap[i].sig == sig) {
                sigmap[i].handler();
                break;
            }
        }
    }
}
