From 015db1c80865eabd28f98378610a9eca6798a7d4 Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Mon, 20 Apr 2026 14:04:57 +0200 Subject: add --pid och --executable till zen down command (#988) --- src/zen/cmds/up_cmd.cpp | 151 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 12 deletions(-) (limited to 'src/zen/cmds/up_cmd.cpp') diff --git a/src/zen/cmds/up_cmd.cpp b/src/zen/cmds/up_cmd.cpp index 809a41bb6..f42da4683 100644 --- a/src/zen/cmds/up_cmd.cpp +++ b/src/zen/cmds/up_cmd.cpp @@ -2,6 +2,7 @@ #include "up_cmd.h" +#include #include #include #include @@ -12,6 +13,57 @@ 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"), ""); @@ -40,6 +92,12 @@ UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) 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, @@ -80,6 +138,11 @@ AttachCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) return; } + if (!m_DataDir.empty()) + { + MakeSafeAbsolutePathInPlace(m_DataDir); + } + ZenServerState Instance; Instance.Initialize(); Instance.Sweep(); @@ -87,9 +150,9 @@ AttachCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!m_DataDir.empty()) { - if (!IsFile(m_DataDir / ".lock")) + if (!LockFile::IsHeldLive(m_DataDir / ".lock", /*AttemptCleanup*/ false)) { - throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); + throw std::runtime_error(fmt::format("No live zen server holding lock file in directory '{}'", m_DataDir)); } CbValidateError ValidateResult = CbValidateError::None; if (CbObject LockFileObject = @@ -134,6 +197,13 @@ DownCommand::DownCommand() 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; @@ -148,16 +218,77 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** 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 @@ -184,15 +315,10 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) 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 (!TryShutdownByPid(Instance, Info.Pid, m_ProgramBaseDir, m_ForceTerminate)) { - if (!ShutdownZenServer(ConsoleLog(), Instance, Entry, m_ProgramBaseDir)) - { - ZEN_CONSOLE_WARN("Failed to shutdown server on port {} (pid {})", Info.Port, Info.Pid); - ++FailCount; - } + ZEN_CONSOLE_WARN("Failed to shutdown server on port {} (pid {})", Info.Port, Info.Pid); + ++FailCount; } } @@ -207,9 +333,10 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!m_DataDir.empty()) { - if (!IsFile(m_DataDir / ".lock")) + if (!LockFile::IsHeldLive(m_DataDir / ".lock", /*AttemptCleanup*/ true)) { - throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); + ZEN_CONSOLE("No live zen server holding lock file in '{}', nothing to do", m_DataDir); + return; } CbValidateError ValidateResult = CbValidateError::None; if (CbObject LockFileObject = -- cgit v1.2.3 From 28a61b12d302e9e0d37d52bf1aa5d19069f3411b Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Mon, 20 Apr 2026 15:53:22 +0200 Subject: zen history command (#987) - Feature: Per-user invocation history for `zen` and `zenserver`; each startup appends a record to a JSONL file capped at the most recent 100 entries. Location: `%LOCALAPPDATA%\Epic\Zen\History\invocations.jsonl` on Windows, `~/.zen/History/invocations.jsonl` on POSIX - `zen history` opens an interactive picker; selecting a zen row re-runs it inline and forwards the exit code, selecting a zenserver row spawns it detached - `zen history --list` (`-l`) prints the table to stdout instead of showing the picker - `zen history --filter zen|zenserver` restricts the listing to one executable - `zen history --print` prints the reconstructed command line of the selected row instead of launching it - `--enable-execution-history` global option on both binaries (default `true`) to opt out per invocation - The history file is attached to Sentry crash reports (alongside the existing zenserver log) --- src/zen/cmds/up_cmd.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/zen/cmds/up_cmd.cpp') diff --git a/src/zen/cmds/up_cmd.cpp b/src/zen/cmds/up_cmd.cpp index f42da4683..1f23e6819 100644 --- a/src/zen/cmds/up_cmd.cpp +++ b/src/zen/cmds/up_cmd.cpp @@ -99,11 +99,12 @@ UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) MakeSafeAbsolutePathInPlace(m_ProgramBaseDir); std::optional StartResult = StartupZenServer(ConsoleLog(), - {.ProgramBaseDir = m_ProgramBaseDir, - .Port = m_Port, - .OpenConsole = m_ShowConsole, - .ShowLog = m_ShowLog, - .ExtraArgs = GlobalOptions.PassthroughCommandLine}); + {.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"); -- cgit v1.2.3