// SPDX-License-Identifier: GPL-3.0-or-later

#include "../libnetdata.h"

static pthread_mutex_t myp_lock;
static int myp_tracking = 0;

struct mypopen {
    pid_t pid;
    struct mypopen *next;
    struct mypopen *prev;
};

static struct mypopen *mypopen_root = NULL;

// myp_add_lock takes the lock if we're tracking.
static void myp_add_lock(void) {
    if (myp_tracking == 0)
        return;

    netdata_mutex_lock(&myp_lock);
}

// myp_add_unlock release the lock if we're tracking.
static void myp_add_unlock(void) {
    if (myp_tracking == 0)
        return;

    netdata_mutex_unlock(&myp_lock);
}

// myp_add_locked adds pid if we're tracking.
// myp_add_lock must have been called previously.
static void myp_add_locked(pid_t pid) {
    struct mypopen *mp;

    if (myp_tracking == 0)
        return;

    mp = mallocz(sizeof(struct mypopen));
    mp->pid = pid;

    mp->next = mypopen_root;
    mp->prev = NULL;
    if (mypopen_root != NULL)
        mypopen_root->prev = mp;
    mypopen_root = mp;
    netdata_mutex_unlock(&myp_lock);
}

// myp_del deletes pid if we're tracking.
static void myp_del(pid_t pid) {
    struct mypopen *mp;

    if (myp_tracking == 0)
        return;

    netdata_mutex_lock(&myp_lock);
    for (mp = mypopen_root; mp != NULL; mp = mp->next) {
        if (mp->pid == pid) {
            if (mp->next != NULL)
                mp->next->prev = mp->prev;
            if (mp->prev != NULL)
                mp->prev->next = mp->next;
            if (mypopen_root == mp)
                mypopen_root = mp->next;
            freez(mp);
            break;
        }
    }

    if (mp == NULL)
        error("Cannot find pid %d.", pid);

    netdata_mutex_unlock(&myp_lock);
}

#define PIPE_READ 0
#define PIPE_WRITE 1

static inline void convert_argv_to_string(char *dst, size_t size, const char *spawn_argv[]) {
    int i;
    for(i = 0; spawn_argv[i] ;i++) {
        if(i == 0) snprintfz(dst, size, "%s", spawn_argv[i]);
        else {
            size_t len = strlen(dst);
            snprintfz(&dst[len], size - len, " '%s'", spawn_argv[i]);
        }
    }
}

/*
 * Returns -1 on failure, 0 on success. When POPEN_FLAG_CREATE_PIPE is set, on success set the FILE *fp pointer.
 */
