Plugin to add Pijul support to the Nix package manager
#include "compat.h"
#include "debug.h"

#ifdef NIX_VERSION
#if NIX_VERSION >= 0x021900
#include <processes.hh>
#else
#include <util.hh>
#endif
#else
#include <csignal>
#include <fcntl.h>
#include <format>
#include <spawn.h>
#include <sys/wait.h>
#include <sysexits.h>
#include <unistd.h>
#endif

using namespace std::string_literals;
using namespace std::string_view_literals;

namespace nixpluginpijul
{

void runProcess(const char *p, const StringList &args, const char *chdir, OutputMode mode)
{
    DBG_BEGIN

#ifdef NIX_VERSION
    auto res = nix::runProgram(nix::RunOptions{
        .program = std::string(p),
        #if NIX_VERSION >= 0x022200
        .lookupPath = true,
        #else
        .searchPath = true,
        #endif
        .args = args,
        .chdir = chdir ? Path(chdir) : std::optional<Path>(),
        .isInteractive = mode.interactive(),
    });

    if (!nix::statusOk(res.first)) {
        throw nix::ExecError(res.first, "program '%1%' %2%", p, nix::statusToString(res.first));
    }

    if (mode.output()) {
        *mode.output() = std::move(res.second);
    }
#else
    int fds[2];

    posix_spawn_file_actions_t fa;
    posix_spawn_file_actions_init(&fa);

    if (mode.output()) {
        if (pipe(fds) != 0) {
            perror("failed to create pipe");
            exit(EX_OSERR);
        }

        posix_spawn_file_actions_adddup2(&fa, fds[1], 0);
        posix_spawn_file_actions_adddup2(&fa, fds[1], 1);
    }

    if (!mode.interactive()) {
        posix_spawn_file_actions_addclose(&fa, 2);
    }

    if (chdir) {
        posix_spawn_file_actions_addchdir_np(&fa, chdir);
    }

    std::vector<const char *> argv;
    argv.push_back(p);

    for (const auto &el : args) {
        argv.push_back(el.c_str());
    }

    pid_t pid;

    if (posix_spawnp(&pid, p, &fa, nullptr, const_cast<char *const *const>(argv.data()), nullptr) != 0) {
        perror("failed to spawn process");
        exit(EX_OSERR);
    }

    close(fds[1]);
    posix_spawn_file_actions_destroy(&fa);

    fd_set set;
    FD_ZERO(&set);
    int nfds = 0;

    if (mode.output()) {
        FD_SET(fds[0], &set);
        fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK);
        nfds = 1;
    }

    sigset_t sigs;
    sigfillset(&sigs);
    sigdelset(&sigs, SIGCHLD);

    timespec timeout{.tv_sec = 1};

    bool running = true;
    int status;

    while (running) {
        // FIXME: this never catches the SIGCHLD
        int ret = pselect(nfds, &set, nullptr, nullptr, &timeout, &sigs);

        printf("%d\n", ret);

        if (ret == -1) {
            if (errno != EINTR) {
                perror("pselect");
                exit(EX_OSERR);
            }
        }

        ret = waitpid(pid, &status, WNOHANG);

        if (ret == -1) {
            perror("waitpid");
            exit(EX_OSERR);
        } else if (ret == pid) {
            running = false;
            ret = 1; // get the last data out of the pipe
        }

        if (ret > 0 && mode.output()) {
            char buf[4096];
            ssize_t len;

            do {
                len = read(fds[0], buf, sizeof(buf));

                if (len < 0 && errno != EWOULDBLOCK) {
                    perror("read");
                    exit(EX_OSERR);
                }

                if (len > 1) {
                    mode.output()->append(buf, len);
                }
            } while (len > 0);
        }
    }

    if (mode.output()) {
        close(fds[0]);
    }

    if (status != 0) {
        fatal(std::format("process `{}' exited with status {}", p, WEXITSTATUS(status)).c_str());
    }
#endif

    DBG_END
}

void fatal(const char *text)
{
#ifdef NIX_VERSION
    throw nix::Error("%1%", text);
#else
    fprintf(stderr, "fatal: %s\n", text);
    exit(EX_SOFTWARE);
#endif
}

}