#include "LocalServer.h" #include "hd2d/Log.h" #include #include #include #include #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #else #include #include #include #include #include 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 args = {server.string(), "--port", std::to_string(port), "--name", name}; std::vector 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