BareGit
#include "engine_process.hpp"

#include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <spawn.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

#include <array>
#include <algorithm>
#include <stdexcept>
#include <vector>

extern char** environ;

namespace nethack_mcp
{

EngineProcess::~EngineProcess()
{
    terminate();
}

bool EngineProcess::start(const Json& start_message, EventCallback callback,
                          std::string& error,
                          std::size_t max_diagnostic_bytes)
{
    if(running_)
    {
        error = "a NetHack worker is already running";
        return false;
    }

    int channel_fds[2] = {-1, -1};
    int diagnostic_fds[2] = {-1, -1};
    if(::socketpair(AF_UNIX, SOCK_STREAM, 0, channel_fds) != 0)
    {
        error = std::string("could not create worker IPC: ")
            + std::strerror(errno);
        return false;
    }
    if(::pipe(diagnostic_fds) != 0)
    {
        error = std::string("could not create worker diagnostics pipe: ")
            + std::strerror(errno);
        ::close(channel_fds[0]);
        ::close(channel_fds[1]);
        return false;
    }

    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_adddup2(&actions, channel_fds[1], 3);
    if(channel_fds[1] != 3)
    {
        posix_spawn_file_actions_addclose(&actions, channel_fds[1]);
    }
    posix_spawn_file_actions_addclose(&actions, channel_fds[0]);
    posix_spawn_file_actions_adddup2(&actions, diagnostic_fds[1], STDOUT_FILENO);
    posix_spawn_file_actions_adddup2(&actions, diagnostic_fds[1], STDERR_FILENO);
    if(diagnostic_fds[1] != STDOUT_FILENO
       && diagnostic_fds[1] != STDERR_FILENO)
    {
        posix_spawn_file_actions_addclose(&actions, diagnostic_fds[1]);
    }
    posix_spawn_file_actions_addclose(&actions, diagnostic_fds[0]);

    std::string executable = executablePath();
    std::vector<char*> arguments;
    std::vector<char> executable_storage(executable.begin(), executable.end());
    executable_storage.push_back('\0');
    char engine_option[] = "--engine";
    char ipc_option[] = "--ipc-fd";
    char ipc_descriptor[] = "3";
    arguments.push_back(executable_storage.data());
    arguments.push_back(engine_option);
    arguments.push_back(ipc_option);
    arguments.push_back(ipc_descriptor);
    arguments.push_back(nullptr);

    pid_t child = -1;
    const int spawn_result = posix_spawn(
        &child, executable.c_str(), &actions, nullptr, arguments.data(),
        environ);
    posix_spawn_file_actions_destroy(&actions);
    ::close(channel_fds[1]);
    ::close(diagnostic_fds[1]);
    if(spawn_result != 0)
    {
        ::close(channel_fds[0]);
        ::close(diagnostic_fds[0]);
        error = std::string("could not spawn NetHack worker: ")
            + std::strerror(spawn_result);
        return false;
    }

    process_id_ = child;
    max_diagnostic_bytes_ = max_diagnostic_bytes;
    callback_ = std::move(callback);
    channel_ = std::make_unique<FramedChannel>(channel_fds[0]);
    running_ = true;
    diagnostic_thread_ = std::thread(
        &EngineProcess::diagnosticLoop, this, diagnostic_fds[0]);
    reader_thread_ = std::thread(&EngineProcess::readLoop, this);

    if(!send(start_message, error))
    {
        terminate();
        return false;
    }
    return true;
}

bool EngineProcess::send(const Json& message, std::string& error)
{
    std::lock_guard lock(state_mutex_);
    if(!channel_ || !running_)
    {
        error = "NetHack worker is not running";
        return false;
    }
    return channel_->send(message, error);
}

void EngineProcess::terminate(bool force)
{
    const pid_t process_id = process_id_;
    if(process_id > 0 && running_)
    {
        ::kill(process_id, force ? SIGKILL : SIGTERM);
    }
    if(channel_)
    {
        channel_->close();
    }
    if(reader_thread_.joinable())
    {
        reader_thread_.join();
    }
    if(diagnostic_thread_.joinable())
    {
        diagnostic_thread_.join();
    }
    channel_.reset();
    process_id_ = -1;
    running_ = false;
}

bool EngineProcess::running() const
{
    return running_;
}

void EngineProcess::readLoop()
{
    Json message;
    std::string error;
    while(channel_ && channel_->receive(message, error))
    {
        if(callback_)
        {
            callback_(message);
        }
    }

    const pid_t process_id = process_id_;
    int status = 0;
    if(process_id > 0)
    {
        while(::waitpid(process_id, &status, 0) < 0 && errno == EINTR)
        {
        }
    }
    running_ = false;
    Json exiting = {
        {"type", "exiting"},
        {"exit_code", WIFEXITED(status) ? WEXITSTATUS(status) : -1},
        {"signal", WIFSIGNALED(status) ? WTERMSIG(status) : 0},
        {"error", error},
    };
    if(callback_)
    {
        callback_(exiting);
    }
}

void EngineProcess::diagnosticLoop(int descriptor)
{
    std::array<char, 4096> buffer{};
    std::size_t logged_bytes = 0;
    while(true)
    {
        const ssize_t count = ::read(descriptor, buffer.data(), buffer.size());
        if(count == 0)
        {
            break;
        }
        if(count < 0)
        {
            if(errno == EINTR)
            {
                continue;
            }
            break;
        }
        const std::size_t remaining = logged_bytes < max_diagnostic_bytes_
            ? max_diagnostic_bytes_ - logged_bytes : 0;
        const std::size_t bytes_to_log = std::min(
            remaining, static_cast<std::size_t>(count));
        if(bytes_to_log > 0)
        {
            std::fwrite(buffer.data(), 1, bytes_to_log, stderr);
            std::fflush(stderr);
            logged_bytes += bytes_to_log;
        }
    }
    ::close(descriptor);
}

std::string EngineProcess::executablePath() const
{
    std::array<char, 4096> buffer{};
    const ssize_t length = ::readlink("/proc/self/exe", buffer.data(),
                                     buffer.size() - 1);
    if(length <= 0)
    {
        throw std::runtime_error("could not resolve the server executable");
    }
    buffer[static_cast<std::size_t>(length)] = '\0';
    return buffer.data();
}

} // namespace nethack_mcp