From 5bcf03ec7fcc412c2259f341f6c7f3f86631466b Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Thu, 9 Apr 2026 13:26:08 +0200 Subject: Fix ZenServerState stale entry detection on PID reuse (k8s) (#932) - Detect stale shared-memory entries whose PID matches the current process but predate our registration (m_OurEntry == nullptr) - Sweep() now reclaims such entries instead of skipping them - Lookup() and LookupByEffectivePort() skip stale same-PID entries - Fixes startup failure on k8s where PID 1 is always reused after an unclean shutdown --- src/zenutil/zenserverprocess.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'src/zenutil/zenserverprocess.cpp') diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index 2b27b2d8b..9a282a848 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -267,6 +267,8 @@ ZenServerState::InitializeReadOnly() ZenServerState::ZenServerEntry* ZenServerState::Lookup(int DesiredListenPort) const { + const uint32_t OurPid = GetCurrentProcessId(); + for (int i = 0; i < m_MaxEntryCount; ++i) { uint16_t EntryPort = m_Data[i].DesiredListenPort; @@ -274,6 +276,14 @@ ZenServerState::Lookup(int DesiredListenPort) const { if (DesiredListenPort == 0 || (EntryPort == DesiredListenPort)) { + // If the entry's PID matches our own but we haven't registered yet, + // this is a stale entry from a previous process incarnation (e.g. PID 1 + // reuse after unclean shutdown in k8s). Skip it. + if (m_Data[i].Pid == OurPid && m_OurEntry == nullptr) + { + continue; + } + std::error_code _; if (IsProcessRunning(m_Data[i].Pid, _)) { @@ -289,6 +299,8 @@ ZenServerState::Lookup(int DesiredListenPort) const ZenServerState::ZenServerEntry* ZenServerState::LookupByEffectivePort(int Port) const { + const uint32_t OurPid = GetCurrentProcessId(); + for (int i = 0; i < m_MaxEntryCount; ++i) { uint16_t EntryPort = m_Data[i].EffectiveListenPort; @@ -296,6 +308,11 @@ ZenServerState::LookupByEffectivePort(int Port) const { if (EntryPort == Port) { + if (m_Data[i].Pid == OurPid && m_OurEntry == nullptr) + { + continue; + } + std::error_code _; if (IsProcessRunning(m_Data[i].Pid, _)) { @@ -358,12 +375,26 @@ ZenServerState::Sweep() ZEN_ASSERT(m_IsReadOnly == false); + const uint32_t OurPid = GetCurrentProcessId(); + for (int i = 0; i < m_MaxEntryCount; ++i) { ZenServerEntry& Entry = m_Data[i]; if (Entry.DesiredListenPort) { + // If the entry's PID matches our own but we haven't registered yet, + // this is a stale entry from a previous process incarnation (e.g. PID 1 + // reuse after unclean shutdown in k8s). Reclaim it. + if (Entry.Pid == OurPid && m_OurEntry == nullptr) + { + ZEN_CONSOLE_DEBUG("Sweep - pid {} matches current process but no registration yet, reclaiming stale entry (port {})", + Entry.Pid.load(), + Entry.DesiredListenPort.load()); + Entry.Reset(); + continue; + } + std::error_code ErrorCode; if (Entry.Pid != 0 && IsProcessRunning(Entry.Pid, ErrorCode) == false) { -- cgit v1.2.3 From e7d3065cf47c9d8430be409a0c53422aea2e3532 Mon Sep 17 00:00:00 2001 From: zousar Date: Mon, 13 Apr 2026 14:08:09 -0600 Subject: Stop using O_CLOEXEC in shm_open --- src/zenutil/zenserverprocess.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src/zenutil/zenserverprocess.cpp') diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index 9a282a848..20208e136 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -181,7 +181,7 @@ ZenServerState::Initialize() ThrowLastError("Could not map view of Zen server state"); } #else - int Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT | O_CLOEXEC, geteuid() == 0 ? 0766 : 0666); + int Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT, geteuid() == 0 ? 0766 : 0666); if (Fd < 0) { // Work around a potential issue if the service user is changed in certain configurations. @@ -191,12 +191,13 @@ ZenServerState::Initialize() // shared memory object and retry, we'll be able to get past shm_open() so long as we have // the appropriate permissions to create the shared memory object. shm_unlink("/UnrealEngineZen"); - Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT | O_CLOEXEC, geteuid() == 0 ? 0766 : 0666); + Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT, geteuid() == 0 ? 0766 : 0666); if (Fd < 0) { ThrowLastError("Could not open a shared memory object"); } } + fcntl(FdGuard.Fd, F_SETFD, FD_CLOEXEC); fchmod(Fd, 0666); void* hMap = (void*)intptr_t(Fd); @@ -244,11 +245,12 @@ ZenServerState::InitializeReadOnly() ThrowLastError("Could not map view of Zen server state"); } #else - int Fd = shm_open("/UnrealEngineZen", O_RDONLY | O_CLOEXEC, 0666); + int Fd = shm_open("/UnrealEngineZen", O_RDONLY, 0666); if (Fd < 0) { return false; } + fcntl(Fd, F_SETFD, FD_CLOEXEC); void* hMap = (void*)intptr_t(Fd); void* pBuf = mmap(nullptr, MapSize, PROT_READ, MAP_SHARED, Fd, 0); @@ -651,11 +653,12 @@ ZenServerInstanceInfo::Create(const Oid& SessionId, const InstanceInfoData& Data ThrowLastError("Could not map instance info shared memory"); } #else - int Fd = shm_open(Name.c_str(), O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0666); + int Fd = shm_open(Name.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666); if (Fd < 0) { ThrowLastError("Could not create instance info shared memory"); } + fcntl(FdGuard.Fd, F_SETFD, FD_CLOEXEC); fchmod(Fd, 0666); if (ftruncate(Fd, kInstanceInfoSize) < 0) @@ -718,11 +721,12 @@ ZenServerInstanceInfo::OpenReadOnly(const Oid& SessionId) return false; } #else - int Fd = shm_open(Name.c_str(), O_RDONLY | O_CLOEXEC, 0666); + int Fd = shm_open(Name.c_str(), O_RDONLY, 0666); if (Fd < 0) { return false; } + fcntl(FdGuard.Fd, F_SETFD, FD_CLOEXEC); void* pBuf = mmap(nullptr, kInstanceInfoSize, PROT_READ, MAP_SHARED, Fd, 0); if (pBuf == MAP_FAILED) -- cgit v1.2.3 From f2eb4ff5ec5446b4d01d67e89976728d88be598e Mon Sep 17 00:00:00 2001 From: zousar Date: Mon, 13 Apr 2026 14:24:11 -0600 Subject: Fix copy and paste errors --- src/zenutil/zenserverprocess.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/zenutil/zenserverprocess.cpp') diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index 20208e136..25e9cfa5d 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -197,7 +197,7 @@ ZenServerState::Initialize() ThrowLastError("Could not open a shared memory object"); } } - fcntl(FdGuard.Fd, F_SETFD, FD_CLOEXEC); + fcntl(Fd, F_SETFD, FD_CLOEXEC); fchmod(Fd, 0666); void* hMap = (void*)intptr_t(Fd); @@ -658,7 +658,7 @@ ZenServerInstanceInfo::Create(const Oid& SessionId, const InstanceInfoData& Data { ThrowLastError("Could not create instance info shared memory"); } - fcntl(FdGuard.Fd, F_SETFD, FD_CLOEXEC); + fcntl(Fd, F_SETFD, FD_CLOEXEC); fchmod(Fd, 0666); if (ftruncate(Fd, kInstanceInfoSize) < 0) @@ -726,7 +726,7 @@ ZenServerInstanceInfo::OpenReadOnly(const Oid& SessionId) { return false; } - fcntl(FdGuard.Fd, F_SETFD, FD_CLOEXEC); + fcntl(Fd, F_SETFD, FD_CLOEXEC); void* pBuf = mmap(nullptr, kInstanceInfoSize, PROT_READ, MAP_SHARED, Fd, 0); if (pBuf == MAP_FAILED) -- cgit v1.2.3 From ecd4acb89406cfa573d1819532dcaec2c44113f5 Mon Sep 17 00:00:00 2001 From: zousar Date: Mon, 13 Apr 2026 14:42:13 -0600 Subject: Removing CLOEXEC use on shared memory descriptors According to documentation, shm_open already sets O_CLOEXEC. --- src/zenutil/zenserverprocess.cpp | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src/zenutil/zenserverprocess.cpp') diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index 25e9cfa5d..1fe373228 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -197,7 +197,6 @@ ZenServerState::Initialize() ThrowLastError("Could not open a shared memory object"); } } - fcntl(Fd, F_SETFD, FD_CLOEXEC); fchmod(Fd, 0666); void* hMap = (void*)intptr_t(Fd); @@ -250,7 +249,6 @@ ZenServerState::InitializeReadOnly() { return false; } - fcntl(Fd, F_SETFD, FD_CLOEXEC); void* hMap = (void*)intptr_t(Fd); void* pBuf = mmap(nullptr, MapSize, PROT_READ, MAP_SHARED, Fd, 0); @@ -658,7 +656,6 @@ ZenServerInstanceInfo::Create(const Oid& SessionId, const InstanceInfoData& Data { ThrowLastError("Could not create instance info shared memory"); } - fcntl(Fd, F_SETFD, FD_CLOEXEC); fchmod(Fd, 0666); if (ftruncate(Fd, kInstanceInfoSize) < 0) @@ -726,7 +723,6 @@ ZenServerInstanceInfo::OpenReadOnly(const Oid& SessionId) { return false; } - fcntl(Fd, F_SETFD, FD_CLOEXEC); void* pBuf = mmap(nullptr, kInstanceInfoSize, PROT_READ, MAP_SHARED, Fd, 0); if (pBuf == MAP_FAILED) -- cgit v1.2.3 From bc17147c11d2e34a287a4e2171484aa3f9e576d4 Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Fri, 17 Apr 2026 14:10:02 +0200 Subject: log cleanup (#969) - Improvement: New `ZEN_SCOPED_LOG(Expr)` macro routes `ZEN_INFO`/`ZEN_WARN`/`ZEN_DEBUG` in the enclosing block through the given logger expression instead of the default - Improvement: `BuildContainer`, `SaveOplog`, and `LoadOplogContext` now take a caller-provided `LoggerRef` so diagnostic messages route through the caller's logger --- src/zenutil/zenserverprocess.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/zenutil/zenserverprocess.cpp') diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index 1fe373228..e1ffeeb3e 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -1614,7 +1614,7 @@ ValidateLockFileInfo(const LockFileInfo& Info, std::string& OutReason) std::optional StartupZenServer(LoggerRef LogRef, const StartupZenServerOptions& Options) { - auto Log = [&LogRef]() { return LogRef; }; + ZEN_SCOPED_LOG(LogRef); // Check if a matching server is already running { @@ -1684,7 +1684,7 @@ ShutdownZenServer(LoggerRef LogRef, ZenServerState::ZenServerEntry* Entry, const std::filesystem::path& ProgramBaseDir) { - auto Log = [&LogRef]() { return LogRef; }; + ZEN_SCOPED_LOG(LogRef); int EntryPort = (int)Entry->DesiredListenPort.load(); const uint32_t ServerProcessPid = Entry->Pid.load(); try -- 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/zenutil/zenserverprocess.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) (limited to 'src/zenutil/zenserverprocess.cpp') diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index e1ffeeb3e..2d4334ffa 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -1113,8 +1113,23 @@ ZenServerInstance::SpawnServerInternal(int ChildId, std::string_view ServerArgs, ChildEventName << "Zen_Child_" << ChildId; NamedEvent ChildEvent{ChildEventName}; + const std::filesystem::path BaseDir = m_Env.ProgramBaseDir(); + const std::filesystem::path Executable = + m_ServerExecutablePath.empty() ? (BaseDir / "zenserver" ZEN_EXE_SUFFIX_LITERAL) : m_ServerExecutablePath; + ExtendableStringBuilder<512> CommandLine; - CommandLine << "zenserver" ZEN_EXE_SUFFIX_LITERAL; // see CreateProc() call for actual binary path + { + const std::string ExeUtf8 = PathToUtf8(Executable); + constexpr AsciiSet QuoteChars = " \t\""; + if (AsciiSet::HasAny(ExeUtf8.c_str(), QuoteChars)) + { + CommandLine << '"' << ExeUtf8 << '"'; + } + else + { + CommandLine << ExeUtf8; + } + } if (m_ServerMode == ServerMode::kHubServer) { @@ -1127,6 +1142,11 @@ ZenServerInstance::SpawnServerInternal(int ChildId, std::string_view ServerArgs, CommandLine << " --child-id " << ChildEventName; + if (!m_EnableExecutionHistory) + { + CommandLine << " --enable-execution-history=false"; + } + if (!ServerArgs.empty()) { CommandLine << " " << ServerArgs; @@ -1141,10 +1161,6 @@ ZenServerInstance::SpawnServerInternal(int ChildId, std::string_view ServerArgs, { CreationFlags |= CreateProcOptions::Flag_NewConsole; } - - const std::filesystem::path BaseDir = m_Env.ProgramBaseDir(); - const std::filesystem::path Executable = - m_ServerExecutablePath.empty() ? (BaseDir / "zenserver" ZEN_EXE_SUFFIX_LITERAL) : m_ServerExecutablePath; const std::filesystem::path OutputPath = (OpenConsole || m_Env.IsPassthroughOutput()) ? std::filesystem::path{} : std::filesystem::temp_directory_path() / ("zenserver_" + m_Name + ".log"); @@ -1647,6 +1663,7 @@ StartupZenServer(LoggerRef LogRef, const StartupZenServerOptions& Options) ZenServerEnvironment ServerEnvironment; ServerEnvironment.Initialize(ProgramBaseDir); ZenServerInstance Server(ServerEnvironment, Options.Mode); + Server.SetEnableExecutionHistory(Options.EnableExecutionHistory); std::string ServerArguments(Options.ExtraArgs); if ((Options.Port != 0) && (ServerArguments.find("--port") == std::string::npos)) -- cgit v1.2.3