#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