diff options
| author | Liam Mitchell <[email protected]> | 2026-03-09 19:06:36 -0700 |
|---|---|---|
| committer | Liam Mitchell <[email protected]> | 2026-03-09 19:06:36 -0700 |
| commit | d1abc50ee9d4fb72efc646e17decafea741caa34 (patch) | |
| tree | e4288e00f2f7ca0391b83d986efcb69d3ba66a83 /src/zenutil | |
| parent | Allow requests with invalid content-types unless specified in command line or... (diff) | |
| parent | updated chunk–block analyser (#818) (diff) | |
| download | zen-d1abc50ee9d4fb72efc646e17decafea741caa34.tar.xz zen-d1abc50ee9d4fb72efc646e17decafea741caa34.zip | |
Merge branch 'main' into lm/restrict-content-type
Diffstat (limited to 'src/zenutil')
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 { |