diff --git a/dCaptureTool/Sandbox.cpp b/dCaptureTool/Sandbox.cpp index c0e9e1511..8d1546c93 100644 --- a/dCaptureTool/Sandbox.cpp +++ b/dCaptureTool/Sandbox.cpp @@ -3,13 +3,19 @@ #include #include #include -#include #include #include #include -#include #include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include #include +#endif #include "BinaryPathFinder.h" #include "CaptureBundle.h" @@ -28,6 +34,46 @@ namespace fs = std::filesystem; using json = nlohmann::json; namespace { + // Settings can come from the environment too (dConfig): never in a sandbox. Cleared in the tool itself, so every + // server it starts goes without them. + void ClearOverrides() { + for (const auto* key : { "REPLAY_SANDBOX", "DATABASE_TYPE", "SQLITE_DATABASE_PATH", "MASTER_SERVER_PORT", "WORLD_PORT_START", "AUTH_SERVER_PORT", + "CHAT_SERVER_PORT", "MASTER_IP", "EXTERNAL_IP", "CLIENT_LOCATION", "MYSQL_HOST", "MYSQL_DATABASE", "DLU_CONFIG_DIR" }) { +#ifdef _WIN32 + _putenv_s(key, ""); +#else + unsetenv(key); +#endif + } + } + + // A program's file name on this platform + fs::path Program(const char* name) { +#ifdef _WIN32 + return std::string(name) + ".exe"; +#else + return name; +#endif + } + + uint64_t ProcessId() { +#ifdef _WIN32 + return GetCurrentProcessId(); +#else + return static_cast(getpid()); +#endif + } + +#ifdef _WIN32 + // The running stack: a job object, so master and every server it starts stop together (also when the tool dies) + HANDLE g_Job = nullptr; + + void StopOnSignal(int signal) { + if (g_Job) TerminateJobObject(g_Job, 1); + std::signal(signal, SIG_DFL); + std::raise(signal); + } +#else // The running stack's process group: stopped too if the tool is interrupted or crashes volatile sig_atomic_t g_Running = 0; @@ -36,6 +82,7 @@ namespace { std::signal(signal, SIG_DFL); std::raise(signal); } +#endif /** * Settings every sandbox gets over the server's own files. The first value of a key counts, so each key's line @@ -67,14 +114,48 @@ namespace { std::ofstream(file, std::ios::trunc) << out; } - pid_t Launch(const fs::path& dir, const fs::path& program, const fs::path& output, const std::vector& args = {}) { + /** + * Starts `program` in `dir` with its output appended to `output`. With `group`, it leads a group of its own (the + * stack: master and what it starts), which Stack::Stop ends as one. Returns 0 when it couldn't start. + */ + Sandbox::ProcessHandle Launch(const fs::path& dir, const fs::path& program, const fs::path& output, const std::vector& args, bool group) { +#ifdef _WIN32 + std::string command = "\"" + program.string() + "\""; + for (const auto& arg : args) { + command += " \""; + for (const char c : arg) if (c != '"') command += c; + command += "\""; + } + SECURITY_ATTRIBUTES inherit{ sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE }; + HANDLE log = CreateFileW(output.wstring().c_str(), FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, &inherit, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + STARTUPINFOA startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = nullptr; + startup.hStdOutput = startup.hStdError = log; + PROCESS_INFORMATION info{}; + const auto dirText = dir.string(); + const BOOL started = CreateProcessA(nullptr, command.data(), nullptr, nullptr, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW, nullptr, dirText.c_str(), &startup, &info); + if (log != INVALID_HANDLE_VALUE) CloseHandle(log); + if (!started) return 0; + if (group) { + if (!g_Job) { + g_Job = CreateJobObjectA(nullptr, nullptr); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject(g_Job, JobObjectExtendedLimitInformation, &limits, sizeof(limits)); + } + AssignProcessToJobObject(g_Job, info.hProcess); + } + ResumeThread(info.hThread); + CloseHandle(info.hThread); + return reinterpret_cast(info.hProcess); +#else const pid_t pid = fork(); + if (pid < 0) return 0; if (pid != 0) return pid; - // The child: its own process group, so the whole stack (master and what it starts) stops together - setpgid(0, 0); - // Settings can come from the environment too (dConfig): not in a sandbox - for (const auto* key : { "REPLAY_SANDBOX", "DATABASE_TYPE", "SQLITE_DATABASE_PATH", "MASTER_SERVER_PORT", "WORLD_PORT_START", "AUTH_SERVER_PORT", - "CHAT_SERVER_PORT", "MASTER_IP", "EXTERNAL_IP", "CLIENT_LOCATION", "MYSQL_HOST", "MYSQL_DATABASE", "DLU_CONFIG_DIR" }) unsetenv(key); + // The child: with `group`, its own process group, so the whole stack (master and what it starts) stops together + if (group) setpgid(0, 0); if (chdir(dir.c_str()) != 0) _exit(127); const int fd = open(output.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd >= 0) { @@ -87,6 +168,33 @@ namespace { argv.push_back(nullptr); execv(program.c_str(), argv.data()); _exit(127); +#endif + } + + // Whether it ended; its exit code in `code` (-1: killed) + bool Ended(Sandbox::ProcessHandle process, bool wait, int& code) { +#ifdef _WIN32 + const auto handle = reinterpret_cast(process); + if (WaitForSingleObject(handle, wait ? INFINITE : 0) != WAIT_OBJECT_0) return false; + DWORD exit = 0; + GetExitCodeProcess(handle, &exit); + code = static_cast(exit); + CloseHandle(handle); + return true; +#else + int status = 0; + if (waitpid(static_cast(process), &status, wait ? 0 : WNOHANG) != static_cast(process)) return false; + code = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + return true; +#endif + } + + // A link to something shared with the built servers; a copy where links can't be made (Windows without the right) + void Share(const fs::path& from, const fs::path& to) { + std::error_code ec; + if (fs::is_directory(from)) fs::create_directory_symlink(from, to, ec); + else fs::create_symlink(from, to, ec); + if (ec) fs::copy(from, to, fs::copy_options::recursive, ec); } } @@ -96,7 +204,8 @@ namespace Sandbox { stack->m_Options = options; stack->m_Keep = options.keep; std::error_code ec; - const auto stamp = std::to_string(std::chrono::system_clock::now().time_since_epoch().count() / 1000000) + "-" + std::to_string(getpid()); + const auto stamp = std::to_string(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + "-" + + std::to_string(ProcessId()); stack->m_Dir = fs::absolute(options.root / ("sandbox-" + stamp)); const auto& dir = stack->m_Dir; if (!fs::create_directories(dir / "resServer", ec) || !fs::create_directories(dir / "logs", ec)) { @@ -105,15 +214,15 @@ namespace Sandbox { } // Binaries are copied: they find their settings and database next to themselves for (const auto* name : { "MasterServer", "AuthServer", "ChatServer", "WorldServer" }) { - fs::copy_file(options.serverDir / name, dir / name, ec); + fs::copy_file(options.serverDir / Program(name), dir / Program(name), ec); if (ec) { - error = "Can't copy " + (options.serverDir / name).string() + ": " + ec.message(); + error = "Can't copy " + (options.serverDir / Program(name)).string() + ": " + ec.message(); return nullptr; } } - fs::copy_file(BinaryPathFinder::GetBinaryDir() / "CaptureTool", dir / "CaptureTool", ec); - for (const auto* name : { "migrations", "navmeshes", "vanity", "blocklist.dcf", "libmariadbcpp.so" }) { - if (fs::exists(options.serverDir / name)) fs::create_symlink(fs::absolute(options.serverDir / name), dir / name, ec); + fs::copy_file(BinaryPathFinder::GetBinaryDir() / Program("CaptureTool"), dir / Program("CaptureTool"), ec); + for (const auto* name : { "migrations", "navmeshes", "vanity", "blocklist.dcf", "libmariadbcpp.so", "libmariadbcpp.dylib", "mariadbcpp.dll", "plugin" }) { + if (fs::exists(options.serverDir / name)) Share(fs::absolute(options.serverDir / name), dir / name); } for (const auto* name : { "sharedconfig.ini", "masterconfig.ini", "authconfig.ini", "chatconfig.ini", "worldconfig.ini" }) { fs::copy_file(options.serverDir / name, dir / name, ec); @@ -172,11 +281,11 @@ namespace Sandbox { bool Stack::Setup(const fs::path& bundle, const std::string& username, const std::string& password, bool characters, json& ids, std::string& error) { const auto out = m_Dir / "setup.json"; - const pid_t pid = Launch(m_Dir, m_Dir / "CaptureTool", m_Dir / "logs" / "setup.out", - { "sandbox-setup", fs::absolute(bundle).string(), username, password, characters ? "1" : "0", out.string() }); - int status = 0; - waitpid(pid, &status, 0); - if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ClearOverrides(); + const auto process = Launch(m_Dir, m_Dir / Program("CaptureTool"), m_Dir / "logs" / "setup.out", + { "sandbox-setup", fs::absolute(bundle).string(), username, password, characters ? "1" : "0", out.string() }, false); + int code = -1; + if (!process || !Ended(process, true, code) || code != 0) { error = "Setting up the sandbox failed (see " + (m_Dir / "logs" / "setup.out").string() + ")"; return false; } @@ -191,13 +300,20 @@ namespace Sandbox { bool Stack::Start(std::string& error) { for (const int signal : { SIGINT, SIGTERM, SIGSEGV, SIGABRT }) std::signal(signal, StopOnSignal); - m_Master = Launch(m_Dir, m_Dir / "MasterServer", m_Dir / "logs" / "master.out"); - g_Running = m_Master; + ClearOverrides(); + m_Master = Launch(m_Dir, m_Dir / Program("MasterServer"), m_Dir / "logs" / "master.out", {}, true); + if (!m_Master) { + error = "Couldn't start the sandbox's master server"; + return false; + } +#ifndef _WIN32 + g_Running = static_cast(m_Master); +#endif // Auth answering is the sign the stack is up (master starts it after chat and the character select world) const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(120); while (std::chrono::steady_clock::now() < until) { - int status = 0; - if (waitpid(m_Master, &status, WNOHANG) == m_Master) { + int code = 0; + if (Ended(m_Master, false, code)) { m_Master = 0; error = "The sandbox's master server stopped (see " + (m_Dir / "logs").string() + ")"; return false; @@ -216,15 +332,28 @@ namespace Sandbox { } void Stack::Stop() { - if (m_Master <= 0) return; - kill(-m_Master, SIGTERM); + if (!m_Master) return; +#ifdef _WIN32 + // Servers have no console to be asked to stop through: end the job (master and every server it started) + if (g_Job) { + TerminateJobObject(g_Job, 0); + CloseHandle(g_Job); + g_Job = nullptr; + } + int code = 0; + Ended(m_Master, true, code); +#else + const auto group = static_cast(m_Master); + kill(-group, SIGTERM); const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(15); - int status = 0; - while (waitpid(m_Master, &status, WNOHANG) == 0 && std::chrono::steady_clock::now() < until) std::this_thread::sleep_for(std::chrono::milliseconds(100)); - kill(-m_Master, SIGKILL); - waitpid(m_Master, &status, 0); - m_Master = 0; + int code = 0; + bool ended = false; + while (!(ended = Ended(m_Master, false, code)) && std::chrono::steady_clock::now() < until) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + kill(-group, SIGKILL); + if (!ended) Ended(m_Master, true, code); g_Running = 0; +#endif + m_Master = 0; } int SetupCommand(const fs::path& bundlePath, const std::string& username, const std::string& password, bool characters, const fs::path& out) { diff --git a/dCaptureTool/Sandbox.h b/dCaptureTool/Sandbox.h index 359fcc428..fc156d374 100644 --- a/dCaptureTool/Sandbox.h +++ b/dCaptureTool/Sandbox.h @@ -4,7 +4,6 @@ #include #include #include -#include #include "json.hpp" @@ -16,6 +15,9 @@ * read-only; CDServer.sqlite is copied. The folder is deleted afterwards unless it is kept. */ namespace Sandbox { + // A started process: its pid, or its process handle on Windows (0: none) + using ProcessHandle = intptr_t; + struct Options { std::filesystem::path serverDir; // built servers (MasterServer, AuthServer, ChatServer, WorldServer, migrations, ...) std::filesystem::path root; // sandboxes are made in here @@ -47,7 +49,7 @@ namespace Sandbox { private: Options m_Options; std::filesystem::path m_Dir; - pid_t m_Master{}; + ProcessHandle m_Master{}; bool m_Keep{}; };