static int custom_popene(volatile pid_t *pidptr, char **env, uint8_t flags, FILE **fpp, const char *command, const char *spawn_argv[]) {
    // create a string to be logged about the command we are running
    char command_to_be_logged[2048];
    convert_argv_to_string(command_to_be_logged, sizeof(command_to_be_logged), spawn_argv);
    // info("custom_popene() running command: %s", command_to_be_logged);

    FILE *fp = NULL;
    int ret = 0; // success by default
    int pipefd[2], error;
    pid_t pid;
    posix_spawnattr_t attr;
    posix_spawn_file_actions_t fa;

    if (flags & POPEN_FLAG_CREATE_PIPE) {
        if (pipe(pipefd) == -1)
            return -1;
        if ((fp = fdopen(pipefd[PIPE_READ], "r")) == NULL) {
            goto error_after_pipe;
        }
    }

    if (flags & POPEN_FLAG_CLOSE_FD) {
        // Mark all files to be closed by the exec() stage of posix_spawn()
        int i;
        for (i = (int) (sysconf(_SC_OPEN_MAX) - 1); i >= 0; i--) {
            if (i != STDIN_FILENO && i != STDERR_FILENO)
                (void) fcntl(i, F_SETFD, FD_CLOEXEC);
        }
    }

    if (!posix_spawn_file_actions_init(&fa)) {
        if (flags & POPEN_FLAG_CREATE_PIPE) {
            // move the pipe to stdout in the child
            if (posix_spawn_file_actions_adddup2(&fa, pipefd[PIPE_WRITE], STDOUT_FILENO)) {
                error("posix_spawn_file_actions_adddup2() failed");
                goto error_after_posix_spawn_file_actions_init;
            }
        } else {
            // set stdout to /dev/null
            if (posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, "/dev/null", O_WRONLY, 0)) {
                error("posix_spawn_file_actions_addopen() failed");
                // this is not a fatal error
            }
        }
    } else {
        error("posix_spawn_file_actions_init() failed.");
        goto error_after_pipe;
    }
    if (!(error = posix_spawnattr_init(&attr))) {
        // reset all signals in the child
        sigset_t mask;

        if (posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF))
            error("posix_spawnattr_setflags() failed.");
        sigemptyset(&mask);
        if (posix_spawnattr_setsigmask(&attr, &mask))
            error("posix_spawnattr_setsigmask() failed.");
    } else {
        error("posix_spawnattr_init() failed.");
    }

    // Take the lock while we fork to ensure we don't race with SIGCHLD
    // delivery on a process which exits quickly.
    myp_add_lock();
    if (!posix_spawn(&pid, command, &fa, &attr, (char * const*)spawn_argv, env)) {
        *pidptr = pid;
        myp_add_locked(pid);
        debug(D_CHILDS, "Spawned command: \"%s\" on pid %d from parent pid %d.", command_to_be_logged, pid, getpid());
    } else {
        myp_add_unlock();
        error("Failed to spawn command: \"%s\" from parent pid %d.", command_to_be_logged, getpid());
        if (flags & POPEN_FLAG_CREATE_PIPE) {
            fclose(fp);
        }
        ret = -1;
    }
    if (flags & POPEN_FLAG_CREATE_PIPE) {
        close(pipefd[PIPE_WRITE]);
        if (0 == ret) // on success set FILE * pointer
            if(fpp) *fpp = fp;
    }

    if (!error) {
        // posix_spawnattr_init() succeeded
        if (posix_spawnattr_destroy(&attr))
            error("posix_spawnattr_destroy");
    }
    if (posix_spawn_file_actions_destroy(&fa))
        error("posix_spawn_file_actions_destroy");

    return ret;

error_after_posix_spawn_file_actions_init:
    if (posix_spawn_file_actions_destroy(&fa))
        error("posix_spawn_file_actions_destroy");

error_after_pipe:
    if (flags & POPEN_FLAG_CREATE_PIPE) {
        if (fp)
            fclose(fp);
        else
            close(pipefd[PIPE_READ]);

        close(pipefd[PIPE_WRITE]);
    }
    return -1;
}

int custom_popene_variadic_internal_dont_use_directly(volatile pid_t *pidptr, char **env, uint8_t flags, FILE **fpp, const char *command, ...) {
    // convert the variable list arguments into what posix_spawn() needs
    // all arguments are expected strings
    va_list args;
    int args_count;

    // count the number variable parameters
    // the variable parameters are expected NULL terminated
    {
        const char *s;

        va_start(args, command);
        args_count = 0;
        while ((s = va_arg(args, const char *))) args_count++;
        va_end(args);
    }

    // create a string pointer array as needed by posix_spawn()
    // variable array in the stack
    const char *spawn_argv[args_count + 1];
    {
        const char *s;
        va_start(args, command);
        int i;
        for (i = 0; i < args_count; i++) {
            s = va_arg(args, const char *);
            spawn_argv[i] = s;
        }
        spawn_argv[args_count] = NULL;
        va_end(args);
    }

    return custom_popene(pidptr, env, flags, fpp, command, spawn_argv);
}

// See man environ
extern char **environ;

// myp_init should be called by apps which act as init
// (pid 1) so that processes created by mypopen and mypopene
// are tracked. This enables the reaper to ignore processes
// which will be handled internally, by calling myp_reap, to
// avoid issues with already reaped processes during wait calls.
//
// Callers should call myp_free() to clean up resources.
void myp_init(void) {
    info("process tracking enabled.");
    myp_tracking = 1;

    if (netdata_mutex_init(&myp_lock) != 0) {
	fatal("myp_init() mutex init failed.");
    }
}

