// Copyright Epic Games, Inc. All Rights Reserved. #include "up_cmd.h" #include #include #include #include #include #include #include #include #include namespace zen { UpCommand::UpCommand() { m_Options.add_option("", "p", "port", "Host port", cxxopts::value(m_Port)->default_value("0"), ""); m_Options.add_option("", "b", "base-dir", "Parent folder of server executable", cxxopts::value(m_ProgramBaseDir), ""); m_Options .add_option("", "c", "show-console", "Open a console window for the zenserver process", cxxopts::value(m_ShowConsole), ""); m_Options.add_option("", "l", "show-log", "Show the output log of the zenserver process after successful start", cxxopts::value(m_ShowLog), ""); } UpCommand::~UpCommand() = default; void UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } if (m_ShowConsole && m_ShowLog) { throw OptionParseException("'--show-console' conficts with '--show-log'", m_Options.help()); } { ZenServerState State; if (State.InitializeReadOnly()) { struct EntryInfo { uint32_t Pid = 0; uint16_t DesiredPort = 0; uint16_t EffectivePort = 0; }; std::vector RunningEntries; State.Snapshot([&RunningEntries, DesiredPort = this->m_Port](const zen::ZenServerState::ZenServerEntry& Entry) { if (DesiredPort == 0 || Entry.DesiredListenPort.load() == DesiredPort) { RunningEntries.push_back(EntryInfo{.Pid = Entry.Pid.load(), .DesiredPort = Entry.DesiredListenPort.load(), .EffectivePort = Entry.EffectiveListenPort.load()}); } }); if (RunningEntries.size() > 0) { ZEN_CONSOLE("Zen server already running with base port {}. First instance at port {}, pid {}", RunningEntries[0].DesiredPort, RunningEntries[0].EffectivePort, RunningEntries[0].Pid); return; } } } if (m_ProgramBaseDir.empty()) { std::filesystem::path ExePath = zen::GetRunningExecutablePath(); m_ProgramBaseDir = ExePath.parent_path(); } ZenServerEnvironment ServerEnvironment; ServerEnvironment.Initialize(m_ProgramBaseDir); ZenServerInstance Server(ServerEnvironment); std::string ServerArguments = GlobalOptions.PassthroughCommandLine; if ((m_Port != 0) && (ServerArguments.find("--port"sv) == std::string::npos)) { ServerArguments.append(fmt::format(" --port {}", m_Port)); } Server.SpawnServer(ServerArguments, m_ShowConsole, /*WaitTimeoutMs*/ 0); int Timeout = 10000; if (!Server.WaitUntilReady(Timeout)) { if (Server.IsRunning()) { ZEN_CONSOLE_WARN("Zen server launch failed (timed out), terminating"); Server.Terminate(); if (!m_ShowConsole) { ZEN_CONSOLE("{}", Server.GetLogOutput()); } throw std::runtime_error("Zen server launch failed (timed out), launched process was terminated"); } int ServerExitCode = Server.Shutdown(); if (!m_ShowConsole) { ZEN_CONSOLE("{}", Server.GetLogOutput()); } if (ServerExitCode != 0) { throw ErrorWithReturnCode( fmt::format("Zen server failed to get to a ready state and exited with return code {}", ServerExitCode), ServerExitCode); } } else { if (m_ShowLog) { ZEN_CONSOLE("{}", Server.GetLogOutput()); } else { ZEN_CONSOLE("Zen server up"); } } } ////////////////////////////////////////////////////////////////////////// AttachCommand::AttachCommand() { m_Options.add_option("", "p", "port", "Host port", cxxopts::value(m_Port)->default_value("8558"), ""); m_Options.add_option("lifetime", "", "owner-pid", "Specify owning process id", cxxopts::value(m_OwnerPid), ""); m_Options.add_option("", "", "data-dir", "Path to data directory to inspect for running server", cxxopts::value(m_DataDir), ""); } AttachCommand::~AttachCommand() = default; void AttachCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } ZenServerState Instance; Instance.Initialize(); Instance.Sweep(); ZenServerState::ZenServerEntry* Entry = Instance.Lookup(m_Port); if (!m_DataDir.empty()) { if (!IsFile(m_DataDir / ".lock")) { throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); } CbValidateError ValidateResult = CbValidateError::None; if (CbObject LockFileObject = ValidateAndReadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"), ValidateResult); ValidateResult == CbValidateError::None && LockFileObject) { LockFileInfo Info = ReadLockFilePayload(LockFileObject); std::string Reason; if (!ValidateLockFileInfo(Info, Reason)) { throw std::runtime_error(fmt::format("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason)); } Entry = Instance.LookupByEffectivePort(Info.EffectiveListenPort); } else { throw std::runtime_error( fmt::format("Lock file in directory '{}' is malformed. Reason: '{}'", m_DataDir, ToString(ValidateResult))); } } if (!Entry) { throw std::runtime_error("No zen server instance to add sponsor process to"); } // Sponsor processes are checked every second, so 2 second wait time should be enough if (!Entry->AddSponsorProcess(m_OwnerPid, 2000)) { throw std::runtime_error("Unable to add sponsor process to running zen server instance"); } ZEN_CONSOLE("Added sponsor process {} to running instance {} on port {}", m_OwnerPid, Entry->Pid.load(), m_Port); } ////////////////////////////////////////////////////////////////////////// DownCommand::DownCommand() { m_Options.add_option("", "p", "port", "Host port", cxxopts::value(m_Port)->default_value("0"), ""); m_Options.add_option("", "a", "all", "Shut down all running zen server instances", cxxopts::value(m_All), ""); m_Options.add_option("", "f", "force", "Force terminate if graceful shutdown fails", cxxopts::value(m_ForceTerminate), ""); m_Options.add_option("", "b", "base-dir", "Parent folder of server executable", cxxopts::value(m_ProgramBaseDir), ""); m_Options.add_option("", "", "data-dir", "Path to data directory to inspect for running server", cxxopts::value(m_DataDir), ""); } DownCommand::~DownCommand() = default; bool DownCommand::ShutdownEntry(ZenServerState& Instance, ZenServerState::ZenServerEntry* Entry) { int EntryPort = (int)Entry->DesiredListenPort.load(); const uint32_t ServerProcessPid = Entry->Pid.load(); try { ZenServerEnvironment ServerEnvironment; ServerEnvironment.Initialize(m_ProgramBaseDir); ZenServerInstance Server(ServerEnvironment); Server.AttachToRunningServer(EntryPort); ZEN_CONSOLE("attached to server on port {} (pid {}), requesting shutdown", EntryPort, ServerProcessPid); std::error_code Ec; if (Server.SignalShutdown(Ec) && !Ec) { Stopwatch Timer; while (Timer.GetElapsedTimeMs() < 10000) { if (Server.WaitUntilExited(100, Ec) && !Ec) { ZEN_CONSOLE("shutdown complete"); return true; } else if (Ec) { ZEN_CONSOLE("Waiting for server on port {} (pid {}) failed. Reason: '{}'", EntryPort, ServerProcessPid, Ec.message()); } } } else if (Ec) { ZEN_CONSOLE_WARN("Requesting shutdown of server on port {} failed. Reason: '{}'", EntryPort, Ec.message()); } } catch (const std::exception& Ex) { ZEN_DEBUG("Exception caught when requesting shutdown: {}", Ex.what()); } // Since we cannot obtain a handle to the process we are unable to block on the process // handle to determine when the server has shut down. Thus we signal that we would like // a shutdown via the shutdown flag and the check if the entry is still running. ZEN_CONSOLE("Requesting detached shutdown of server on port {}", EntryPort); Entry->SignalShutdownRequest(); Stopwatch Timer; while (Timer.GetElapsedTimeMs() < 10000) { Instance.Sweep(); Entry = Instance.Lookup(EntryPort); if (Entry == nullptr || Entry->Pid.load() != ServerProcessPid) { ZEN_CONSOLE("Shutdown complete"); return true; } Sleep(100); } return false; } void DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } if (m_ProgramBaseDir.empty()) { std::filesystem::path ExePath = GetRunningExecutablePath(); m_ProgramBaseDir = ExePath.parent_path(); } // Discover executing instances ZenServerState Instance; Instance.Initialize(); if (m_All) { struct EntryInfo { uint16_t Port = 0; uint32_t Pid = 0; }; std::vector Entries; Instance.Snapshot([&Entries](const ZenServerState::ZenServerEntry& Entry) { uint16_t Port = Entry.DesiredListenPort.load(); uint32_t Pid = Entry.Pid.load(); if (Port != 0 && Pid != 0) { Entries.push_back({Port, Pid}); } }); if (Entries.empty()) { ZEN_CONSOLE("No zen server instances to bring down"); return; } int FailCount = 0; for (const EntryInfo& Info : Entries) { Instance.Sweep(); ZenServerState::ZenServerEntry* Entry = Instance.Lookup(Info.Port); if (Entry && Entry->Pid.load() == Info.Pid) { if (!ShutdownEntry(Instance, Entry)) { ZEN_CONSOLE_WARN("Failed to shutdown server on port {} (pid {})", Info.Port, Info.Pid); ++FailCount; } } } if (FailCount > 0 && !m_ForceTerminate) { throw std::runtime_error(fmt::format("Failed to shutdown {} instance(s), use --force to hard terminate", FailCount)); } return; } ZenServerState::ZenServerEntry* Entry = Instance.Lookup(m_Port); if (!m_DataDir.empty()) { if (!IsFile(m_DataDir / ".lock")) { throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); } CbValidateError ValidateResult = CbValidateError::None; if (CbObject LockFileObject = ValidateAndReadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"), ValidateResult); ValidateResult == CbValidateError::None && LockFileObject) { LockFileInfo Info = ReadLockFilePayload(LockFileObject); std::string Reason; if (!ValidateLockFileInfo(Info, Reason)) { throw std::runtime_error(fmt::format("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason)); } Entry = Instance.LookupByEffectivePort(Info.EffectiveListenPort); } else { throw std::runtime_error( fmt::format("Lock file in directory '{}' is malformed. Reason: '{}'", m_DataDir, ToString(ValidateResult))); } } if (Entry) { if (ShutdownEntry(Instance, Entry)) { return; } } if (m_ForceTerminate) { // Try to find the running executable by path name std::filesystem::path ServerExePath = m_ProgramBaseDir / "zenserver" ZEN_EXE_SUFFIX_LITERAL; ProcessHandle RunningProcess; if (std::error_code Ec = FindProcess(ServerExePath, RunningProcess); !Ec, /*IncludeSelf*/ false) { ZEN_CONSOLE_WARN("Attempting hard terminate of zen process with pid ({})", RunningProcess.Pid()); if (RunningProcess.Terminate(0)) { ZEN_CONSOLE("Terminate complete"); return; } throw std::runtime_error("Failed to terminate server, still running"); } else { ZEN_CONSOLE_WARN("Failed to find process '{}', reason: {}", ServerExePath.string(), Ec.message()); } } else if (Entry) { throw std::runtime_error( fmt::format("Failed to shutdown of server on port {}, use --force to hard terminate process", Entry->DesiredListenPort.load())); } ZEN_CONSOLE("No zen server to bring down"); } } // namespace zen