aboutsummaryrefslogtreecommitdiff
path: root/src/zencore
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-13 14:05:03 +0200
committerGitHub Enterprise <[email protected]>2026-04-13 14:05:03 +0200
commitc49e5b15e0f86080d7d33e4e31aecfb701f8f96f (patch)
treeff41dcec20502c0cc4cf853222273f50d025cda3 /src/zencore
parentAdd MemoryCidStore and ChunkStore interface (#940) (diff)
downloadzen-c49e5b15e0f86080d7d33e4e31aecfb701f8f96f.tar.xz
zen-c49e5b15e0f86080d7d33e4e31aecfb701f8f96f.zip
Some minor polish from tourist branch (#949)
- Replace per-type fmt::formatter specializations (StringBuilderBase, NiceBase) with a single generic formatter using a HasStringViewConversion concept - Add ThousandsNum for comma-separated integer formatting (e.g. "1,234,567") - Thread naming now accepts a sort hint for trace ordering - Fix main thread trace registration to use actual thread ID and sort first - Add ExpandEnvironmentVariables() for expanding %VAR% references in strings, with tests - Add ParseHexBytes() overload with expected byte count validation - Add Flag_BelowNormalPriority to CreateProcOptions (BELOW_NORMAL_PRIORITY_CLASS on Windows, setpriority on POSIX) - Add PrettyScroll progress bar mode that pins the status line to the bottom of the terminal using scroll regions, with signal handler cleanup for Ctrl+C/SIGTERM
Diffstat (limited to 'src/zencore')
-rw-r--r--src/zencore/compactbinaryjson.cpp34
-rw-r--r--src/zencore/filesystem.cpp68
-rw-r--r--src/zencore/include/zencore/filesystem.h4
-rw-r--r--src/zencore/include/zencore/fmtutils.h22
-rw-r--r--src/zencore/include/zencore/process.h3
-rw-r--r--src/zencore/include/zencore/string.h37
-rw-r--r--src/zencore/include/zencore/thread.h2
-rw-r--r--src/zencore/process.cpp19
-rw-r--r--src/zencore/string.cpp28
-rw-r--r--src/zencore/thread.cpp4
-rw-r--r--src/zencore/trace.cpp3
11 files changed, 204 insertions, 20 deletions
diff --git a/src/zencore/compactbinaryjson.cpp b/src/zencore/compactbinaryjson.cpp
index da560a449..5bfbd5e3e 100644
--- a/src/zencore/compactbinaryjson.cpp
+++ b/src/zencore/compactbinaryjson.cpp
@@ -11,6 +11,8 @@
#include <zencore/testing.h>
#include <fmt/format.h>
+#include <cmath>
+#include <limits>
#include <vector>
ZEN_THIRD_PARTY_INCLUDES_START
@@ -570,13 +572,37 @@ private:
break;
case Json::Type::NUMBER:
{
- if (FieldName.empty())
- {
- Writer.AddFloat(Json.number_value());
+ // If the JSON number has no fractional part and fits in an int64,
+ // store it as an integer so that AsInt32/AsInt64 work without
+ // requiring callers to go through AsFloat.
+ double Value = Json.number_value();
+ double IntPart;
+ bool IsIntegral = (std::modf(Value, &IntPart) == 0.0) &&
+ Value >= static_cast<double>(std::numeric_limits<int64_t>::min()) &&
+ Value <= static_cast<double>(std::numeric_limits<int64_t>::max());
+
+ if (IsIntegral)
+ {
+ int64_t IntValue = static_cast<int64_t>(Value);
+ if (FieldName.empty())
+ {
+ Writer.AddInteger(IntValue);
+ }
+ else
+ {
+ Writer.AddInteger(FieldName, IntValue);
+ }
}
else
{
- Writer.AddFloat(FieldName, Json.number_value());
+ if (FieldName.empty())
+ {
+ Writer.AddFloat(Value);
+ }
+ else
+ {
+ Writer.AddFloat(FieldName, Value);
+ }
}
}
break;
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp
index cae6ba5d4..518146648 100644
--- a/src/zencore/filesystem.cpp
+++ b/src/zencore/filesystem.cpp
@@ -2977,6 +2977,35 @@ GetEnvVariable(std::string_view VariableName)
return "";
}
+std::string
+ExpandEnvironmentVariables(std::string_view Input)
+{
+ std::string Result;
+ Result.reserve(Input.size());
+
+ for (size_t i = 0; i < Input.size(); ++i)
+ {
+ if (Input[i] == '%')
+ {
+ size_t End = Input.find('%', i + 1);
+ if (End != std::string_view::npos && End > i + 1)
+ {
+ std::string_view VarName = Input.substr(i + 1, End - i - 1);
+ std::string Value = GetEnvVariable(VarName);
+ if (!Value.empty())
+ {
+ Result += Value;
+ i = End;
+ continue;
+ }
+ }
+ }
+ Result += Input[i];
+ }
+
+ return Result;
+}
+
std::error_code
RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles)
{
@@ -4122,6 +4151,45 @@ TEST_CASE("filesystem.MakeSafeAbsolutePath")
# endif // ZEN_PLATFORM_WINDOWS
}
+TEST_CASE("ExpandEnvironmentVariables")
+{
+ // No variables — pass-through
+ CHECK_EQ(ExpandEnvironmentVariables("plain/path"), "plain/path");
+ CHECK_EQ(ExpandEnvironmentVariables(""), "");
+
+ // Single percent sign is not a variable reference
+ CHECK_EQ(ExpandEnvironmentVariables("50%"), "50%");
+
+ // Empty variable name (%%) is not expanded
+ CHECK_EQ(ExpandEnvironmentVariables("%%"), "%%");
+
+ // Known variable
+# if ZEN_PLATFORM_WINDOWS
+ // PATH is always set on Windows
+ std::string PathValue = GetEnvVariable("PATH");
+ CHECK(!PathValue.empty());
+ CHECK_EQ(ExpandEnvironmentVariables("%PATH%"), PathValue);
+ CHECK_EQ(ExpandEnvironmentVariables("prefix/%PATH%/suffix"), "prefix/" + PathValue + "/suffix");
+# else
+ std::string HomeValue = GetEnvVariable("HOME");
+ CHECK(!HomeValue.empty());
+ CHECK_EQ(ExpandEnvironmentVariables("%HOME%"), HomeValue);
+ CHECK_EQ(ExpandEnvironmentVariables("prefix/%HOME%/suffix"), "prefix/" + HomeValue + "/suffix");
+# endif
+
+ // Unknown variable is left unexpanded
+ CHECK_EQ(ExpandEnvironmentVariables("%ZEN_UNLIKELY_SET_VAR_12345%"), "%ZEN_UNLIKELY_SET_VAR_12345%");
+
+ // Multiple variables
+# if ZEN_PLATFORM_WINDOWS
+ std::string OSValue = GetEnvVariable("OS");
+ if (!OSValue.empty())
+ {
+ CHECK_EQ(ExpandEnvironmentVariables("%PATH%/%OS%"), PathValue + "/" + OSValue);
+ }
+# endif
+}
+
TEST_SUITE_END();
#endif
diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h
index 6dc159a83..73769cdb4 100644
--- a/src/zencore/include/zencore/filesystem.h
+++ b/src/zencore/include/zencore/filesystem.h
@@ -400,6 +400,10 @@ void GetDirectoryContent(const std::filesystem::path& RootDir,
std::string GetEnvVariable(std::string_view VariableName);
+// Expands %VAR% environment variable references in a string.
+// Unknown or empty variables are left unexpanded.
+std::string ExpandEnvironmentVariables(std::string_view Input);
+
std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName);
std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles);
diff --git a/src/zencore/include/zencore/fmtutils.h b/src/zencore/include/zencore/fmtutils.h
index 4ec05f901..2a829d2d5 100644
--- a/src/zencore/include/zencore/fmtutils.h
+++ b/src/zencore/include/zencore/fmtutils.h
@@ -38,27 +38,25 @@ struct fmt::formatter<T> : fmt::formatter<std::string_view>
}
};
-// Custom formatting for some zencore types
+// Generic formatter for any type that is explicitly convertible to std::string_view.
+// This covers NiceNum, NiceBytes, ThousandsNum, StringBuilder, and similar types
+// without needing per-type fmt::formatter specializations.
template<typename T>
-requires DerivedFrom<T, zen::StringBuilderBase>
-struct fmt::formatter<T> : fmt::formatter<std::string_view>
+concept HasStringViewConversion = std::is_class_v<T> && requires(const T& v)
{
- template<typename FormatContext>
- auto format(const zen::StringBuilderBase& a, FormatContext& ctx) const
{
- return fmt::formatter<std::string_view>::format(a.ToView(), ctx);
- }
-};
+ std::string_view(v)
+ } -> std::same_as<std::string_view>;
+} && !HasFreeToString<T> && !std::is_same_v<T, std::string> && !std::is_same_v<T, std::string_view>;
-template<typename T>
-requires DerivedFrom<T, zen::NiceBase>
+template<HasStringViewConversion T>
struct fmt::formatter<T> : fmt::formatter<std::string_view>
{
template<typename FormatContext>
- auto format(const zen::NiceBase& a, FormatContext& ctx) const
+ auto format(const T& Value, FormatContext& ctx) const
{
- return fmt::formatter<std::string_view>::format(std::string_view(a), ctx);
+ return fmt::formatter<std::string_view>::format(std::string_view(Value), ctx);
}
};
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index 8cbed781d..19804795b 100644
--- a/src/zencore/include/zencore/process.h
+++ b/src/zencore/include/zencore/process.h
@@ -183,6 +183,9 @@ struct CreateProcOptions
// Flag_NoConsole the child still gets a console (and a conhost.exe) but no visible
// window. Use this when the child needs a console for stdio but should not show a window.
Flag_NoWindow = 1 << 5,
+ // Launch the child at below-normal scheduling priority.
+ // On Windows: BELOW_NORMAL_PRIORITY_CLASS. On POSIX: nice(5).
+ Flag_BelowNormalPriority = 1 << 6,
};
const std::filesystem::path* WorkingDirectory = nullptr;
diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h
index 60293a313..308a8a7d2 100644
--- a/src/zencore/include/zencore/string.h
+++ b/src/zencore/include/zencore/string.h
@@ -609,6 +609,17 @@ ParseHexBytes(std::string_view InputString, uint8_t* OutPtr)
return ParseHexBytes(InputString.data(), InputString.size(), OutPtr);
}
+/** Parse hex string into a byte buffer, validating that the hex string is exactly ExpectedByteCount * 2 characters. */
+inline bool
+ParseHexBytes(std::string_view InputString, uint8_t* OutPtr, size_t ExpectedByteCount)
+{
+ if (InputString.size() != ExpectedByteCount * 2)
+ {
+ return false;
+ }
+ return ParseHexBytes(InputString.data(), InputString.size(), OutPtr);
+}
+
inline void
ToHexBytes(const uint8_t* InputData, size_t ByteCount, char* OutString)
{
@@ -722,6 +733,32 @@ struct NiceNum : public NiceBase
inline NiceNum(uint64_t Num) { NiceNumToBuffer(Num, m_Buffer); }
};
+size_t ThousandsToBuffer(uint64_t Num, std::span<char> Buffer);
+
+/// Integer formatted with comma thousands separators (e.g. "1,234,567")
+struct ThousandsNum
+{
+ inline ThousandsNum(UnsignedIntegral auto Number) { ThousandsToBuffer(uint64_t(Number), m_Buffer); }
+ inline ThousandsNum(SignedIntegral auto Number)
+ {
+ if (Number < 0)
+ {
+ m_Buffer[0] = '-';
+ ThousandsToBuffer(uint64_t(-Number), std::span<char>(m_Buffer + 1, sizeof(m_Buffer) - 1));
+ }
+ else
+ {
+ ThousandsToBuffer(uint64_t(Number), m_Buffer);
+ }
+ }
+
+ inline const char* c_str() const { return m_Buffer; }
+ inline operator std::string_view() const { return std::string_view(m_Buffer); }
+
+private:
+ char m_Buffer[28]; // max uint64: "18,446,744,073,709,551,615" (26) + NUL + sign
+};
+
struct NiceBytes : public NiceBase
{
inline NiceBytes(uint64_t Num) { NiceBytesToBuffer(Num, m_Buffer); }
diff --git a/src/zencore/include/zencore/thread.h b/src/zencore/include/zencore/thread.h
index 56ce5904b..0f7733df5 100644
--- a/src/zencore/include/zencore/thread.h
+++ b/src/zencore/include/zencore/thread.h
@@ -14,7 +14,7 @@
namespace zen {
-void SetCurrentThreadName(std::string_view ThreadName);
+void SetCurrentThreadName(std::string_view ThreadName, int32_t SortHint = 0);
/**
* Reader-writer lock
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index ee821944a..aa41c82ff 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -28,6 +28,7 @@ ZEN_THIRD_PARTY_INCLUDES_START
# include <pthread.h>
# include <signal.h>
# include <sys/file.h>
+# include <sys/resource.h>
# include <sys/sem.h>
# include <sys/stat.h>
# include <sys/syscall.h>
@@ -833,6 +834,10 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
{
CreationFlags |= CREATE_NEW_PROCESS_GROUP;
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ CreationFlags |= BELOW_NORMAL_PRIORITY_CLASS;
+ }
if (AssignToJob)
{
CreationFlags |= CREATE_SUSPENDED;
@@ -1043,6 +1048,10 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C
{
CreateProcFlags |= CREATE_NO_WINDOW;
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ CreateProcFlags |= BELOW_NORMAL_PRIORITY_CLASS;
+ }
if (AssignToJob)
{
CreateProcFlags |= CREATE_SUSPENDED;
@@ -1201,6 +1210,11 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
_exit(127);
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ setpriority(PRIO_PROCESS, ChildPid, 5);
+ }
+
return ChildPid;
#else // macOS
std::vector<char*> ArgV;
@@ -1280,6 +1294,11 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
ThrowSystemError(Err, "Failed to posix_spawn a new child process");
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ setpriority(PRIO_PROCESS, ChildPid, 5);
+ }
+
return int(ChildPid);
#endif
}
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
index 358722b0b..df7d71250 100644
--- a/src/zencore/string.cpp
+++ b/src/zencore/string.cpp
@@ -381,6 +381,34 @@ NiceNumGeneral(uint64_t Num, std::span<char> Buffer, NicenumFormat Format)
}
size_t
+ThousandsToBuffer(uint64_t Num, std::span<char> Buffer)
+{
+ // Format into a temporary buffer without separators
+ char Tmp[24];
+ int Len = snprintf(Tmp, sizeof(Tmp), "%llu", (unsigned long long)Num);
+
+ // Insert comma separators
+ int SepCount = (Len - 1) / 3;
+ int TotalLen = Len + SepCount;
+ ZEN_ASSERT(TotalLen < (int)Buffer.size());
+
+ int Src = Len - 1;
+ int Dst = TotalLen;
+ Buffer[Dst--] = '\0';
+
+ for (int i = 0; Src >= 0; i++)
+ {
+ if (i > 0 && i % 3 == 0)
+ {
+ Buffer[Dst--] = ',';
+ }
+ Buffer[Dst--] = Tmp[Src--];
+ }
+
+ return TotalLen;
+}
+
+size_t
NiceNumToBuffer(uint64_t Num, std::span<char> Buffer)
{
return NiceNumGeneral(Num, Buffer, kNicenum1024);
diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp
index 067e66c0d..fd72afaa7 100644
--- a/src/zencore/thread.cpp
+++ b/src/zencore/thread.cpp
@@ -99,7 +99,7 @@ SetNameInternal(DWORD thread_id, const char* name)
#endif
void
-SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName)
+SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName, [[maybe_unused]] int32_t SortHint)
{
constexpr std::string_view::size_type MaxThreadNameLength = 255;
std::string_view LimitedThreadName = ThreadName.substr(0, MaxThreadNameLength);
@@ -108,7 +108,7 @@ SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName)
const int ThreadId = GetCurrentThreadId();
#if ZEN_WITH_TRACE
- trace::ThreadRegister(ThreadNameZ.c_str(), /* system id */ ThreadId, /* sort id */ 0);
+ trace::ThreadRegister(ThreadNameZ.c_str(), /* system id */ ThreadId, /* sort id */ SortHint);
#endif // ZEN_WITH_TRACE
#if ZEN_PLATFORM_WINDOWS
diff --git a/src/zencore/trace.cpp b/src/zencore/trace.cpp
index 7c195e69f..d7084bbd1 100644
--- a/src/zencore/trace.cpp
+++ b/src/zencore/trace.cpp
@@ -6,6 +6,7 @@
# include <zencore/zencore.h>
# include <zencore/commandline.h>
# include <zencore/string.h>
+# include <zencore/thread.h>
# include <zencore/logging.h>
# define TRACE_IMPLEMENT 1
@@ -121,7 +122,7 @@ TraceInit(std::string_view ProgramName)
const char* CommandLineString = "";
# endif
- trace::ThreadRegister("main", /* system id */ 0, /* sort id */ 0);
+ trace::ThreadRegister("main", /* system id */ GetCurrentThreadId(), /* sort id */ -1);
trace::DescribeSession(ProgramName,
# if ZEN_BUILD_DEBUG
trace::Build::Debug,