// myp_free cleans up any resources allocated for process
// tracking.
void myp_free(void) {
    struct mypopen *mp, *next;

    if (myp_tracking == 0)
        return;

    netdata_mutex_lock(&myp_lock);
    for (mp = mypopen_root; mp != NULL; mp = next) {
        next = mp->next;
        freez(mp);
    }

    mypopen_root = NULL;
    myp_tracking = 0;
    netdata_mutex_unlock(&myp_lock);
}

// myp_reap returns 1 if pid should be reaped, 0 otherwise.
int myp_reap(pid_t pid) {
    struct mypopen *mp;

    if (myp_tracking == 0)
        return 0;

    netdata_mutex_lock(&myp_lock);
    for (mp = mypopen_root; mp != NULL; mp = mp->next) {
        if (mp->pid == pid) {
            netdata_mutex_unlock(&myp_lock);
            return 0;
        }
    }
    netdata_mutex_unlock(&myp_lock);

    return 1;
}

FILE *mypopen(const char *command, volatile pid_t *pidptr) {
    FILE *fp = NULL;
    const char *spawn_argv[] = {
        "sh",
        "-c",
        command,
        NULL
    };
    (void)custom_popene(pidptr, environ, POPEN_FLAG_CREATE_PIPE|POPEN_FLAG_CLOSE_FD, &fp, "/bin/sh", spawn_argv);
    return fp;
}

FILE *mypopene(const char *command, volatile pid_t *pidptr, char **env) {
    FILE *fp = NULL;
    const char *spawn_argv[] = {
        "sh",
        "-c",
        command,
        NULL
    };
    (void)custom_popene( pidptr, env, POPEN_FLAG_CREATE_PIPE|POPEN_FLAG_CLOSE_FD, &fp, "/bin/sh", spawn_argv);
    return fp;
}

// returns 0 on success, -1 on failure
int netdata_spawn(const char *command, volatile pid_t *pidptr) {
    const char *spawn_argv[] = {
        "sh",
        "-c",
        command,
        NULL
    };
    return custom_popene( pidptr, environ, POPEN_FLAG_NONE, NULL, "/bin/sh", spawn_argv);
}

int custom_pclose(FILE *fp, pid_t pid) {
    int ret;
    siginfo_t info;

    debug(D_EXIT, "Request to mypclose() on pid %d", pid);

    if (fp) {
        // close the pipe fd
        // this is required in musl
        // without it the childs do not exit
        close(fileno(fp));

        // close the pipe file pointer
        fclose(fp);
    }

    errno = 0;

    ret = waitid(P_PID, (id_t) pid, &info, WEXITED);
    myp_del(pid);

    if (ret != -1) {
        switch (info.si_code) {
            case CLD_EXITED:
                if(info.si_status)
                    error("child pid %d exited with code %d.", info.si_pid, info.si_status);
                return(info.si_status);

            case CLD_KILLED:
                if(info.si_status == 15) {
                    info("child pid %d killed by signal %d.", info.si_pid, info.si_status);
                    return(0);
                }
                else {
                    error("child pid %d killed by signal %d.", info.si_pid, info.si_status);
                    return(-1);
                }

            case CLD_DUMPED:
                error("child pid %d core dumped by signal %d.", info.si_pid, info.si_status);
                return(-2);

            case CLD_STOPPED:
                error("child pid %d stopped by signal %d.", info.si_pid, info.si_status);
                return(0);

            case CLD_TRAPPED:
                error("child pid %d trapped by signal %d.", info.si_pid, info.si_status);
                return(-4);

            case CLD_CONTINUED:
                error("child pid %d continued by signal %d.", info.si_pid, info.si_status);
                return(0);

            default:
                error("child pid %d gave us a SIGCHLD with code %d and status %d.", info.si_pid, info.si_code, info.si_status);
                return(-5);
        }
    }
    else
        error("Cannot waitid() for pid %d", pid);
    
    return 0;
}

int mypclose(FILE *fp, pid_t pid)
{
    return custom_pclose(fp, pid);
}

int netdata_spawn_waitpid(pid_t pid)
{
    return custom_pclose(NULL, pid);
}
