Windows support: portable local server launch + clean quit, %APPDATA% data dir, Win32 libs, MinGW/MSVC build docs

This commit is contained in:
Gabe
2026-10-02 15:44:12 -05:00
parent 238cd9c6c3
commit 34fa21139f
8 changed files with 326 additions and 53 deletions

195
client/LocalServer.cpp Normal file
View File

@@ -0,0 +1,195 @@
#include "LocalServer.h"
#include "hd2d/Log.h"
#include <chrono>
#include <cstdlib>
#include <thread>
#include <vector>
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <fcntl.h>
#include <signal.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
extern char** environ;
#endif
namespace bs {
namespace fs = std::filesystem;
fs::path userDataDir() {
fs::path base;
#if defined(_WIN32)
if (const char* a = std::getenv("APPDATA"); a && *a) base = a;
#else
if (const char* x = std::getenv("XDG_DATA_HOME"); x && *x) base = x;
else if (const char* h = std::getenv("HOME"); h && *h) base = fs::path(h) / ".local/share";
#endif
if (base.empty()) base = ".";
fs::path d = base / "brandling-safari";
std::error_code ec;
fs::create_directories(d, ec);
return d;
}
fs::path executableDir() {
#if defined(_WIN32)
std::wstring buf(32768, L'\0');
DWORD n = GetModuleFileNameW(nullptr, buf.data(), DWORD(buf.size()));
buf.resize(n);
return fs::path(buf).parent_path();
#else
std::error_code ec;
fs::path exe = fs::read_symlink("/proc/self/exe", ec);
return ec ? fs::current_path() : exe.parent_path();
#endif
}
#if defined(_WIN32)
namespace {
std::wstring quoteArg(const std::wstring& a) {
std::wstring out = L"\"";
for (wchar_t c : a) {
if (c == L'"') out += L'\\';
out += c;
}
return out + L"\"";
}
std::wstring widen(const std::string& s) {
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), int(s.size()), nullptr, 0);
std::wstring w(size_t(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.data(), int(s.size()), w.data(), n);
return w;
}
} // namespace
bool LocalServer::start(uint16_t port, const std::string& name, std::string& error) {
if (running()) return true;
fs::path server = executableDir() / "safari_server.exe";
if (!fs::exists(server)) {
error = "safari_server.exe was not found next to the game";
return false;
}
SECURITY_ATTRIBUTES sa{sizeof sa, nullptr, TRUE};
HANDLE readEnd = nullptr, writeEnd = nullptr;
if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) {
error = "could not create a pipe for the local server";
return false;
}
SetHandleInformation(writeEnd, HANDLE_FLAG_INHERIT, 0); // only the read end goes to the child
HANDLE log = CreateFileW((userDataDir() / "server.log").wstring().c_str(), GENERIC_WRITE, FILE_SHARE_READ, &sa,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
STARTUPINFOW si{};
si.cb = sizeof si;
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = readEnd;
si.hStdOutput = log != INVALID_HANDLE_VALUE ? log : nullptr;
si.hStdError = si.hStdOutput;
PROCESS_INFORMATION pi{};
std::wstring cmd = quoteArg(server.wstring()) + L" --port " + std::to_wstring(port) + L" --name " +
quoteArg(widen(name));
BOOL ok = CreateProcessW(server.wstring().c_str(), cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr,
nullptr, &si, &pi);
CloseHandle(readEnd);
if (log != INVALID_HANDLE_VALUE) CloseHandle(log);
if (!ok) {
CloseHandle(writeEnd);
error = "could not start the local server";
return false;
}
CloseHandle(pi.hThread);
process_ = pi.hProcess;
stdin_ = writeEnd;
HD2D_LOG("started local server (pid %lu) on port %u", pi.dwProcessId, unsigned(port));
return true;
}
void LocalServer::stop() {
if (!process_) return;
DWORD written = 0;
WriteFile(stdin_, "quit\n", 5, &written, nullptr);
CloseHandle(stdin_);
if (WaitForSingleObject(process_, 5000) != WAIT_OBJECT_0) TerminateProcess(process_, 1);
CloseHandle(process_);
process_ = stdin_ = nullptr;
}
bool LocalServer::running() const { return process_ && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT; }
#else
bool LocalServer::start(uint16_t port, const std::string& name, std::string& error) {
if (running()) return true;
fs::path server = executableDir() / "safari_server";
if (!fs::exists(server)) {
error = "safari_server was not found next to the game";
return false;
}
int fds[2];
if (pipe(fds) != 0) {
error = "could not create a pipe for the local server";
return false;
}
fcntl(fds[1], F_SETFD, FD_CLOEXEC);
std::string log = (userDataDir() / "server.log").string();
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
posix_spawn_file_actions_adddup2(&fa, fds[0], 0);
posix_spawn_file_actions_addopen(&fa, 1, log.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
posix_spawn_file_actions_adddup2(&fa, 1, 2);
std::vector<std::string> args = {server.string(), "--port", std::to_string(port), "--name", name};
std::vector<char*> argv;
for (std::string& a : args) argv.push_back(a.data());
argv.push_back(nullptr);
pid_t pid = 0;
int rc = posix_spawn(&pid, server.c_str(), &fa, nullptr, argv.data(), environ);
posix_spawn_file_actions_destroy(&fa);
close(fds[0]);
if (rc != 0) {
close(fds[1]);
error = "could not start the local server";
return false;
}
signal(SIGPIPE, SIG_IGN); // writing `quit` to a server that already exited must not kill us
pid_ = int(pid);
stdin_ = fds[1];
HD2D_LOG("started local server (pid %d) on port %u", pid_, unsigned(port));
return true;
}
void LocalServer::stop() {
if (pid_ <= 0) return;
if (write(stdin_, "quit\n", 5) < 0) {
}
close(stdin_);
int status = 0;
bool exited = false;
for (int i = 0; i < 50 && !exited; ++i) {
exited = waitpid(pid_t(pid_), &status, WNOHANG) != 0;
if (!exited) std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (!exited) {
kill(pid_t(pid_), SIGKILL);
waitpid(pid_t(pid_), &status, 0);
}
pid_ = stdin_ = -1;
}
bool LocalServer::running() const { return pid_ > 0 && waitpid(pid_t(pid_), nullptr, WNOHANG) == 0; }
#endif
} // namespace bs