// Copyright Epic Games, Inc. All Rights Reserved. #include "up_cmd.h" #include #include #include #include #include #include #include #include namespace zen { namespace { bool TryShutdownByPid(ZenServerState& Instance, uint32_t Pid, const std::filesystem::path& ProgramBaseDir, bool ForceTerminate) { Instance.Sweep(); uint16_t DesiredPort = 0; Instance.Snapshot([&](const ZenServerState::ZenServerEntry& Entry) { if (Entry.Pid.load() == Pid) { DesiredPort = Entry.DesiredListenPort.load(); } }); ZenServerState::ZenServerEntry* Entry = (DesiredPort != 0) ? Instance.Lookup(DesiredPort) : nullptr; if (Entry && Entry->Pid.load() != Pid) { Entry = nullptr; } if (Entry) { if (ShutdownZenServer(ConsoleLog(), Instance, Entry, ProgramBaseDir)) { return true; } } std::error_code Ec; ProcessHandle Proc; Proc.Initialize(int(Pid), Ec); if (!Ec && Proc.IsValid() && !Proc.IsRunning()) { return true; } if (ForceTerminate && !Ec && Proc.IsValid() && Proc.IsRunning()) { ZEN_CONSOLE_WARN("Hard terminating zen process with pid ({})", Pid); if (Proc.Terminate(0)) { ZEN_CONSOLE("Terminate complete"); return true; } } return false; } } // namespace 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) { if (!ParseOptions(argc, argv)) { return; } if (m_ShowConsole && m_ShowLog) { throw OptionParseException("'--show-console' conflicts with '--show-log'", m_Options.help()); } if (m_ProgramBaseDir.empty()) { m_ProgramBaseDir = GetRunningExecutablePath().parent_path(); } MakeSafeAbsolutePathInPlace(m_ProgramBaseDir); std::optional StartResult = StartupZenServer(ConsoleLog(), {.ProgramBaseDir = m_ProgramBaseDir, .Port = m_Port, .OpenConsole = m_ShowConsole, .ShowLog = m_ShowLog, .ExtraArgs = GlobalOptions.PassthroughCommandLine, .EnableExecutionHistory = GlobalOptions.EnableExecutionHistory}); if (!StartResult.has_value()) { ZEN_CONSOLE("Zen server already running"); return; } if (*StartResult != 0) { throw ErrorWithReturnCode("Zen server failed to start", *StartResult); } 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; } if (!m_DataDir.empty()) { MakeSafeAbsolutePathInPlace(m_DataDir); } ZenServerState Instance; Instance.Initialize(); Instance.Sweep(); ZenServerState::ZenServerEntry* Entry = Instance.Lookup(m_Port); if (!m_DataDir.empty()) { if (!LockFile::IsHeldLive(m_DataDir / ".lock", /*AttemptCleanup*/ false)) { throw std::runtime_error(fmt::format("No live zen server holding lock file 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), ""); m_Options.add_option("", "", "pid", "Shut down zen server process by PID", cxxopts::value(m_Pid)->default_value("0"), ""); m_Options.add_option("", "", "executable", "Shut down all zen server processes matching executable path", cxxopts::value(m_ExecutablePath), ""); } DownCommand::~DownCommand() = default; void DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } const bool HasPid = m_Pid != 0; const bool HasExecutable = !m_ExecutablePath.empty(); const bool HasDataDir = !m_DataDir.empty(); const int SelectorCount = int(m_All) + int(HasPid) + int(HasExecutable) + int(HasDataDir); if (SelectorCount > 1) { throw OptionParseException("--all, --pid, --executable, and --data-dir are mutually exclusive", m_Options.help()); } if (m_ProgramBaseDir.empty()) { std::filesystem::path ExePath = GetRunningExecutablePath(); m_ProgramBaseDir = ExePath.parent_path(); } MakeSafeAbsolutePathInPlace(m_ProgramBaseDir); if (!m_DataDir.empty()) { MakeSafeAbsolutePathInPlace(m_DataDir); } if (!m_ExecutablePath.empty() && m_ExecutablePath.has_parent_path()) { MakeSafeAbsolutePathInPlace(m_ExecutablePath); } // Discover executing instances ZenServerState Instance; Instance.Initialize(); if (HasPid) { if (!TryShutdownByPid(Instance, m_Pid, m_ProgramBaseDir, m_ForceTerminate)) { throw std::runtime_error(fmt::format("Failed to shut down zen process with pid {}, use --force to hard terminate", m_Pid)); } ZEN_CONSOLE("Zen server with pid {} is down", m_Pid); return; } if (HasExecutable) { int ShutdownCount = 0; while (true) { ProcessHandle Proc; std::error_code Ec = FindProcess(m_ExecutablePath, Proc, /*IncludeSelf*/ false); if (Ec) { throw std::system_error(Ec, fmt::format("FindProcess failed for '{}'", m_ExecutablePath)); } if (!Proc.IsValid()) { break; } const uint32_t Pid = uint32_t(Proc.Pid()); if (!TryShutdownByPid(Instance, Pid, m_ProgramBaseDir, m_ForceTerminate)) { throw std::runtime_error(fmt::format("Failed to shut down zen process with pid {}, use --force to hard terminate", Pid)); } ++ShutdownCount; } if (ShutdownCount == 0) { ZEN_CONSOLE("No zen server processes matching executable '{}'", m_ExecutablePath); } else { ZEN_CONSOLE("Shut down {} zen server instance(s) matching executable '{}'", ShutdownCount, m_ExecutablePath); } return; } 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) { if (!TryShutdownByPid(Instance, Info.Pid, m_ProgramBaseDir, m_ForceTerminate)) { 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 (!LockFile::IsHeldLive(m_DataDir / ".lock", /*AttemptCleanup*/ true)) { ZEN_CONSOLE("No live zen server holding lock file in '{}', nothing to do", m_DataDir); return; } 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 (ShutdownZenServer(ConsoleLog(), Instance, Entry, m_ProgramBaseDir)) { 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, /*IncludeSelf*/ false); !Ec) { 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 shut down server on port {}, use --force to hard terminate process", Entry->DesiredListenPort.load())); } ZEN_CONSOLE("No zen server to bring down"); } } // namespace zen