aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil
diff options
context:
space:
mode:
authorLiam Mitchell <[email protected]>2026-03-09 19:06:36 -0700
committerLiam Mitchell <[email protected]>2026-03-09 19:06:36 -0700
commitd1abc50ee9d4fb72efc646e17decafea741caa34 (patch)
treee4288e00f2f7ca0391b83d986efcb69d3ba66a83 /src/zenutil
parentAllow requests with invalid content-types unless specified in command line or... (diff)
parentupdated chunk–block analyser (#818) (diff)
downloadzen-d1abc50ee9d4fb72efc646e17decafea741caa34.tar.xz
zen-d1abc50ee9d4fb72efc646e17decafea741caa34.zip
Merge branch 'main' into lm/restrict-content-type
Diffstat (limited to 'src/zenutil')
-rw-r--r--src/zenutil/config/commandlineoptions.cpp (renamed from src/zenutil/commandlineoptions.cpp)7
-rw-r--r--src/zenutil/config/environmentoptions.cpp (renamed from src/zenutil/environmentoptions.cpp)2
-rw-r--r--src/zenutil/config/loggingconfig.cpp77
-rw-r--r--src/zenutil/consoletui.cpp483
-rw-r--r--src/zenutil/include/zenutil/config/commandlineoptions.h (renamed from src/zenutil/include/zenutil/commandlineoptions.h)0
-rw-r--r--src/zenutil/include/zenutil/config/environmentoptions.h (renamed from src/zenutil/include/zenutil/environmentoptions.h)2
-rw-r--r--src/zenutil/include/zenutil/config/loggingconfig.h37
-rw-r--r--src/zenutil/include/zenutil/consoletui.h60
-rw-r--r--src/zenutil/include/zenutil/logging.h11
-rw-r--r--src/zenutil/include/zenutil/logging/fullformatter.h89
-rw-r--r--src/zenutil/include/zenutil/logging/jsonformatter.h168
-rw-r--r--src/zenutil/include/zenutil/logging/rotatingfilesink.h89
-rw-r--r--src/zenutil/include/zenutil/logging/testformatter.h160
-rw-r--r--src/zenutil/include/zenutil/zenserverprocess.h24
-rw-r--r--src/zenutil/logging.cpp149
-rw-r--r--src/zenutil/rpcrecording.cpp2
-rw-r--r--src/zenutil/wildcard.cpp4
-rw-r--r--src/zenutil/xmake.lua2
-rw-r--r--src/zenutil/zenserverprocess.cpp31
-rw-r--r--src/zenutil/zenutil.cpp2
20 files changed, 954 insertions, 445 deletions
diff --git a/src/zenutil/commandlineoptions.cpp b/src/zenutil/config/commandlineoptions.cpp
index d94564843..25f5522d8 100644
--- a/src/zenutil/commandlineoptions.cpp
+++ b/src/zenutil/config/commandlineoptions.cpp
@@ -1,7 +1,8 @@
// Copyright Epic Games, Inc. All Rights Reserved.
-#include <zenutil/commandlineoptions.h>
+#include <zenutil/config/commandlineoptions.h>
+#include <zencore/filesystem.h>
#include <zencore/string.h>
#include <filesystem>
@@ -194,6 +195,8 @@ commandlineoptions_forcelink()
{
}
+TEST_SUITE_BEGIN("util.commandlineoptions");
+
TEST_CASE("CommandLine")
{
std::vector<std::string> v1 = ParseCommandLine("c:\\my\\exe.exe \"quoted arg\" \"one\",two,\"three\\\"");
@@ -235,5 +238,7 @@ TEST_CASE("CommandLine")
CHECK_EQ(v3Stripped[5], std::string("--build-part-name=win64"));
}
+TEST_SUITE_END();
+
#endif
} // namespace zen
diff --git a/src/zenutil/environmentoptions.cpp b/src/zenutil/config/environmentoptions.cpp
index ee40086c1..fb7f71706 100644
--- a/src/zenutil/environmentoptions.cpp
+++ b/src/zenutil/config/environmentoptions.cpp
@@ -1,6 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
-#include <zenutil/environmentoptions.h>
+#include <zenutil/config/environmentoptions.h>
#include <zencore/filesystem.h>
diff --git a/src/zenutil/config/loggingconfig.cpp b/src/zenutil/config/loggingconfig.cpp
new file mode 100644
index 000000000..5092c60aa
--- /dev/null
+++ b/src/zenutil/config/loggingconfig.cpp
@@ -0,0 +1,77 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zenutil/config/loggingconfig.h"
+
+#include <zenbase/zenbase.h>
+#include <zencore/filesystem.h>
+#include <zencore/logging.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <cxxopts.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+void
+ZenLoggingCmdLineOptions::AddCliOptions(cxxopts::Options& options, ZenLoggingConfig& LoggingConfig)
+{
+ // clang-format off
+ options.add_options("logging")
+ ("abslog", "Path to log file", cxxopts::value<std::string>(m_AbsLogFile))
+ ("log-id", "Specify id for adding context to log output", cxxopts::value<std::string>(LoggingConfig.LogId))
+ ("quiet", "Configure console logger output to level WARN", cxxopts::value<bool>(LoggingConfig.QuietConsole)->default_value("false"))
+ ("noconsole", "Disable console logging", cxxopts::value<bool>(LoggingConfig.NoConsoleOutput)->default_value("false"))
+ ("log-trace", "Change selected loggers to level TRACE", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Trace]))
+ ("log-debug", "Change selected loggers to level DEBUG", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Debug]))
+ ("log-info", "Change selected loggers to level INFO", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Info]))
+ ("log-warn", "Change selected loggers to level WARN", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Warn]))
+ ("log-error", "Change selected loggers to level ERROR", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Err]))
+ ("log-critical", "Change selected loggers to level CRITICAL", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Critical]))
+ ("log-off", "Change selected loggers to level OFF", cxxopts::value<std::string>(LoggingConfig.Loggers[logging::Off]))
+ ("otlp-endpoint", "OpenTelemetry endpoint URI (e.g http://localhost:4318)", cxxopts::value<std::string>(LoggingConfig.OtelEndpointUri))
+ ;
+ // clang-format on
+}
+
+void
+ZenLoggingCmdLineOptions::ApplyOptions(ZenLoggingConfig& LoggingConfig)
+{
+ LoggingConfig.AbsLogFile = MakeSafeAbsolutePath(m_AbsLogFile);
+}
+
+void
+ApplyLoggingOptions(cxxopts::Options& options, ZenLoggingConfig& LoggingConfig)
+{
+ ZEN_UNUSED(options);
+
+ if (LoggingConfig.QuietConsole)
+ {
+ bool HasExplicitConsoleLevel = false;
+ for (int i = 0; i < logging::LogLevelCount; ++i)
+ {
+ if (LoggingConfig.Loggers[i].find("console") != std::string::npos)
+ {
+ HasExplicitConsoleLevel = true;
+ break;
+ }
+ }
+
+ if (!HasExplicitConsoleLevel)
+ {
+ std::string& WarnLoggers = LoggingConfig.Loggers[logging::Warn];
+ if (!WarnLoggers.empty())
+ {
+ WarnLoggers += ",";
+ }
+ WarnLoggers += "console";
+ }
+ }
+
+ for (int i = 0; i < logging::LogLevelCount; ++i)
+ {
+ logging::ConfigureLogLevels(logging::LogLevel(i), LoggingConfig.Loggers[i]);
+ }
+ logging::RefreshLogLevels();
+}
+
+} // namespace zen
diff --git a/src/zenutil/consoletui.cpp b/src/zenutil/consoletui.cpp
new file mode 100644
index 000000000..4410d463d
--- /dev/null
+++ b/src/zenutil/consoletui.cpp
@@ -0,0 +1,483 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/consoletui.h>
+
+#include <zencore/zencore.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+#else
+# include <poll.h>
+# include <sys/ioctl.h>
+# include <termios.h>
+# include <unistd.h>
+#endif
+
+#include <cstdio>
+
+namespace zen {
+
+//////////////////////////////////////////////////////////////////////////
+// Platform-specific terminal helpers
+
+#if ZEN_PLATFORM_WINDOWS
+
+static bool
+CheckIsInteractiveTerminal()
+{
+ DWORD dwMode = 0;
+ return GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &dwMode) && GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwMode);
+}
+
+static void
+EnableVirtualTerminal()
+{
+ HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
+ DWORD dwMode = 0;
+ if (GetConsoleMode(hStdOut, &dwMode))
+ {
+ SetConsoleMode(hStdOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+ }
+}
+
+// RAII guard: sets the console output code page for the lifetime of the object and
+// restores the original on destruction. Required for UTF-8 glyphs to render correctly
+// via printf/fflush since the default console code page is not UTF-8.
+class ConsoleCodePageGuard
+{
+public:
+ explicit ConsoleCodePageGuard(UINT NewCP) : m_OldCP(GetConsoleOutputCP()) { SetConsoleOutputCP(NewCP); }
+ ~ConsoleCodePageGuard() { SetConsoleOutputCP(m_OldCP); }
+
+private:
+ UINT m_OldCP;
+};
+
+enum class ConsoleKey
+{
+ Unknown,
+ ArrowUp,
+ ArrowDown,
+ Enter,
+ Escape,
+};
+
+static ConsoleKey
+ReadKey()
+{
+ HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
+ INPUT_RECORD Record{};
+ DWORD dwRead = 0;
+ while (true)
+ {
+ if (!ReadConsoleInputA(hStdin, &Record, 1, &dwRead))
+ {
+ return ConsoleKey::Escape; // treat read error as cancel
+ }
+ if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown)
+ {
+ switch (Record.Event.KeyEvent.wVirtualKeyCode)
+ {
+ case VK_UP:
+ return ConsoleKey::ArrowUp;
+ case VK_DOWN:
+ return ConsoleKey::ArrowDown;
+ case VK_RETURN:
+ return ConsoleKey::Enter;
+ case VK_ESCAPE:
+ return ConsoleKey::Escape;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+#else // POSIX
+
+static bool
+CheckIsInteractiveTerminal()
+{
+ return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO);
+}
+
+static void
+EnableVirtualTerminal()
+{
+ // ANSI escape codes are native on POSIX terminals; nothing to do
+}
+
+// RAII guard: switches the terminal to raw/unbuffered input mode and restores
+// the original attributes on destruction.
+class RawModeGuard
+{
+public:
+ RawModeGuard()
+ {
+ if (tcgetattr(STDIN_FILENO, &m_OldAttrs) != 0)
+ {
+ return;
+ }
+
+ struct termios Raw = m_OldAttrs;
+ Raw.c_iflag &= ~static_cast<tcflag_t>(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ Raw.c_cflag |= CS8;
+ Raw.c_lflag &= ~static_cast<tcflag_t>(ECHO | ICANON | IEXTEN | ISIG);
+ Raw.c_cc[VMIN] = 1;
+ Raw.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &Raw) == 0)
+ {
+ m_Valid = true;
+ }
+ }
+
+ ~RawModeGuard()
+ {
+ if (m_Valid)
+ {
+ tcsetattr(STDIN_FILENO, TCSANOW, &m_OldAttrs);
+ }
+ }
+
+ bool IsValid() const { return m_Valid; }
+
+private:
+ struct termios m_OldAttrs = {};
+ bool m_Valid = false;
+};
+
+static int
+ReadByteWithTimeout(int TimeoutMs)
+{
+ struct pollfd Pfd
+ {
+ STDIN_FILENO, POLLIN, 0
+ };
+ if (poll(&Pfd, 1, TimeoutMs) > 0 && (Pfd.revents & POLLIN))
+ {
+ unsigned char c = 0;
+ if (read(STDIN_FILENO, &c, 1) == 1)
+ {
+ return static_cast<int>(c);
+ }
+ }
+ return -1;
+}
+
+// State for fullscreen live mode (alternate screen + raw input)
+static struct termios s_SavedAttrs = {};
+static bool s_InLiveMode = false;
+
+enum class ConsoleKey
+{
+ Unknown,
+ ArrowUp,
+ ArrowDown,
+ Enter,
+ Escape,
+};
+
+static ConsoleKey
+ReadKey()
+{
+ unsigned char c = 0;
+ if (read(STDIN_FILENO, &c, 1) != 1)
+ {
+ return ConsoleKey::Escape; // treat read error as cancel
+ }
+
+ if (c == 27) // ESC byte or start of an escape sequence
+ {
+ int Next = ReadByteWithTimeout(50);
+ if (Next == '[')
+ {
+ int Final = ReadByteWithTimeout(50);
+ if (Final == 'A')
+ {
+ return ConsoleKey::ArrowUp;
+ }
+ if (Final == 'B')
+ {
+ return ConsoleKey::ArrowDown;
+ }
+ }
+ return ConsoleKey::Escape;
+ }
+
+ if (c == '\r' || c == '\n')
+ {
+ return ConsoleKey::Enter;
+ }
+
+ return ConsoleKey::Unknown;
+}
+
+#endif // ZEN_PLATFORM_WINDOWS / POSIX
+
+//////////////////////////////////////////////////////////////////////////
+// Public API
+
+uint32_t
+TuiConsoleColumns(uint32_t Default)
+{
+#if ZEN_PLATFORM_WINDOWS
+ CONSOLE_SCREEN_BUFFER_INFO Csbi = {};
+ if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Csbi))
+ {
+ return static_cast<uint32_t>(Csbi.dwSize.X);
+ }
+#else
+ struct winsize Ws = {};
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_col > 0)
+ {
+ return static_cast<uint32_t>(Ws.ws_col);
+ }
+#endif
+ return Default;
+}
+
+void
+TuiEnableOutput()
+{
+ EnableVirtualTerminal();
+#if ZEN_PLATFORM_WINDOWS
+ SetConsoleOutputCP(CP_UTF8);
+#endif
+}
+
+bool
+TuiIsStdoutTty()
+{
+#if ZEN_PLATFORM_WINDOWS
+ static bool Cached = [] {
+ DWORD dwMode = 0;
+ return GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwMode) != 0;
+ }();
+ return Cached;
+#else
+ static bool Cached = isatty(STDOUT_FILENO) != 0;
+ return Cached;
+#endif
+}
+
+bool
+IsTuiAvailable()
+{
+ static bool Cached = CheckIsInteractiveTerminal();
+ return Cached;
+}
+
+int
+TuiPickOne(std::string_view Title, std::span<const std::string> Items)
+{
+ EnableVirtualTerminal();
+
+#if ZEN_PLATFORM_WINDOWS
+ ConsoleCodePageGuard CodePageGuard(CP_UTF8);
+#else
+ RawModeGuard RawMode;
+ if (!RawMode.IsValid())
+ {
+ return -1;
+ }
+#endif
+
+ const int Count = static_cast<int>(Items.size());
+ int SelectedIndex = 0;
+
+ printf("\n%.*s\n\n", static_cast<int>(Title.size()), Title.data());
+
+ // Hide cursor during interaction
+ printf("\033[?25l");
+
+ // Renders the full entry list and hint footer.
+ // On subsequent calls, moves the cursor back up first to overwrite the previous output.
+ bool FirstRender = true;
+ auto RenderAll = [&] {
+ if (!FirstRender)
+ {
+ printf("\033[%dA", Count + 2); // move up: entries + blank line + hint line
+ }
+ FirstRender = false;
+
+ for (int i = 0; i < Count; ++i)
+ {
+ bool IsSelected = (i == SelectedIndex);
+
+ printf("\r\033[K"); // erase line
+
+ if (IsSelected)
+ {
+ printf("\033[1;7m"); // bold + reverse video
+ }
+
+ // \xe2\x96\xb6 = U+25B6 BLACK RIGHT-POINTING TRIANGLE (▶)
+ const char* Indicator = IsSelected ? " \xe2\x96\xb6 " : " ";
+
+ printf("%s%s", Indicator, Items[i].c_str());
+
+ if (IsSelected)
+ {
+ printf("\033[0m"); // reset attributes
+ }
+
+ printf("\n");
+ }
+
+ // Blank separator line
+ printf("\r\033[K\n");
+
+ // Hint footer
+ // \xe2\x86\x91 = U+2191 ↑ \xe2\x86\x93 = U+2193 ↓
+ printf(
+ "\r\033[K \033[2m\xe2\x86\x91/\xe2\x86\x93\033[0m navigate "
+ "\033[2mEnter\033[0m confirm "
+ "\033[2mEsc\033[0m cancel\n");
+
+ fflush(stdout);
+ };
+
+ RenderAll();
+
+ int Result = -1;
+ bool Done = false;
+ while (!Done)
+ {
+ ConsoleKey Key = ReadKey();
+ switch (Key)
+ {
+ case ConsoleKey::ArrowUp:
+ SelectedIndex = (SelectedIndex - 1 + Count) % Count;
+ RenderAll();
+ break;
+
+ case ConsoleKey::ArrowDown:
+ SelectedIndex = (SelectedIndex + 1) % Count;
+ RenderAll();
+ break;
+
+ case ConsoleKey::Enter:
+ Result = SelectedIndex;
+ Done = true;
+ break;
+
+ case ConsoleKey::Escape:
+ Done = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ // Restore cursor and add a blank line for visual separation
+ printf("\033[?25h\n");
+ fflush(stdout);
+
+ return Result;
+}
+
+void
+TuiEnterAlternateScreen()
+{
+ EnableVirtualTerminal();
+#if ZEN_PLATFORM_WINDOWS
+ SetConsoleOutputCP(CP_UTF8);
+#endif
+
+ printf("\033[?1049h"); // Enter alternate screen buffer
+ printf("\033[?25l"); // Hide cursor
+ fflush(stdout);
+
+#if !ZEN_PLATFORM_WINDOWS
+ if (tcgetattr(STDIN_FILENO, &s_SavedAttrs) == 0)
+ {
+ struct termios Raw = s_SavedAttrs;
+ Raw.c_iflag &= ~static_cast<tcflag_t>(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ Raw.c_cflag |= CS8;
+ Raw.c_lflag &= ~static_cast<tcflag_t>(ECHO | ICANON | IEXTEN | ISIG);
+ Raw.c_cc[VMIN] = 1;
+ Raw.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &Raw) == 0)
+ {
+ s_InLiveMode = true;
+ }
+ }
+#endif
+}
+
+void
+TuiExitAlternateScreen()
+{
+ printf("\033[?25h"); // Show cursor
+ printf("\033[?1049l"); // Exit alternate screen buffer
+ fflush(stdout);
+
+#if !ZEN_PLATFORM_WINDOWS
+ if (s_InLiveMode)
+ {
+ tcsetattr(STDIN_FILENO, TCSANOW, &s_SavedAttrs);
+ s_InLiveMode = false;
+ }
+#endif
+}
+
+void
+TuiCursorHome()
+{
+ printf("\033[H");
+}
+
+uint32_t
+TuiConsoleRows(uint32_t Default)
+{
+#if ZEN_PLATFORM_WINDOWS
+ CONSOLE_SCREEN_BUFFER_INFO Csbi = {};
+ if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Csbi))
+ {
+ return static_cast<uint32_t>(Csbi.srWindow.Bottom - Csbi.srWindow.Top + 1);
+ }
+#else
+ struct winsize Ws = {};
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_row > 0)
+ {
+ return static_cast<uint32_t>(Ws.ws_row);
+ }
+#endif
+ return Default;
+}
+
+bool
+TuiPollQuit()
+{
+#if ZEN_PLATFORM_WINDOWS
+ HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
+ DWORD dwCount = 0;
+ if (!GetNumberOfConsoleInputEvents(hStdin, &dwCount) || dwCount == 0)
+ {
+ return false;
+ }
+ INPUT_RECORD Record{};
+ DWORD dwRead = 0;
+ while (PeekConsoleInputA(hStdin, &Record, 1, &dwRead) && dwRead > 0)
+ {
+ ReadConsoleInputA(hStdin, &Record, 1, &dwRead);
+ if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown)
+ {
+ WORD vk = Record.Event.KeyEvent.wVirtualKeyCode;
+ char ch = Record.Event.KeyEvent.uChar.AsciiChar;
+ if (vk == VK_ESCAPE || ch == 'q' || ch == 'Q')
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+#else
+ // Non-blocking read: character 3 = Ctrl+C, 27 = Esc, 'q'/'Q' = quit
+ int b = ReadByteWithTimeout(0);
+ return (b == 3 || b == 27 || b == 'q' || b == 'Q');
+#endif
+}
+
+} // namespace zen
diff --git a/src/zenutil/include/zenutil/commandlineoptions.h b/src/zenutil/include/zenutil/config/commandlineoptions.h
index 01cceedb1..01cceedb1 100644
--- a/src/zenutil/include/zenutil/commandlineoptions.h
+++ b/src/zenutil/include/zenutil/config/commandlineoptions.h
diff --git a/src/zenutil/include/zenutil/environmentoptions.h b/src/zenutil/include/zenutil/config/environmentoptions.h
index 7418608e4..1ecdf591a 100644
--- a/src/zenutil/include/zenutil/environmentoptions.h
+++ b/src/zenutil/include/zenutil/config/environmentoptions.h
@@ -3,7 +3,7 @@
#pragma once
#include <zencore/string.h>
-#include <zenutil/commandlineoptions.h>
+#include <zenutil/config/commandlineoptions.h>
namespace zen {
diff --git a/src/zenutil/include/zenutil/config/loggingconfig.h b/src/zenutil/include/zenutil/config/loggingconfig.h
new file mode 100644
index 000000000..b55b2d9f7
--- /dev/null
+++ b/src/zenutil/include/zenutil/config/loggingconfig.h
@@ -0,0 +1,37 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/logbase.h>
+#include <filesystem>
+#include <string>
+
+namespace cxxopts {
+class Options;
+}
+
+namespace zen {
+
+struct ZenLoggingConfig
+{
+ bool NoConsoleOutput = false; // Control default use of stdout for diagnostics
+ bool QuietConsole = false; // Configure console logger output to level WARN
+ std::filesystem::path AbsLogFile; // Absolute path to main log file
+ std::string Loggers[logging::LogLevelCount];
+ std::string LogId; // Id for tagging log output
+ std::string OtelEndpointUri; // OpenTelemetry endpoint URI
+};
+
+void ApplyLoggingOptions(cxxopts::Options& options, ZenLoggingConfig& LoggingConfig);
+
+class ZenLoggingCmdLineOptions
+{
+public:
+ void AddCliOptions(cxxopts::Options& options, ZenLoggingConfig& LoggingConfig);
+ void ApplyOptions(ZenLoggingConfig& LoggingConfig);
+
+private:
+ std::string m_AbsLogFile;
+};
+
+} // namespace zen
diff --git a/src/zenutil/include/zenutil/consoletui.h b/src/zenutil/include/zenutil/consoletui.h
new file mode 100644
index 000000000..5f74fa82b
--- /dev/null
+++ b/src/zenutil/include/zenutil/consoletui.h
@@ -0,0 +1,60 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <cstdint>
+#include <span>
+#include <string>
+#include <string_view>
+
+namespace zen {
+
+// Returns the width of the console in columns, or Default if it cannot be determined.
+uint32_t TuiConsoleColumns(uint32_t Default = 120);
+
+// Enables ANSI/VT escape code processing and UTF-8 console output.
+// Call once before printing ANSI escape sequences or multi-byte UTF-8 characters via printf.
+// Safe to call multiple times. No-op on POSIX (escape codes are native there).
+void TuiEnableOutput();
+
+// Returns true if stdout is connected to a real terminal (not piped or redirected).
+// Useful for deciding whether to use ANSI escape codes for progress output.
+bool TuiIsStdoutTty();
+
+// Returns true if both stdin and stdout are connected to an interactive terminal
+// (i.e. not piped or redirected). Must be checked before calling TuiPickOne().
+bool IsTuiAvailable();
+
+// Displays a cursor-navigable single-select list in the terminal.
+//
+// - Title: a short description printed once above the list
+// - Items: pre-formatted display labels, one per selectable entry
+//
+// Arrow keys (↑/↓) navigate the selection, Enter confirms, Esc cancels.
+// Returns the index of the selected item, or -1 if the user cancelled.
+//
+// Precondition: IsTuiAvailable() must be true.
+int TuiPickOne(std::string_view Title, std::span<const std::string> Items);
+
+// Enter the alternate screen buffer for fullscreen live-update mode.
+// Hides the cursor. On POSIX, switches to raw/unbuffered terminal input.
+// Must be balanced by a call to TuiExitAlternateScreen().
+// Precondition: IsTuiAvailable() must be true.
+void TuiEnterAlternateScreen();
+
+// Exit alternate screen buffer. Restores the cursor and, on POSIX, the original
+// terminal mode. Safe to call even if TuiEnterAlternateScreen() was not called.
+void TuiExitAlternateScreen();
+
+// Move the cursor to the top-left corner of the terminal (row 1, col 1).
+void TuiCursorHome();
+
+// Returns the height of the console in rows, or Default if it cannot be determined.
+uint32_t TuiConsoleRows(uint32_t Default = 40);
+
+// Non-blocking check: returns true if the user has pressed a key that means quit
+// (Esc, 'q', 'Q', or Ctrl+C). Consumes the event if one is pending.
+// Should only be called while in alternate screen mode.
+bool TuiPollQuit();
+
+} // namespace zen
diff --git a/src/zenutil/include/zenutil/logging.h b/src/zenutil/include/zenutil/logging.h
index 85ddc86cd..95419c274 100644
--- a/src/zenutil/include/zenutil/logging.h
+++ b/src/zenutil/include/zenutil/logging.h
@@ -3,19 +3,12 @@
#pragma once
#include <zencore/logging.h>
+#include <zencore/logging/sink.h>
#include <filesystem>
#include <memory>
#include <string>
-namespace spdlog::sinks {
-class sink;
-}
-
-namespace spdlog {
-using sink_ptr = std::shared_ptr<sinks::sink>;
-}
-
//////////////////////////////////////////////////////////////////////////
//
// Logging utilities
@@ -45,6 +38,6 @@ void FinishInitializeLogging(const LoggingOptions& LoggingOptions);
void InitializeLogging(const LoggingOptions& LoggingOptions);
void ShutdownLogging();
-spdlog::sink_ptr GetFileSink();
+logging::SinkPtr GetFileSink();
} // namespace zen
diff --git a/src/zenutil/include/zenutil/logging/fullformatter.h b/src/zenutil/include/zenutil/logging/fullformatter.h
index 9f245becd..33cb94dae 100644
--- a/src/zenutil/include/zenutil/logging/fullformatter.h
+++ b/src/zenutil/include/zenutil/logging/fullformatter.h
@@ -2,21 +2,19 @@
#pragma once
+#include <zencore/logging/formatter.h>
+#include <zencore/logging/helpers.h>
#include <zencore/memory/llm.h>
#include <zencore/zencore.h>
#include <string_view>
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <spdlog/formatter.h>
-ZEN_THIRD_PARTY_INCLUDES_END
-
namespace zen::logging {
-class full_formatter final : public spdlog::formatter
+class FullFormatter final : public Formatter
{
public:
- full_formatter(std::string_view LogId, std::chrono::time_point<std::chrono::system_clock> Epoch)
+ FullFormatter(std::string_view LogId, std::chrono::time_point<std::chrono::system_clock> Epoch)
: m_Epoch(Epoch)
, m_LogId(LogId)
, m_LinePrefix(128, ' ')
@@ -24,16 +22,19 @@ public:
{
}
- full_formatter(std::string_view LogId) : m_LogId(LogId), m_LinePrefix(128, ' '), m_UseFullDate(true) {}
+ FullFormatter(std::string_view LogId) : m_LogId(LogId), m_LinePrefix(128, ' '), m_UseFullDate(true) {}
- virtual std::unique_ptr<formatter> clone() const override
+ virtual std::unique_ptr<Formatter> Clone() const override
{
ZEN_MEMSCOPE(ELLMTag::Logging);
- // Note: this does not properly clone m_UseFullDate
- return std::make_unique<full_formatter>(m_LogId, m_Epoch);
+ if (m_UseFullDate)
+ {
+ return std::make_unique<FullFormatter>(m_LogId);
+ }
+ return std::make_unique<FullFormatter>(m_LogId, m_Epoch);
}
- virtual void format(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& OutBuffer) override
+ virtual void Format(const LogMessage& Msg, MemoryBuffer& OutBuffer) override
{
ZEN_MEMSCOPE(ELLMTag::Logging);
@@ -44,38 +45,38 @@ public:
std::chrono::seconds TimestampSeconds;
- std::chrono::milliseconds millis;
+ std::chrono::milliseconds Millis;
if (m_UseFullDate)
{
- TimestampSeconds = std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
+ TimestampSeconds = std::chrono::duration_cast<std::chrono::seconds>(Msg.GetTime().time_since_epoch());
if (TimestampSeconds != m_LastLogSecs)
{
RwLock::ExclusiveLockScope _(m_TimestampLock);
m_LastLogSecs = TimestampSeconds;
- m_CachedLocalTm = spdlog::details::os::localtime(spdlog::log_clock::to_time_t(msg.time));
+ m_CachedLocalTm = helpers::SafeLocaltime(LogClock::to_time_t(Msg.GetTime()));
m_CachedDatetime.clear();
m_CachedDatetime.push_back('[');
- spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_year % 100, m_CachedDatetime);
+ helpers::Pad2(m_CachedLocalTm.tm_year % 100, m_CachedDatetime);
m_CachedDatetime.push_back('-');
- spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_mon + 1, m_CachedDatetime);
+ helpers::Pad2(m_CachedLocalTm.tm_mon + 1, m_CachedDatetime);
m_CachedDatetime.push_back('-');
- spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_mday, m_CachedDatetime);
+ helpers::Pad2(m_CachedLocalTm.tm_mday, m_CachedDatetime);
m_CachedDatetime.push_back(' ');
- spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_hour, m_CachedDatetime);
+ helpers::Pad2(m_CachedLocalTm.tm_hour, m_CachedDatetime);
m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_min, m_CachedDatetime);
+ helpers::Pad2(m_CachedLocalTm.tm_min, m_CachedDatetime);
m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_sec, m_CachedDatetime);
+ helpers::Pad2(m_CachedLocalTm.tm_sec, m_CachedDatetime);
m_CachedDatetime.push_back('.');
}
- millis = spdlog::details::fmt_helper::time_fraction<std::chrono::milliseconds>(msg.time);
+ Millis = helpers::TimeFraction<std::chrono::milliseconds>(Msg.GetTime());
}
else
{
- auto ElapsedTime = msg.time - m_Epoch;
+ auto ElapsedTime = Msg.GetTime() - m_Epoch;
TimestampSeconds = std::chrono::duration_cast<std::chrono::seconds>(ElapsedTime);
if (m_CacheTimestamp.load() != TimestampSeconds)
@@ -93,15 +94,15 @@ public:
m_CachedDatetime.clear();
m_CachedDatetime.push_back('[');
- spdlog::details::fmt_helper::pad2(LogHours, m_CachedDatetime);
+ helpers::Pad2(LogHours, m_CachedDatetime);
m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(LogMins, m_CachedDatetime);
+ helpers::Pad2(LogMins, m_CachedDatetime);
m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(LogSecs, m_CachedDatetime);
+ helpers::Pad2(LogSecs, m_CachedDatetime);
m_CachedDatetime.push_back('.');
}
- millis = std::chrono::duration_cast<std::chrono::milliseconds>(ElapsedTime - TimestampSeconds);
+ Millis = std::chrono::duration_cast<std::chrono::milliseconds>(ElapsedTime - TimestampSeconds);
}
{
@@ -109,44 +110,43 @@ public:
OutBuffer.append(m_CachedDatetime.begin(), m_CachedDatetime.end());
}
- spdlog::details::fmt_helper::pad3(static_cast<uint32_t>(millis.count()), OutBuffer);
+ helpers::Pad3(static_cast<uint32_t>(Millis.count()), OutBuffer);
OutBuffer.push_back(']');
OutBuffer.push_back(' ');
if (!m_LogId.empty())
{
OutBuffer.push_back('[');
- spdlog::details::fmt_helper::append_string_view(m_LogId, OutBuffer);
+ helpers::AppendStringView(m_LogId, OutBuffer);
OutBuffer.push_back(']');
OutBuffer.push_back(' ');
}
// append logger name if exists
- if (msg.logger_name.size() > 0)
+ if (Msg.GetLoggerName().size() > 0)
{
OutBuffer.push_back('[');
- spdlog::details::fmt_helper::append_string_view(msg.logger_name, OutBuffer);
+ helpers::AppendStringView(Msg.GetLoggerName(), OutBuffer);
OutBuffer.push_back(']');
OutBuffer.push_back(' ');
}
OutBuffer.push_back('[');
// wrap the level name with color
- msg.color_range_start = OutBuffer.size();
- spdlog::details::fmt_helper::append_string_view(spdlog::level::to_string_view(msg.level), OutBuffer);
- msg.color_range_end = OutBuffer.size();
+ Msg.ColorRangeStart = OutBuffer.size();
+ helpers::AppendStringView(helpers::LevelToShortString(Msg.GetLevel()), OutBuffer);
+ Msg.ColorRangeEnd = OutBuffer.size();
OutBuffer.push_back(']');
OutBuffer.push_back(' ');
// add source location if present
- if (!msg.source.empty())
+ if (Msg.GetSource())
{
OutBuffer.push_back('[');
- const char* filename =
- spdlog::details::short_filename_formatter<spdlog::details::null_scoped_padder>::basename(msg.source.filename);
- spdlog::details::fmt_helper::append_string_view(filename, OutBuffer);
+ const char* Filename = helpers::ShortFilename(Msg.GetSource().Filename);
+ helpers::AppendStringView(Filename, OutBuffer);
OutBuffer.push_back(':');
- spdlog::details::fmt_helper::append_int(msg.source.line, OutBuffer);
+ helpers::AppendInt(Msg.GetSource().Line, OutBuffer);
OutBuffer.push_back(']');
OutBuffer.push_back(' ');
}
@@ -156,8 +156,9 @@ public:
const size_t LinePrefixCount = Min<size_t>(OutBuffer.size(), m_LinePrefix.size());
- auto ItLineBegin = msg.payload.begin();
- auto ItMessageEnd = msg.payload.end();
+ auto MsgPayload = Msg.GetPayload();
+ auto ItLineBegin = MsgPayload.begin();
+ auto ItMessageEnd = MsgPayload.end();
bool IsFirstline = true;
{
@@ -170,9 +171,9 @@ public:
}
else
{
- spdlog::details::fmt_helper::append_string_view(std::string_view(m_LinePrefix.data(), LinePrefixCount), OutBuffer);
+ helpers::AppendStringView(std::string_view(m_LinePrefix.data(), LinePrefixCount), OutBuffer);
}
- spdlog::details::fmt_helper::append_string_view(spdlog::string_view_t(&*ItLineBegin, ItLineEnd - ItLineBegin), OutBuffer);
+ helpers::AppendStringView(std::string_view(&*ItLineBegin, ItLineEnd - ItLineBegin), OutBuffer);
};
while (ItLineEnd != ItMessageEnd)
@@ -187,7 +188,7 @@ public:
if (ItLineBegin != ItMessageEnd)
{
EmitLine();
- spdlog::details::fmt_helper::append_string_view("\n"sv, OutBuffer);
+ helpers::AppendStringView("\n"sv, OutBuffer);
}
}
}
@@ -197,7 +198,7 @@ private:
std::tm m_CachedLocalTm;
std::chrono::seconds m_LastLogSecs{std::chrono::seconds(87654321)};
std::atomic<std::chrono::seconds> m_CacheTimestamp{std::chrono::seconds(87654321)};
- spdlog::memory_buf_t m_CachedDatetime;
+ MemoryBuffer m_CachedDatetime;
std::string m_LogId;
std::string m_LinePrefix;
bool m_UseFullDate = true;
diff --git a/src/zenutil/include/zenutil/logging/jsonformatter.h b/src/zenutil/include/zenutil/logging/jsonformatter.h
index 3f660e421..216b1b5e5 100644
--- a/src/zenutil/include/zenutil/logging/jsonformatter.h
+++ b/src/zenutil/include/zenutil/logging/jsonformatter.h
@@ -2,27 +2,26 @@
#pragma once
+#include <zencore/logging/formatter.h>
+#include <zencore/logging/helpers.h>
#include <zencore/memory/llm.h>
#include <zencore/zencore.h>
#include <string_view>
-
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <spdlog/formatter.h>
-ZEN_THIRD_PARTY_INCLUDES_END
+#include <unordered_map>
namespace zen::logging {
using namespace std::literals;
-class json_formatter final : public spdlog::formatter
+class JsonFormatter final : public Formatter
{
public:
- json_formatter(std::string_view LogId) : m_LogId(LogId) {}
+ JsonFormatter(std::string_view LogId) : m_LogId(LogId) {}
- virtual std::unique_ptr<formatter> clone() const override { return std::make_unique<json_formatter>(m_LogId); }
+ virtual std::unique_ptr<Formatter> Clone() const override { return std::make_unique<JsonFormatter>(m_LogId); }
- virtual void format(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& dest) override
+ virtual void Format(const LogMessage& Msg, MemoryBuffer& Dest) override
{
ZEN_MEMSCOPE(ELLMTag::Logging);
@@ -30,141 +29,132 @@ public:
using std::chrono::milliseconds;
using std::chrono::seconds;
- auto secs = std::chrono::duration_cast<seconds>(msg.time.time_since_epoch());
- if (secs != m_LastLogSecs)
+ auto Secs = std::chrono::duration_cast<seconds>(Msg.GetTime().time_since_epoch());
+ if (Secs != m_LastLogSecs)
{
- m_CachedTm = spdlog::details::os::localtime(spdlog::log_clock::to_time_t(msg.time));
- m_LastLogSecs = secs;
- }
-
- const auto& tm_time = m_CachedTm;
+ RwLock::ExclusiveLockScope _(m_TimestampLock);
+ m_CachedTm = helpers::SafeLocaltime(LogClock::to_time_t(Msg.GetTime()));
+ m_LastLogSecs = Secs;
- // cache the date/time part for the next second.
-
- if (m_CacheTimestamp != secs || m_CachedDatetime.size() == 0)
- {
+ // cache the date/time part for the next second.
m_CachedDatetime.clear();
- spdlog::details::fmt_helper::append_int(tm_time.tm_year + 1900, m_CachedDatetime);
+ helpers::AppendInt(m_CachedTm.tm_year + 1900, m_CachedDatetime);
m_CachedDatetime.push_back('-');
- spdlog::details::fmt_helper::pad2(tm_time.tm_mon + 1, m_CachedDatetime);
+ helpers::Pad2(m_CachedTm.tm_mon + 1, m_CachedDatetime);
m_CachedDatetime.push_back('-');
- spdlog::details::fmt_helper::pad2(tm_time.tm_mday, m_CachedDatetime);
+ helpers::Pad2(m_CachedTm.tm_mday, m_CachedDatetime);
m_CachedDatetime.push_back(' ');
- spdlog::details::fmt_helper::pad2(tm_time.tm_hour, m_CachedDatetime);
+ helpers::Pad2(m_CachedTm.tm_hour, m_CachedDatetime);
m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(tm_time.tm_min, m_CachedDatetime);
+ helpers::Pad2(m_CachedTm.tm_min, m_CachedDatetime);
m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(tm_time.tm_sec, m_CachedDatetime);
+ helpers::Pad2(m_CachedTm.tm_sec, m_CachedDatetime);
m_CachedDatetime.push_back('.');
-
- m_CacheTimestamp = secs;
}
- dest.append("{"sv);
- dest.append("\"time\": \""sv);
- dest.append(m_CachedDatetime.begin(), m_CachedDatetime.end());
- auto millis = spdlog::details::fmt_helper::time_fraction<milliseconds>(msg.time);
- spdlog::details::fmt_helper::pad3(static_cast<uint32_t>(millis.count()), dest);
- dest.append("\", "sv);
+ helpers::AppendStringView("{"sv, Dest);
+ helpers::AppendStringView("\"time\": \""sv, Dest);
+ {
+ RwLock::SharedLockScope _(m_TimestampLock);
+ Dest.append(m_CachedDatetime.begin(), m_CachedDatetime.end());
+ }
+ auto Millis = helpers::TimeFraction<milliseconds>(Msg.GetTime());
+ helpers::Pad3(static_cast<uint32_t>(Millis.count()), Dest);
+ helpers::AppendStringView("\", "sv, Dest);
- dest.append("\"status\": \""sv);
- dest.append(spdlog::level::to_string_view(msg.level));
- dest.append("\", "sv);
+ helpers::AppendStringView("\"status\": \""sv, Dest);
+ helpers::AppendStringView(helpers::LevelToShortString(Msg.GetLevel()), Dest);
+ helpers::AppendStringView("\", "sv, Dest);
- dest.append("\"source\": \""sv);
- dest.append("zenserver"sv);
- dest.append("\", "sv);
+ helpers::AppendStringView("\"source\": \""sv, Dest);
+ helpers::AppendStringView("zenserver"sv, Dest);
+ helpers::AppendStringView("\", "sv, Dest);
- dest.append("\"service\": \""sv);
- dest.append("zencache"sv);
- dest.append("\", "sv);
+ helpers::AppendStringView("\"service\": \""sv, Dest);
+ helpers::AppendStringView("zencache"sv, Dest);
+ helpers::AppendStringView("\", "sv, Dest);
if (!m_LogId.empty())
{
- dest.append("\"id\": \""sv);
- dest.append(m_LogId);
- dest.append("\", "sv);
+ helpers::AppendStringView("\"id\": \""sv, Dest);
+ helpers::AppendStringView(m_LogId, Dest);
+ helpers::AppendStringView("\", "sv, Dest);
}
- if (msg.logger_name.size() > 0)
+ if (Msg.GetLoggerName().size() > 0)
{
- dest.append("\"logger.name\": \""sv);
- dest.append(msg.logger_name);
- dest.append("\", "sv);
+ helpers::AppendStringView("\"logger.name\": \""sv, Dest);
+ helpers::AppendStringView(Msg.GetLoggerName(), Dest);
+ helpers::AppendStringView("\", "sv, Dest);
}
- if (msg.thread_id != 0)
+ if (Msg.GetThreadId() != 0)
{
- dest.append("\"logger.thread_name\": \""sv);
- spdlog::details::fmt_helper::pad_uint(msg.thread_id, 0, dest);
- dest.append("\", "sv);
+ helpers::AppendStringView("\"logger.thread_name\": \""sv, Dest);
+ helpers::PadUint(Msg.GetThreadId(), 0, Dest);
+ helpers::AppendStringView("\", "sv, Dest);
}
- if (!msg.source.empty())
+ if (Msg.GetSource())
{
- dest.append("\"file\": \""sv);
- WriteEscapedString(
- dest,
- spdlog::details::short_filename_formatter<spdlog::details::null_scoped_padder>::basename(msg.source.filename));
- dest.append("\","sv);
-
- dest.append("\"line\": \""sv);
- dest.append(fmt::format("{}", msg.source.line));
- dest.append("\","sv);
-
- dest.append("\"logger.method_name\": \""sv);
- WriteEscapedString(dest, msg.source.funcname);
- dest.append("\", "sv);
+ helpers::AppendStringView("\"file\": \""sv, Dest);
+ WriteEscapedString(Dest, helpers::ShortFilename(Msg.GetSource().Filename));
+ helpers::AppendStringView("\","sv, Dest);
+
+ helpers::AppendStringView("\"line\": \""sv, Dest);
+ helpers::AppendInt(Msg.GetSource().Line, Dest);
+ helpers::AppendStringView("\","sv, Dest);
}
- dest.append("\"message\": \""sv);
- WriteEscapedString(dest, msg.payload);
- dest.append("\""sv);
+ helpers::AppendStringView("\"message\": \""sv, Dest);
+ WriteEscapedString(Dest, Msg.GetPayload());
+ helpers::AppendStringView("\""sv, Dest);
- dest.append("}\n"sv);
+ helpers::AppendStringView("}\n"sv, Dest);
}
private:
- static inline const std::unordered_map<char, std::string_view> SpecialCharacterMap{{'\b', "\\b"sv},
- {'\f', "\\f"sv},
- {'\n', "\\n"sv},
- {'\r', "\\r"sv},
- {'\t', "\\t"sv},
- {'"', "\\\""sv},
- {'\\', "\\\\"sv}};
-
- static void WriteEscapedString(spdlog::memory_buf_t& dest, const spdlog::string_view_t& payload)
+ static inline const std::unordered_map<char, std::string_view> s_SpecialCharacterMap{{'\b', "\\b"sv},
+ {'\f', "\\f"sv},
+ {'\n', "\\n"sv},
+ {'\r', "\\r"sv},
+ {'\t', "\\t"sv},
+ {'"', "\\\""sv},
+ {'\\', "\\\\"sv}};
+
+ static void WriteEscapedString(MemoryBuffer& Dest, const std::string_view& Text)
{
- const char* RangeStart = payload.begin();
- for (const char* It = RangeStart; It != payload.end(); ++It)
+ const char* RangeStart = Text.data();
+ const char* End = Text.data() + Text.size();
+ for (const char* It = RangeStart; It != End; ++It)
{
- if (auto SpecialIt = SpecialCharacterMap.find(*It); SpecialIt != SpecialCharacterMap.end())
+ if (auto SpecialIt = s_SpecialCharacterMap.find(*It); SpecialIt != s_SpecialCharacterMap.end())
{
if (RangeStart != It)
{
- dest.append(RangeStart, It);
+ Dest.append(RangeStart, It);
}
- dest.append(SpecialIt->second);
+ helpers::AppendStringView(SpecialIt->second, Dest);
RangeStart = It + 1;
}
}
- if (RangeStart != payload.end())
+ if (RangeStart != End)
{
- dest.append(RangeStart, payload.end());
+ Dest.append(RangeStart, End);
}
};
std::tm m_CachedTm{0, 0, 0, 0, 0, 0, 0, 0, 0};
std::chrono::seconds m_LastLogSecs{0};
- std::chrono::seconds m_CacheTimestamp{0};
- spdlog::memory_buf_t m_CachedDatetime;
+ MemoryBuffer m_CachedDatetime;
std::string m_LogId;
+ RwLock m_TimestampLock;
};
} // namespace zen::logging
diff --git a/src/zenutil/include/zenutil/logging/rotatingfilesink.h b/src/zenutil/include/zenutil/logging/rotatingfilesink.h
index 8901b7779..cebc5b110 100644
--- a/src/zenutil/include/zenutil/logging/rotatingfilesink.h
+++ b/src/zenutil/include/zenutil/logging/rotatingfilesink.h
@@ -3,14 +3,11 @@
#pragma once
#include <zencore/basicfile.h>
+#include <zencore/logging/formatter.h>
+#include <zencore/logging/messageonlyformatter.h>
+#include <zencore/logging/sink.h>
#include <zencore/memory/llm.h>
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <spdlog/details/log_msg.h>
-#include <spdlog/pattern_formatter.h>
-#include <spdlog/sinks/sink.h>
-ZEN_THIRD_PARTY_INCLUDES_END
-
#include <atomic>
#include <filesystem>
@@ -19,13 +16,14 @@ namespace zen::logging {
// Basically the same functionality as spdlog::sinks::rotating_file_sink with the biggest difference
// being that it just ignores any errors when writing/rotating files and keeps chugging on.
// It will keep trying to log, and if it starts to work it will continue to log.
-class RotatingFileSink : public spdlog::sinks::sink
+class RotatingFileSink : public Sink
{
public:
RotatingFileSink(const std::filesystem::path& BaseFilename, std::size_t MaxSize, std::size_t MaxFiles, bool RotateOnOpen = false)
: m_BaseFilename(BaseFilename)
, m_MaxSize(MaxSize)
, m_MaxFiles(MaxFiles)
+ , m_Formatter(std::make_unique<MessageOnlyFormatter>())
{
ZEN_MEMSCOPE(ELLMTag::Logging);
@@ -76,18 +74,21 @@ public:
RotatingFileSink& operator=(const RotatingFileSink&) = delete;
RotatingFileSink& operator=(RotatingFileSink&&) = delete;
- virtual void log(const spdlog::details::log_msg& msg) override
+ virtual void Log(const LogMessage& Msg) override
{
ZEN_MEMSCOPE(ELLMTag::Logging);
try
{
- spdlog::memory_buf_t Formatted;
- if (TrySinkIt(msg, Formatted))
+ MemoryBuffer Formatted;
+ if (TrySinkIt(Msg, Formatted))
{
return;
}
- while (true)
+
+ // This intentionally has no limit on the number of retries, see
+ // comment above.
+ for (;;)
{
{
RwLock::ExclusiveLockScope RotateLock(m_Lock);
@@ -113,7 +114,7 @@ public:
// Silently eat errors
}
}
- virtual void flush() override
+ virtual void Flush() override
{
if (!m_NeedFlush)
{
@@ -138,28 +139,14 @@ public:
m_NeedFlush = false;
}
- virtual void set_pattern(const std::string& pattern) override
+ virtual void SetFormatter(std::unique_ptr<Formatter> InFormatter) override
{
ZEN_MEMSCOPE(ELLMTag::Logging);
try
{
RwLock::ExclusiveLockScope _(m_Lock);
- m_Formatter = spdlog::details::make_unique<spdlog::pattern_formatter>(pattern);
- }
- catch (const std::exception&)
- {
- // Silently eat errors
- }
- }
- virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) override
- {
- ZEN_MEMSCOPE(ELLMTag::Logging);
-
- try
- {
- RwLock::ExclusiveLockScope _(m_Lock);
- m_Formatter = std::move(sink_formatter);
+ m_Formatter = std::move(InFormatter);
}
catch (const std::exception&)
{
@@ -186,11 +173,17 @@ private:
return;
}
- // If we fail to rotate, try extending the current log file
m_CurrentSize = m_CurrentFile.FileSize(OutEc);
+ if (OutEc)
+ {
+ // FileSize failed but we have an open file — reset to 0
+ // so we can at least attempt writes from the start
+ m_CurrentSize = 0;
+ OutEc.clear();
+ }
}
- bool TrySinkIt(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& OutFormatted)
+ bool TrySinkIt(const LogMessage& Msg, MemoryBuffer& OutFormatted)
{
ZEN_MEMSCOPE(ELLMTag::Logging);
@@ -199,15 +192,15 @@ private:
{
return false;
}
- m_Formatter->format(msg, OutFormatted);
- size_t add_size = OutFormatted.size();
- size_t write_pos = m_CurrentSize.fetch_add(add_size);
- if (write_pos + add_size > m_MaxSize)
+ m_Formatter->Format(Msg, OutFormatted);
+ size_t AddSize = OutFormatted.size();
+ size_t WritePos = m_CurrentSize.fetch_add(AddSize);
+ if (WritePos + AddSize > m_MaxSize)
{
return false;
}
std::error_code Ec;
- m_CurrentFile.Write(OutFormatted.data(), OutFormatted.size(), write_pos, Ec);
+ m_CurrentFile.Write(OutFormatted.data(), OutFormatted.size(), WritePos, Ec);
if (Ec)
{
return false;
@@ -216,7 +209,7 @@ private:
return true;
}
- bool TrySinkIt(const spdlog::memory_buf_t& Formatted)
+ bool TrySinkIt(const MemoryBuffer& Formatted)
{
ZEN_MEMSCOPE(ELLMTag::Logging);
@@ -225,15 +218,15 @@ private:
{
return false;
}
- size_t add_size = Formatted.size();
- size_t write_pos = m_CurrentSize.fetch_add(add_size);
- if (write_pos + add_size > m_MaxSize)
+ size_t AddSize = Formatted.size();
+ size_t WritePos = m_CurrentSize.fetch_add(AddSize);
+ if (WritePos + AddSize > m_MaxSize)
{
return false;
}
std::error_code Ec;
- m_CurrentFile.Write(Formatted.data(), Formatted.size(), write_pos, Ec);
+ m_CurrentFile.Write(Formatted.data(), Formatted.size(), WritePos, Ec);
if (Ec)
{
return false;
@@ -242,14 +235,14 @@ private:
return true;
}
- RwLock m_Lock;
- const std::filesystem::path m_BaseFilename;
- std::unique_ptr<spdlog::formatter> m_Formatter;
- std::atomic_size_t m_CurrentSize;
- const std::size_t m_MaxSize;
- const std::size_t m_MaxFiles;
- BasicFile m_CurrentFile;
- std::atomic<bool> m_NeedFlush = false;
+ RwLock m_Lock;
+ const std::filesystem::path m_BaseFilename;
+ const std::size_t m_MaxSize;
+ const std::size_t m_MaxFiles;
+ std::unique_ptr<Formatter> m_Formatter;
+ std::atomic_size_t m_CurrentSize;
+ BasicFile m_CurrentFile;
+ std::atomic<bool> m_NeedFlush = false;
};
} // namespace zen::logging
diff --git a/src/zenutil/include/zenutil/logging/testformatter.h b/src/zenutil/include/zenutil/logging/testformatter.h
deleted file mode 100644
index 0b0c191fb..000000000
--- a/src/zenutil/include/zenutil/logging/testformatter.h
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright Epic Games, Inc. All Rights Reserved.
-
-#pragma once
-
-#include <zencore/memory/llm.h>
-
-#include <spdlog/spdlog.h>
-
-namespace zen::logging {
-
-class full_test_formatter final : public spdlog::formatter
-{
-public:
- full_test_formatter(std::string_view LogId, std::chrono::time_point<std::chrono::system_clock> Epoch) : m_Epoch(Epoch), m_LogId(LogId)
- {
- }
-
- virtual std::unique_ptr<formatter> clone() const override
- {
- ZEN_MEMSCOPE(ELLMTag::Logging);
- return std::make_unique<full_test_formatter>(m_LogId, m_Epoch);
- }
-
- static constexpr bool UseDate = false;
-
- virtual void format(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& dest) override
- {
- ZEN_MEMSCOPE(ELLMTag::Logging);
-
- using namespace std::literals;
-
- if constexpr (UseDate)
- {
- auto secs = std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
- if (secs != m_LastLogSecs)
- {
- m_CachedTm = spdlog::details::os::localtime(spdlog::log_clock::to_time_t(msg.time));
- m_LastLogSecs = secs;
- }
- }
-
- const auto& tm_time = m_CachedTm;
-
- // cache the date/time part for the next second.
- auto duration = msg.time - m_Epoch;
- auto secs = std::chrono::duration_cast<std::chrono::seconds>(duration);
-
- if (m_CacheTimestamp != secs)
- {
- RwLock::ExclusiveLockScope _(m_TimestampLock);
-
- m_CachedDatetime.clear();
- m_CachedDatetime.push_back('[');
-
- if constexpr (UseDate)
- {
- spdlog::details::fmt_helper::append_int(tm_time.tm_year + 1900, m_CachedDatetime);
- m_CachedDatetime.push_back('-');
-
- spdlog::details::fmt_helper::pad2(tm_time.tm_mon + 1, m_CachedDatetime);
- m_CachedDatetime.push_back('-');
-
- spdlog::details::fmt_helper::pad2(tm_time.tm_mday, m_CachedDatetime);
- m_CachedDatetime.push_back(' ');
-
- spdlog::details::fmt_helper::pad2(tm_time.tm_hour, m_CachedDatetime);
- m_CachedDatetime.push_back(':');
-
- spdlog::details::fmt_helper::pad2(tm_time.tm_min, m_CachedDatetime);
- m_CachedDatetime.push_back(':');
-
- spdlog::details::fmt_helper::pad2(tm_time.tm_sec, m_CachedDatetime);
- }
- else
- {
- int Count = int(secs.count());
-
- const int LogSecs = Count % 60;
- Count /= 60;
-
- const int LogMins = Count % 60;
- Count /= 60;
-
- const int LogHours = Count;
-
- spdlog::details::fmt_helper::pad2(LogHours, m_CachedDatetime);
- m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(LogMins, m_CachedDatetime);
- m_CachedDatetime.push_back(':');
- spdlog::details::fmt_helper::pad2(LogSecs, m_CachedDatetime);
- }
-
- m_CachedDatetime.push_back('.');
-
- m_CacheTimestamp = secs;
- }
-
- {
- RwLock::SharedLockScope _(m_TimestampLock);
- dest.append(m_CachedDatetime.begin(), m_CachedDatetime.end());
- }
-
- auto millis = spdlog::details::fmt_helper::time_fraction<std::chrono::milliseconds>(msg.time);
- spdlog::details::fmt_helper::pad3(static_cast<uint32_t>(millis.count()), dest);
- dest.push_back(']');
- dest.push_back(' ');
-
- if (!m_LogId.empty())
- {
- dest.push_back('[');
- spdlog::details::fmt_helper::append_string_view(m_LogId, dest);
- dest.push_back(']');
- dest.push_back(' ');
- }
-
- // append logger name if exists
- if (msg.logger_name.size() > 0)
- {
- dest.push_back('[');
- spdlog::details::fmt_helper::append_string_view(msg.logger_name, dest);
- dest.push_back(']');
- dest.push_back(' ');
- }
-
- dest.push_back('[');
- // wrap the level name with color
- msg.color_range_start = dest.size();
- spdlog::details::fmt_helper::append_string_view(spdlog::level::to_string_view(msg.level), dest);
- msg.color_range_end = dest.size();
- dest.push_back(']');
- dest.push_back(' ');
-
- // add source location if present
- if (!msg.source.empty())
- {
- dest.push_back('[');
- const char* filename =
- spdlog::details::short_filename_formatter<spdlog::details::null_scoped_padder>::basename(msg.source.filename);
- spdlog::details::fmt_helper::append_string_view(filename, dest);
- dest.push_back(':');
- spdlog::details::fmt_helper::append_int(msg.source.line, dest);
- dest.push_back(']');
- dest.push_back(' ');
- }
-
- spdlog::details::fmt_helper::append_string_view(msg.payload, dest);
- spdlog::details::fmt_helper::append_string_view("\n"sv, dest);
- }
-
-private:
- std::chrono::time_point<std::chrono::system_clock> m_Epoch;
- std::tm m_CachedTm;
- std::chrono::seconds m_LastLogSecs{std::chrono::seconds(87654321)};
- std::chrono::seconds m_CacheTimestamp{std::chrono::seconds(87654321)};
- spdlog::memory_buf_t m_CachedDatetime;
- std::string m_LogId;
- RwLock m_TimestampLock;
-};
-
-} // namespace zen::logging
diff --git a/src/zenutil/include/zenutil/zenserverprocess.h b/src/zenutil/include/zenutil/zenserverprocess.h
index d0402640b..2a8617162 100644
--- a/src/zenutil/include/zenutil/zenserverprocess.h
+++ b/src/zenutil/include/zenutil/zenserverprocess.h
@@ -42,9 +42,13 @@ public:
std::filesystem::path GetTestRootDir(std::string_view Path);
inline bool IsInitialized() const { return m_IsInitialized; }
inline bool IsTestEnvironment() const { return m_IsTestInstance; }
+ inline bool IsHubEnvironment() const { return m_IsHubInstance; }
inline std::string_view GetServerClass() const { return m_ServerClass; }
inline uint16_t GetNewPortNumber() { return m_NextPortNumber.fetch_add(1); }
+ void SetPassthroughOutput(bool Enable) { m_PassthroughOutput = Enable; }
+ bool IsPassthroughOutput() const { return m_PassthroughOutput; }
+
// The defaults will work for a single root process only. For hierarchical
// setups (e.g., hub managing storage servers), we need to be able to
// allocate distinct child IDs and ports to avoid overlap/conflicts.
@@ -54,9 +58,10 @@ public:
private:
std::filesystem::path m_ProgramBaseDir;
std::filesystem::path m_ChildProcessBaseDir;
- bool m_IsInitialized = false;
- bool m_IsTestInstance = false;
- bool m_IsHubInstance = false;
+ bool m_IsInitialized = false;
+ bool m_IsTestInstance = false;
+ bool m_IsHubInstance = false;
+ bool m_PassthroughOutput = false;
std::string m_ServerClass;
std::atomic_uint16_t m_NextPortNumber{20000};
};
@@ -79,6 +84,7 @@ struct ZenServerInstance
{
kStorageServer, // default
kHubServer,
+ kComputeServer,
};
ZenServerInstance(ZenServerEnvironment& TestEnvironment, ServerMode Mode = ServerMode::kStorageServer);
@@ -96,9 +102,12 @@ struct ZenServerInstance
inline int GetPid() const { return m_Process.Pid(); }
inline void SetOwnerPid(int Pid) { m_OwnerPid = Pid; }
void* GetProcessHandle() const { return m_Process.Handle(); }
- bool IsRunning();
- bool Terminate();
- std::string GetLogOutput() const;
+#if ZEN_PLATFORM_WINDOWS
+ void SetJobObject(JobObject* Job) { m_JobObject = Job; }
+#endif
+ bool IsRunning();
+ bool Terminate();
+ std::string GetLogOutput() const;
inline ServerMode GetServerMode() const { return m_ServerMode; }
@@ -147,6 +156,9 @@ private:
std::string m_Name;
std::filesystem::path m_OutputCapturePath;
std::filesystem::path m_ServerExecutablePath;
+#if ZEN_PLATFORM_WINDOWS
+ JobObject* m_JobObject = nullptr;
+#endif
void CreateShutdownEvent(int BasePort);
void SpawnServer(int BasePort, std::string_view AdditionalServerArgs, int WaitTimeoutMs);
diff --git a/src/zenutil/logging.cpp b/src/zenutil/logging.cpp
index 806b96d52..1258ca155 100644
--- a/src/zenutil/logging.cpp
+++ b/src/zenutil/logging.cpp
@@ -2,18 +2,15 @@
#include "zenutil/logging.h"
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <spdlog/async.h>
-#include <spdlog/async_logger.h>
-#include <spdlog/sinks/ansicolor_sink.h>
-#include <spdlog/sinks/msvc_sink.h>
-#include <spdlog/spdlog.h>
-ZEN_THIRD_PARTY_INCLUDES_END
-
#include <zencore/callstack.h>
#include <zencore/compactbinary.h>
#include <zencore/filesystem.h>
#include <zencore/logging.h>
+#include <zencore/logging/ansicolorsink.h>
+#include <zencore/logging/asyncsink.h>
+#include <zencore/logging/logger.h>
+#include <zencore/logging/msvcsink.h>
+#include <zencore/logging/registry.h>
#include <zencore/memory/llm.h>
#include <zencore/string.h>
#include <zencore/timer.h>
@@ -27,9 +24,9 @@ ZEN_THIRD_PARTY_INCLUDES_END
namespace zen {
static bool g_IsLoggingInitialized;
-spdlog::sink_ptr g_FileSink;
+logging::SinkPtr g_FileSink;
-spdlog::sink_ptr
+logging::SinkPtr
GetFileSink()
{
return g_FileSink;
@@ -52,33 +49,9 @@ BeginInitializeLogging(const LoggingOptions& LogOptions)
zen::logging::InitializeLogging();
zen::logging::EnableVTMode();
- bool IsAsync = LogOptions.AllowAsync;
-
- if (LogOptions.IsDebug)
- {
- IsAsync = false;
- }
-
- if (LogOptions.IsTest)
- {
- IsAsync = false;
- }
-
- if (IsAsync)
- {
- const int QueueSize = 8192;
- const int ThreadCount = 1;
- spdlog::init_thread_pool(QueueSize, ThreadCount, [&] { SetCurrentThreadName("spdlog_async"); });
-
- auto AsyncSink = spdlog::create_async<spdlog::sinks::ansicolor_stdout_sink_mt>("main");
- zen::logging::SetDefault("main");
- }
-
// Sinks
- spdlog::sink_ptr FileSink;
-
- // spdlog can't create directories that starts with `\\?\` so we make sure the folder exists before creating the logger instance
+ logging::SinkPtr FileSink;
if (!LogOptions.AbsLogFile.empty())
{
@@ -87,17 +60,17 @@ BeginInitializeLogging(const LoggingOptions& LogOptions)
zen::CreateDirectories(LogOptions.AbsLogFile.parent_path());
}
- FileSink = std::make_shared<zen::logging::RotatingFileSink>(LogOptions.AbsLogFile,
- /* max size */ 128 * 1024 * 1024,
- /* max files */ 16,
- /* rotate on open */ true);
+ FileSink = logging::SinkPtr(new zen::logging::RotatingFileSink(LogOptions.AbsLogFile,
+ /* max size */ 128 * 1024 * 1024,
+ /* max files */ 16,
+ /* rotate on open */ true));
if (LogOptions.AbsLogFile.extension() == ".json")
{
- FileSink->set_formatter(std::make_unique<logging::json_formatter>(LogOptions.LogId));
+ FileSink->SetFormatter(std::make_unique<logging::JsonFormatter>(LogOptions.LogId));
}
else
{
- FileSink->set_formatter(std::make_unique<logging::full_formatter>(LogOptions.LogId)); // this will have a date prefix
+ FileSink->SetFormatter(std::make_unique<logging::FullFormatter>(LogOptions.LogId)); // this will have a date prefix
}
}
@@ -127,7 +100,7 @@ BeginInitializeLogging(const LoggingOptions& LogOptions)
Message.push_back('\0');
// We use direct ZEN_LOG here instead of ZEN_ERROR as we don't care about *this* code location in the log
- ZEN_LOG(Log(), zen::logging::level::Critical, "{}", Message.data());
+ ZEN_LOG(Log(), zen::logging::Critical, "{}", Message.data());
zen::logging::FlushLogging();
}
catch (const std::exception&)
@@ -143,9 +116,9 @@ BeginInitializeLogging(const LoggingOptions& LogOptions)
// Default
LoggerRef DefaultLogger = zen::logging::Default();
- auto& Sinks = DefaultLogger.SpdLogger->sinks();
- Sinks.clear();
+ // Collect sinks into a local vector first so we can optionally wrap them
+ std::vector<logging::SinkPtr> Sinks;
if (LogOptions.NoConsoleOutput)
{
@@ -153,10 +126,10 @@ BeginInitializeLogging(const LoggingOptions& LogOptions)
}
else
{
- auto ConsoleSink = std::make_shared<spdlog::sinks::ansicolor_stdout_sink_mt>();
+ logging::SinkPtr ConsoleSink(new logging::AnsiColorStdoutSink());
if (LogOptions.QuietConsole)
{
- ConsoleSink->set_level(spdlog::level::warn);
+ ConsoleSink->SetLevel(logging::Warn);
}
Sinks.push_back(ConsoleSink);
}
@@ -169,40 +142,54 @@ BeginInitializeLogging(const LoggingOptions& LogOptions)
#if ZEN_PLATFORM_WINDOWS
if (zen::IsDebuggerPresent() && LogOptions.IsDebug)
{
- auto DebugSink = std::make_shared<spdlog::sinks::msvc_sink_mt>();
- DebugSink->set_level(spdlog::level::debug);
+ logging::SinkPtr DebugSink(new logging::MsvcSink());
+ DebugSink->SetLevel(logging::Debug);
Sinks.push_back(DebugSink);
}
#endif
- spdlog::set_error_handler([](const std::string& msg) {
- if (msg == std::bad_alloc().what())
- {
- // Don't report out of memory in spdlog as we usually log in response to errors which will cause another OOM crashing the
- // program
- return;
- }
- // Bypass zen logging wrapping to reduce potential other error sources
- if (auto ErrLogger = zen::logging::ErrorLog())
+ bool IsAsync = LogOptions.AllowAsync && !LogOptions.IsDebug && !LogOptions.IsTest;
+
+ if (IsAsync)
+ {
+ std::vector<logging::SinkPtr> AsyncSinks;
+ AsyncSinks.emplace_back(new logging::AsyncSink(std::move(Sinks)));
+ DefaultLogger->SetSinks(std::move(AsyncSinks));
+ }
+ else
+ {
+ DefaultLogger->SetSinks(std::move(Sinks));
+ }
+
+ static struct : logging::ErrorHandler
+ {
+ void HandleError(const std::string_view& ErrorMsg) override
{
+ if (ErrorMsg == std::bad_alloc().what())
+ {
+ return;
+ }
+ static constinit logging::LogPoint ErrorPoint{{}, logging::Err, "{}"};
+ if (auto ErrLogger = zen::logging::ErrorLog())
+ {
+ try
+ {
+ ErrLogger->Log(ErrorPoint, fmt::make_format_args(ErrorMsg));
+ }
+ catch (const std::exception&)
+ {
+ }
+ }
try
{
- ErrLogger.SpdLogger->log(spdlog::level::err, msg);
+ Log()->Log(ErrorPoint, fmt::make_format_args(ErrorMsg));
}
catch (const std::exception&)
{
- // Just ignore any errors when in error handler
}
}
- try
- {
- Log().SpdLogger->error(msg);
- }
- catch (const std::exception&)
- {
- // Just ignore any errors when in error handler
- }
- });
+ } s_ErrorHandler;
+ logging::Registry::Instance().SetErrorHandler(&s_ErrorHandler);
g_FileSink = std::move(FileSink);
}
@@ -212,41 +199,47 @@ FinishInitializeLogging(const LoggingOptions& LogOptions)
{
ZEN_MEMSCOPE(ELLMTag::Logging);
- logging::level::LogLevel LogLevel = logging::level::Info;
+ logging::LogLevel LogLevel = logging::Info;
if (LogOptions.IsDebug)
{
- LogLevel = logging::level::Debug;
+ LogLevel = logging::Debug;
}
if (LogOptions.IsTest || LogOptions.IsVerbose)
{
- LogLevel = logging::level::Trace;
+ LogLevel = logging::Trace;
}
// Configure all registered loggers according to settings
logging::RefreshLogLevels(LogLevel);
- spdlog::flush_on(spdlog::level::err);
- spdlog::flush_every(std::chrono::seconds{2});
- spdlog::set_formatter(std::make_unique<logging::full_formatter>(
+ logging::Registry::Instance().FlushOn(logging::Err);
+ logging::Registry::Instance().FlushEvery(std::chrono::seconds{2});
+ logging::Registry::Instance().SetFormatter(std::make_unique<logging::FullFormatter>(
LogOptions.LogId,
std::chrono::system_clock::now() - std::chrono::milliseconds(GetTimeSinceProcessStart()))); // default to duration prefix
+ // If the console logger was initialized before, the above will change the output format
+ // so we need to reset it
+
+ logging::ResetConsoleLog();
+
if (g_FileSink)
{
if (LogOptions.AbsLogFile.extension() == ".json")
{
- g_FileSink->set_formatter(std::make_unique<logging::json_formatter>(LogOptions.LogId));
+ g_FileSink->SetFormatter(std::make_unique<logging::JsonFormatter>(LogOptions.LogId));
}
else
{
- g_FileSink->set_formatter(std::make_unique<logging::full_formatter>(LogOptions.LogId)); // this will have a date prefix
+ g_FileSink->SetFormatter(std::make_unique<logging::FullFormatter>(LogOptions.LogId)); // this will have a date prefix
}
const std::string StartLogTime = zen::DateTime::Now().ToIso8601();
- spdlog::apply_all([&](auto Logger) { Logger->info("log starting at {}", StartLogTime); });
+ static constinit logging::LogPoint LogStartPoint{{}, logging::Info, "log starting at {}"};
+ logging::Registry::Instance().ApplyAll([&](auto Logger) { Logger->Log(LogStartPoint, fmt::make_format_args(StartLogTime)); });
}
g_IsLoggingInitialized = true;
@@ -263,7 +256,7 @@ ShutdownLogging()
zen::logging::ShutdownLogging();
- g_FileSink.reset();
+ g_FileSink = nullptr;
}
} // namespace zen
diff --git a/src/zenutil/rpcrecording.cpp b/src/zenutil/rpcrecording.cpp
index 54f27dee7..28a0091cb 100644
--- a/src/zenutil/rpcrecording.cpp
+++ b/src/zenutil/rpcrecording.cpp
@@ -1119,7 +1119,7 @@ rpcrecord_forcelink()
{
}
-TEST_SUITE_BEGIN("rpc.recording");
+TEST_SUITE_BEGIN("util.rpcrecording");
TEST_CASE("rpc.record")
{
diff --git a/src/zenutil/wildcard.cpp b/src/zenutil/wildcard.cpp
index 7a44c0498..7f2f77780 100644
--- a/src/zenutil/wildcard.cpp
+++ b/src/zenutil/wildcard.cpp
@@ -118,6 +118,8 @@ wildcard_forcelink()
{
}
+TEST_SUITE_BEGIN("util.wildcard");
+
TEST_CASE("Wildcard")
{
CHECK(MatchWildcard("*.*", "normal.txt", true));
@@ -151,5 +153,7 @@ TEST_CASE("Wildcard")
CHECK(MatchWildcard("*.d", "dir/path.d", true));
}
+TEST_SUITE_END();
+
#endif
} // namespace zen
diff --git a/src/zenutil/xmake.lua b/src/zenutil/xmake.lua
index bc33adf9e..1d5be5977 100644
--- a/src/zenutil/xmake.lua
+++ b/src/zenutil/xmake.lua
@@ -6,7 +6,7 @@ target('zenutil')
add_headerfiles("**.h")
add_files("**.cpp")
add_includedirs("include", {public=true})
- add_deps("zencore", "zenhttp", "spdlog")
+ add_deps("zencore", "zenhttp")
add_deps("cxxopts")
add_deps("robin-map")
diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp
index ef2a4fda5..b09c2d89a 100644
--- a/src/zenutil/zenserverprocess.cpp
+++ b/src/zenutil/zenserverprocess.cpp
@@ -787,6 +787,8 @@ ToString(ZenServerInstance::ServerMode Mode)
return "storage"sv;
case ZenServerInstance::ServerMode::kHubServer:
return "hub"sv;
+ case ZenServerInstance::ServerMode::kComputeServer:
+ return "compute"sv;
default:
return "invalid"sv;
}
@@ -808,6 +810,10 @@ ZenServerInstance::SpawnServerInternal(int ChildId, std::string_view ServerArgs,
{
CommandLine << " hub";
}
+ else if (m_ServerMode == ServerMode::kComputeServer)
+ {
+ CommandLine << " compute";
+ }
CommandLine << " --child-id " << ChildEventName;
@@ -829,10 +835,18 @@ ZenServerInstance::SpawnServerInternal(int ChildId, std::string_view ServerArgs,
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 ? std::filesystem::path{} : std::filesystem::temp_directory_path() / ("zenserver_" + m_Name + ".log");
- CreateProcOptions CreateOptions = {.WorkingDirectory = &CurrentDirectory, .Flags = CreationFlags, .StdoutFile = OutputPath};
- CreateProcResult ChildPid = CreateProc(Executable, CommandLine.ToView(), CreateOptions);
+ const std::filesystem::path OutputPath = (OpenConsole || m_Env.IsPassthroughOutput())
+ ? std::filesystem::path{}
+ : std::filesystem::temp_directory_path() / ("zenserver_" + m_Name + ".log");
+ CreateProcOptions CreateOptions = {
+ .WorkingDirectory = &CurrentDirectory,
+ .Flags = CreationFlags,
+ .StdoutFile = OutputPath,
+#if ZEN_PLATFORM_WINDOWS
+ .AssignToJob = m_JobObject,
+#endif
+ };
+ CreateProcResult ChildPid = CreateProc(Executable, CommandLine.ToView(), CreateOptions);
#if ZEN_PLATFORM_WINDOWS
if (!ChildPid)
{
@@ -841,6 +855,12 @@ ZenServerInstance::SpawnServerInternal(int ChildId, std::string_view ServerArgs,
{
ZEN_DEBUG("Regular spawn failed - spawning elevated server");
CreateOptions.Flags |= CreateProcOptions::Flag_Elevated;
+ // ShellExecuteEx (used by the elevated path) does not support job object assignment
+ if (CreateOptions.AssignToJob)
+ {
+ ZEN_WARN("Elevated process spawn does not support job object assignment; child will not be auto-terminated on parent exit");
+ CreateOptions.AssignToJob = nullptr;
+ }
ChildPid = CreateProc(Executable, CommandLine.ToView(), CreateOptions);
}
else
@@ -934,7 +954,8 @@ ZenServerInstance::SpawnServer(int BasePort, std::string_view AdditionalServerAr
CommandLine << " " << AdditionalServerArgs;
}
- SpawnServerInternal(ChildId, CommandLine, !IsTest, WaitTimeoutMs);
+ const bool OpenConsole = !IsTest && !m_Env.IsHubEnvironment();
+ SpawnServerInternal(ChildId, CommandLine, OpenConsole, WaitTimeoutMs);
}
void
diff --git a/src/zenutil/zenutil.cpp b/src/zenutil/zenutil.cpp
index 51c1ee72e..291dbeadd 100644
--- a/src/zenutil/zenutil.cpp
+++ b/src/zenutil/zenutil.cpp
@@ -5,7 +5,7 @@
#if ZEN_WITH_TESTS
# include <zenutil/rpcrecording.h>
-# include <zenutil/commandlineoptions.h>
+# include <zenutil/config/commandlineoptions.h>
# include <zenutil/wildcard.h>
namespace zen {