diff options
| author | Stefan Boberg <[email protected]> | 2023-11-21 14:52:56 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-11-21 14:52:56 +0100 |
| commit | 8e1cb139d817880c557d407c363eb838c0893f5a (patch) | |
| tree | 69da94b1df3f2fd1d19e623084c88543c0ce85f6 /src | |
| parent | basic ZEN_ASSERT_FORMAT implementation (#556) (diff) | |
| download | zen-8e1cb139d817880c557d407c363eb838c0893f5a.tar.xz zen-8e1cb139d817880c557d407c363eb838c0893f5a.zip | |
zen run command (#552)
initial version -- this is primarily intended to be used for running stress tests and/or benchmarks
example usage:
`zen run -n 10 -- zenserver-test`
`zen run -n 10 -- zenserver-test --ts=core.assert` run zenserver-test 10 times (testing only the `core.assert` test suite)
`zen run --time 600 --basepath=d:\test_dir\test1 -- zenserver-test` keeps spawning new instances for 10 minutes (600 seconds)
Diffstat (limited to 'src')
| -rw-r--r-- | src/zen/cmds/admin_cmd.h | 10 | ||||
| -rw-r--r-- | src/zen/cmds/bench_cmd.cpp | 4 | ||||
| -rw-r--r-- | src/zen/cmds/bench_cmd.h | 5 | ||||
| -rw-r--r-- | src/zen/cmds/cache_cmd.cpp | 4 | ||||
| -rw-r--r-- | src/zen/cmds/cache_cmd.h | 12 | ||||
| -rw-r--r-- | src/zen/cmds/copy_cmd.h | 1 | ||||
| -rw-r--r-- | src/zen/cmds/dedup_cmd.h | 1 | ||||
| -rw-r--r-- | src/zen/cmds/hash_cmd.cpp | 172 | ||||
| -rw-r--r-- | src/zen/cmds/hash_cmd.h | 27 | ||||
| -rw-r--r-- | src/zen/cmds/print_cmd.h | 2 | ||||
| -rw-r--r-- | src/zen/cmds/projectstore_cmd.h | 29 | ||||
| -rw-r--r-- | src/zen/cmds/rpcreplay_cmd.h | 6 | ||||
| -rw-r--r-- | src/zen/cmds/run_cmd.cpp | 197 | ||||
| -rw-r--r-- | src/zen/cmds/run_cmd.h | 27 | ||||
| -rw-r--r-- | src/zen/cmds/vfs_cmd.h | 2 | ||||
| -rw-r--r-- | src/zen/internalfile.cpp | 306 | ||||
| -rw-r--r-- | src/zen/internalfile.h | 64 | ||||
| -rw-r--r-- | src/zen/zen.cpp | 184 | ||||
| -rw-r--r-- | src/zen/zen.h | 29 | ||||
| -rw-r--r-- | src/zencore/filesystem.cpp | 119 | ||||
| -rw-r--r-- | src/zencore/include/zencore/filesystem.h | 3 | ||||
| -rw-r--r-- | src/zencore/include/zencore/process.h | 4 | ||||
| -rw-r--r-- | src/zencore/process.cpp | 61 | ||||
| -rw-r--r-- | src/zenserver-test/zenserver-test.cpp | 11 |
24 files changed, 605 insertions, 675 deletions
diff --git a/src/zen/cmds/admin_cmd.h b/src/zen/cmds/admin_cmd.h index 356f58363..1938ad1fa 100644 --- a/src/zen/cmds/admin_cmd.h +++ b/src/zen/cmds/admin_cmd.h @@ -10,7 +10,7 @@ namespace zen { /** Scrub storage */ -class ScrubCommand : public ZenCmdBase +class ScrubCommand : public StorageCommand { public: ScrubCommand(); @@ -26,7 +26,7 @@ private: /** Garbage collect storage */ -class GcCommand : public ZenCmdBase +class GcCommand : public StorageCommand { public: GcCommand(); @@ -47,7 +47,7 @@ private: bool m_ForceUseGCV2{false}; }; -class GcStatusCommand : public ZenCmdBase +class GcStatusCommand : public StorageCommand { public: GcStatusCommand(); @@ -101,7 +101,7 @@ private: /** Flush storage */ -class FlushCommand : public ZenCmdBase +class FlushCommand : public StorageCommand { public: FlushCommand(); @@ -117,7 +117,7 @@ private: /** Copy state */ -class CopyStateCommand : public ZenCmdBase +class CopyStateCommand : public StorageCommand { public: CopyStateCommand(); diff --git a/src/zen/cmds/bench_cmd.cpp b/src/zen/cmds/bench_cmd.cpp index 4b5c471de..d0a513a1d 100644 --- a/src/zen/cmds/bench_cmd.cpp +++ b/src/zen/cmds/bench_cmd.cpp @@ -131,6 +131,8 @@ EmptyStandByList() #endif +namespace zen { + BenchCommand::BenchCommand() { m_Options.add_options()("h,help", "Print help"); @@ -216,3 +218,5 @@ BenchCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) return 0; } + +} // namespace zen diff --git a/src/zen/cmds/bench_cmd.h b/src/zen/cmds/bench_cmd.h index 8a8bd4a7c..29d7fcc08 100644 --- a/src/zen/cmds/bench_cmd.h +++ b/src/zen/cmds/bench_cmd.h @@ -4,6 +4,8 @@ #include "../zen.h" +namespace zen { + class BenchCommand : public ZenCmdBase { public: @@ -12,9 +14,12 @@ public: virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } + virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: cxxopts::Options m_Options{"bench", "Benchmarking utility command"}; bool m_PurgeStandbyLists = false; bool m_SingleProcess = false; }; + +} // namespace zen diff --git a/src/zen/cmds/cache_cmd.cpp b/src/zen/cmds/cache_cmd.cpp index 1bf6ee60e..823f10f1c 100644 --- a/src/zen/cmds/cache_cmd.cpp +++ b/src/zen/cmds/cache_cmd.cpp @@ -14,6 +14,8 @@ ZEN_THIRD_PARTY_INCLUDES_START #include <cpr/cpr.h> ZEN_THIRD_PARTY_INCLUDES_END +namespace zen { + DropCommand::DropCommand() { m_Options.add_options()("h,help", "Print help"); @@ -302,3 +304,5 @@ CacheDetailsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** ar return 1; } + +} // namespace zen diff --git a/src/zen/cmds/cache_cmd.h b/src/zen/cmds/cache_cmd.h index 1f368bdec..80079c452 100644 --- a/src/zen/cmds/cache_cmd.h +++ b/src/zen/cmds/cache_cmd.h @@ -4,7 +4,9 @@ #include "../zen.h" -class DropCommand : public ZenCmdBase +namespace zen { + +class DropCommand : public CacheStoreCommand { public: DropCommand(); @@ -20,7 +22,7 @@ private: std::string m_BucketName; }; -class CacheInfoCommand : public ZenCmdBase +class CacheInfoCommand : public CacheStoreCommand { public: CacheInfoCommand(); @@ -35,7 +37,7 @@ private: std::string m_BucketName; }; -class CacheStatsCommand : public ZenCmdBase +class CacheStatsCommand : public CacheStoreCommand { public: CacheStatsCommand(); @@ -48,7 +50,7 @@ private: std::string m_HostName; }; -class CacheDetailsCommand : public ZenCmdBase +class CacheDetailsCommand : public CacheStoreCommand { public: CacheDetailsCommand(); @@ -66,3 +68,5 @@ private: std::string m_Bucket; std::string m_ValueKey; }; + +} // namespace zen diff --git a/src/zen/cmds/copy_cmd.h b/src/zen/cmds/copy_cmd.h index 549114160..0dbebddfa 100644 --- a/src/zen/cmds/copy_cmd.h +++ b/src/zen/cmds/copy_cmd.h @@ -16,6 +16,7 @@ public: virtual cxxopts::Options& Options() override { return m_Options; } virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: cxxopts::Options m_Options{"copy", "Copy files"}; diff --git a/src/zen/cmds/dedup_cmd.h b/src/zen/cmds/dedup_cmd.h index 6318704f5..c4f0068e4 100644 --- a/src/zen/cmds/dedup_cmd.h +++ b/src/zen/cmds/dedup_cmd.h @@ -16,6 +16,7 @@ public: virtual cxxopts::Options& Options() override { return m_Options; } virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: cxxopts::Options m_Options{"dedup", "Deduplicate files"}; diff --git a/src/zen/cmds/hash_cmd.cpp b/src/zen/cmds/hash_cmd.cpp deleted file mode 100644 index f5541906b..000000000 --- a/src/zen/cmds/hash_cmd.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "hash_cmd.h" - -#include <zencore/blake3.h> -#include <zencore/fmtutils.h> -#include <zencore/logging.h> -#include <zencore/string.h> -#include <zencore/timer.h> - -#if ZEN_PLATFORM_WINDOWS -# include <ppl.h> -#endif - -namespace zen { - -//////////////////////////////////////////////////////////////////////////////// - -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - -namespace Concurrency { - - template<typename IterType, typename LambdaType> - void parallel_for_each(IterType Cursor, IterType End, const LambdaType& Lambda) - { - for (; Cursor < End; ++Cursor) - { - Lambda(*Cursor); - } - } - - template<typename T> - struct combinable - { - combinable<T>& local() { return *this; } - - void operator+=(T Rhs) { Value += Rhs; } - - template<typename LambdaType> - void combine_each(const LambdaType& Lambda) - { - Lambda(Value); - } - - T Value = 0; - }; - -} // namespace Concurrency - -#endif // ZEN_PLATFORM_LINUX|MAC - -//////////////////////////////////////////////////////////////////////////////// - -HashCommand::HashCommand() -{ - m_Options.add_options()("d,dir", "Directory to scan", cxxopts::value<std::string>(m_ScanDirectory))( - "o,output", - "Output file", - cxxopts::value<std::string>(m_OutputFile)); -} - -HashCommand::~HashCommand() = default; - -int -HashCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) -{ - ZEN_UNUSED(GlobalOptions); - - if (!ParseOptions(argc, argv)) - { - return 0; - } - - bool valid = m_ScanDirectory.length(); - - if (!valid) - throw zen::OptionParseException("Hash command requires a directory to scan"); - - // Gather list of files to process - - ZEN_CONSOLE("Gathering files from {}", m_ScanDirectory); - - struct FileEntry - { - std::filesystem::path FilePath; - zen::BLAKE3 FileHash; - }; - - std::vector<FileEntry> FileList; - uint64_t FileBytes = 0; - - std::filesystem::path ScanDirectoryPath{m_ScanDirectory}; - - for (const std::filesystem::directory_entry& Entry : std::filesystem::recursive_directory_iterator(ScanDirectoryPath)) - { - if (Entry.is_regular_file()) - { - FileList.push_back({Entry.path()}); - FileBytes += Entry.file_size(); - } - } - - ZEN_CONSOLE("Gathered {} files, total size {}", FileList.size(), zen::NiceBytes(FileBytes)); - - Concurrency::combinable<uint64_t> TotalBytes; - - auto hashFile = [&](FileEntry& File) { - InternalFile InputFile; - InputFile.OpenRead(File.FilePath); - const uint8_t* DataPointer = (const uint8_t*)InputFile.MemoryMapFile(); - const size_t DataSize = InputFile.GetFileSize(); - - File.FileHash = zen::BLAKE3::HashMemory(DataPointer, DataSize); - - TotalBytes.local() += DataSize; - }; - - // Process them as quickly as possible - - zen::Stopwatch Timer; - -#if 1 - Concurrency::parallel_for_each(begin(FileList), end(FileList), [&](auto& file) { hashFile(file); }); -#else - for (const auto& file : FileList) - { - hashFile(file); - } -#endif - - size_t TotalByteCount = 0; - - TotalBytes.combine_each([&](size_t Total) { TotalByteCount += Total; }); - - const uint64_t ElapsedMs = Timer.GetElapsedTimeMs(); - ZEN_CONSOLE("Scanned {} files in {}", FileList.size(), zen::NiceTimeSpanMs(ElapsedMs)); - ZEN_CONSOLE("Total bytes {} ({})", zen::NiceBytes(TotalByteCount), zen::NiceByteRate(TotalByteCount, ElapsedMs)); - - InternalFile Output; - - if (m_OutputFile.empty()) - { - // TEMPORARY -- should properly open stdout - Output.OpenWrite("CONOUT$", false); - } - else - { - Output.OpenWrite(m_OutputFile, true); - } - - zen::ExtendableStringBuilder<256> Line; - - uint64_t CurrentOffset = 0; - - for (const auto& File : FileList) - { - Line.Append(File.FilePath.generic_u8string().c_str()); - Line.Append(','); - File.FileHash.ToHexString(Line); - Line.Append('\n'); - - Output.Write(Line.Data(), Line.Size(), CurrentOffset); - CurrentOffset += Line.Size(); - - Line.Reset(); - } - - // TODO: implement snapshot enumeration and display - return 0; -} - -} // namespace zen diff --git a/src/zen/cmds/hash_cmd.h b/src/zen/cmds/hash_cmd.h deleted file mode 100644 index e5ee071e9..000000000 --- a/src/zen/cmds/hash_cmd.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "../internalfile.h" -#include "../zen.h" - -namespace zen { - -/** Generate hash list file - */ -class HashCommand : public ZenCmdBase -{ -public: - HashCommand(); - ~HashCommand(); - - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; - virtual cxxopts::Options& Options() override { return m_Options; } - -private: - cxxopts::Options m_Options{"hash", "Hash files"}; - std::string m_ScanDirectory; - std::string m_OutputFile; -}; - -} // namespace zen diff --git a/src/zen/cmds/print_cmd.h b/src/zen/cmds/print_cmd.h index 09d91830a..4d6a492b7 100644 --- a/src/zen/cmds/print_cmd.h +++ b/src/zen/cmds/print_cmd.h @@ -16,6 +16,7 @@ public: virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } + virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: cxxopts::Options m_Options{"print", "Print compact binary object"}; @@ -32,6 +33,7 @@ public: virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } + virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: cxxopts::Options m_Options{"printpkg", "Print compact binary package"}; diff --git a/src/zen/cmds/projectstore_cmd.h b/src/zen/cmds/projectstore_cmd.h index fd1590423..8891fdaf4 100644 --- a/src/zen/cmds/projectstore_cmd.h +++ b/src/zen/cmds/projectstore_cmd.h @@ -6,7 +6,12 @@ namespace zen { -class DropProjectCommand : public ZenCmdBase +class ProjectStoreCommand : public ZenCmdBase +{ + virtual ZenCmdCategory& CommandCategory() const override { return g_ProjectStoreCategory; } +}; + +class DropProjectCommand : public ProjectStoreCommand { public: DropProjectCommand(); @@ -22,7 +27,7 @@ private: std::string m_OplogName; }; -class ProjectInfoCommand : public ZenCmdBase +class ProjectInfoCommand : public ProjectStoreCommand { public: ProjectInfoCommand(); @@ -37,7 +42,7 @@ private: std::string m_OplogName; }; -class CreateProjectCommand : public ZenCmdBase +class CreateProjectCommand : public ProjectStoreCommand { public: CreateProjectCommand(); @@ -57,7 +62,7 @@ private: bool m_ForceUpdate = false; }; -class DeleteProjectCommand : public ZenCmdBase +class DeleteProjectCommand : public ProjectStoreCommand { public: DeleteProjectCommand(); @@ -72,7 +77,7 @@ private: std::string m_ProjectId; }; -class CreateOplogCommand : public ZenCmdBase +class CreateOplogCommand : public ProjectStoreCommand { public: CreateOplogCommand(); @@ -90,7 +95,7 @@ private: bool m_ForceUpdate = false; }; -class DeleteOplogCommand : public ZenCmdBase +class DeleteOplogCommand : public ProjectStoreCommand { public: DeleteOplogCommand(); @@ -106,7 +111,7 @@ private: std::string m_OplogId; }; -class ExportOplogCommand : public ZenCmdBase +class ExportOplogCommand : public ProjectStoreCommand { public: ExportOplogCommand(); @@ -150,7 +155,7 @@ private: bool m_FileForceEnableTempBlocks = false; }; -class ImportOplogCommand : public ZenCmdBase +class ImportOplogCommand : public ProjectStoreCommand { public: ImportOplogCommand(); @@ -188,7 +193,7 @@ private: std::string m_FileName; }; -class SnapshotOplogCommand : public ZenCmdBase +class SnapshotOplogCommand : public ProjectStoreCommand { public: SnapshotOplogCommand(); @@ -204,7 +209,7 @@ private: std::string m_OplogName; }; -class ProjectStatsCommand : public ZenCmdBase +class ProjectStatsCommand : public ProjectStoreCommand { public: ProjectStatsCommand(); @@ -217,7 +222,7 @@ private: std::string m_HostName; }; -class ProjectDetailsCommand : public ZenCmdBase +class ProjectDetailsCommand : public ProjectStoreCommand { public: ProjectDetailsCommand(); @@ -237,7 +242,7 @@ private: std::string m_OpId; }; -class OplogMirrorCommand : public ZenCmdBase +class OplogMirrorCommand : public ProjectStoreCommand { public: OplogMirrorCommand(); diff --git a/src/zen/cmds/rpcreplay_cmd.h b/src/zen/cmds/rpcreplay_cmd.h index e1c2831a5..42cdd4ac1 100644 --- a/src/zen/cmds/rpcreplay_cmd.h +++ b/src/zen/cmds/rpcreplay_cmd.h @@ -6,7 +6,7 @@ namespace zen { -class RpcStartRecordingCommand : public ZenCmdBase +class RpcStartRecordingCommand : public CacheStoreCommand { public: RpcStartRecordingCommand(); @@ -21,7 +21,7 @@ private: std::string m_RecordingPath; }; -class RpcStopRecordingCommand : public ZenCmdBase +class RpcStopRecordingCommand : public CacheStoreCommand { public: RpcStopRecordingCommand(); @@ -35,7 +35,7 @@ private: std::string m_HostName; }; -class RpcReplayCommand : public ZenCmdBase +class RpcReplayCommand : public CacheStoreCommand { public: RpcReplayCommand(); diff --git a/src/zen/cmds/run_cmd.cpp b/src/zen/cmds/run_cmd.cpp new file mode 100644 index 000000000..a99ba9704 --- /dev/null +++ b/src/zen/cmds/run_cmd.cpp @@ -0,0 +1,197 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "run_cmd.h" + +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/logging.h> +#include <zencore/process.h> +#include <zencore/string.h> +#include <zencore/timer.h> + +using namespace std::literals; + +#define ZEN_COLOR_BLACK "\033[0;30m" +#define ZEN_COLOR_RED "\033[0;31m" +#define ZEN_COLOR_GREEN "\033[0;32m" +#define ZEN_COLOR_YELLOW "\033[0;33m" +#define ZEN_COLOR_BLUE "\033[0;34m" +#define ZEN_COLOR_MAGENTA "\033[0;35m" +#define ZEN_COLOR_CYAN "\033[0;36m" +#define ZEN_COLOR_WHITE "\033[0;37m" + +#define ZEN_BRIGHT_COLOR_BLACK "\033[1;30m" +#define ZEN_BRIGHT_COLOR_RED "\033[1;31m" +#define ZEN_BRIGHT_COLOR_GREEN "\033[1;32m" +#define ZEN_BRIGHT_COLOR_YELLOW "\033[1;33m" +#define ZEN_BRIGHT_COLOR_BLUE "\033[1;34m" +#define ZEN_BRIGHT_COLOR_MAGENTA "\033[1;35m" +#define ZEN_BRIGHT_COLOR_CYAN "\033[1;36m" +#define ZEN_BRIGHT_COLOR_WHITE "\033[1;37m" + +#define ZEN_COLOR_RESET "\033[0m" + +namespace zen { + +RunCommand::RunCommand() +{ + m_Options.add_options()("h,help", "Print help"); + m_Options.add_option("", "n", "count", "Number of times to run command", cxxopts::value(m_RunCount), "<count>"); + m_Options.add_option("", "t", "time", "How long to run command(s) for", cxxopts::value(m_RunTime), "<seconds>"); + m_Options.add_option("", + "", + "basepath", + "Where to run command. Each run will execute in its own numbered subdirectory below this directory. Additionally, " + "stdout will be redirected to a file in the provided directory", + cxxopts::value(m_BaseDirectory), + "<path>"); + m_Options.add_option("", + "", + "max-dirs", + "Number of base directories to retain on rotation", + cxxopts::value(m_MaxBaseDirectoryCount), + "<count>"); +} + +RunCommand::~RunCommand() +{ +} + +int +RunCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) +{ + ZEN_UNUSED(GlobalOptions); + + if (!ParseOptions(argc, argv)) + { + return 0; + } + + // Validate arguments + + if (GlobalOptions.PassthroughArgV.empty() || GlobalOptions.PassthroughArgV[0].empty()) + throw OptionParseException("No command specified. The command to run is passed in after a double dash ('--') on the command line"); + + if (m_RunCount < 0) + throw OptionParseException("Invalid count specified"); + + if (m_RunTime < -1 || m_RunTime == 0) + throw OptionParseException("Invalid run time specified"); + + if (m_MaxBaseDirectoryCount < 0) + throw OptionParseException("Invalid directory count specified"); + + if (m_RunTime > 0 && m_RunCount > 0) + throw OptionParseException("Specify either time or count, not both"); + + if (m_RunCount == 0) + m_RunCount = 1; + + std::filesystem::path BaseDirectory; + + if (!m_BaseDirectory.empty()) + { + BaseDirectory = m_BaseDirectory; + + if (m_MaxBaseDirectoryCount) + { + RotateDirectories(BaseDirectory, m_MaxBaseDirectoryCount); + CreateDirectories(BaseDirectory); + } + else + { + CleanDirectory(BaseDirectory); + } + } + + bool TimedRun = false; + auto CommandStartTime = std::chrono::system_clock::now(); + auto CommandDeadlineTime = std::chrono::system_clock::now(); + + if (m_RunTime > 0) + { + m_RunCount = 1'000'000; + TimedRun = true; + + CommandDeadlineTime += std::chrono::seconds(m_RunTime); + } + + struct RunResults + { + int ExitCode = 0; + std::chrono::duration<long, std::milli> Duration{}; + }; + + std::vector<RunResults> Results; + int ErrorCount = 0; + + std::filesystem::path ExecutablePath = SearchPathForExecutable(GlobalOptions.PassthroughArgV[0]); + std::string CommandArguments = GlobalOptions.PassthroughArgs; + + for (int i = 0; i < m_RunCount; ++i) + { + std::filesystem::path RunDir; + if (!BaseDirectory.empty()) + { + RunDir = BaseDirectory / IntNum(i + 1).c_str(); + CreateDirectories(RunDir); + } + + Stopwatch Timer; + + CreateProcOptions ProcOptions; + + if (!RunDir.empty()) + { + ProcOptions.WorkingDirectory = &RunDir; + ProcOptions.StdoutFile = RunDir / "stdout.txt"; + } + + fmt::print(ZEN_BRIGHT_COLOR_WHITE "run #{}" ZEN_COLOR_RESET ": {}\n", i + 1, GlobalOptions.PassthroughCommandLine); + + ProcessHandle Proc; + Proc.Initialize(CreateProc(ExecutablePath, GlobalOptions.PassthroughCommandLine, ProcOptions)); + if (!Proc.IsValid()) + { + throw std::runtime_error(fmt::format("failed to launch '{}'", ExecutablePath)); + } + + int ExitCode = Proc.WaitExitCode(); + + auto EndTime = std::chrono::system_clock::now(); + + if (ExitCode) + ++ErrorCount; + + Results.emplace_back(RunResults{.ExitCode = ExitCode, .Duration = std::chrono::milliseconds(Timer.GetElapsedTimeMs())}); + + if (TimedRun) + { + if (EndTime >= CommandDeadlineTime) + { + m_RunCount = i + 1; + break; + } + } + } + + fmt::print("{:>5} {:>3} {:>6}\n", "run", "rc", "time"); + int i = 0; + for (const auto& Entry : Results) + { + fmt::print("{:5} {:3} {:>6}\n", ++i, Entry.ExitCode, NiceTimeSpanMs(Entry.Duration.count())); + } + + if (ErrorCount) + { + fmt::print("run complete ({}/{} failed)\n", ErrorCount, m_RunCount); + } + else + { + fmt::print("run complete, no error exit code\n", m_RunCount); + } + + return 0; +} + +} // namespace zen diff --git a/src/zen/cmds/run_cmd.h b/src/zen/cmds/run_cmd.h new file mode 100644 index 000000000..f6512a4e8 --- /dev/null +++ b/src/zen/cmds/run_cmd.h @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "../zen.h" + +namespace zen { + +class RunCommand : public ZenCmdBase +{ +public: + RunCommand(); + ~RunCommand(); + + virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual cxxopts::Options& Options() override { return m_Options; } + virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } + +private: + cxxopts::Options m_Options{"run", "Run executable"}; + int m_RunCount = 0; + int m_RunTime = -1; + std::string m_BaseDirectory; + int m_MaxBaseDirectoryCount = 10; +}; + +} // namespace zen diff --git a/src/zen/cmds/vfs_cmd.h b/src/zen/cmds/vfs_cmd.h index 35546c9b6..9b2497c0e 100644 --- a/src/zen/cmds/vfs_cmd.h +++ b/src/zen/cmds/vfs_cmd.h @@ -6,7 +6,7 @@ namespace zen { -class VfsCommand : public ZenCmdBase +class VfsCommand : public StorageCommand { public: VfsCommand(); diff --git a/src/zen/internalfile.cpp b/src/zen/internalfile.cpp deleted file mode 100644 index 671a2093e..000000000 --- a/src/zen/internalfile.cpp +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "internalfile.h" - -#include <zencore/except.h> -#include <zencore/filesystem.h> -#include <zencore/fmtutils.h> -#include <zencore/logging.h> -#include <zencore/memory.h> - -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC -# include <fcntl.h> -# include <sys/file.h> -# include <sys/mman.h> -# include <sys/stat.h> -# include <unistd.h> -#endif - -#include <gsl/gsl-lite.hpp> - -namespace zen { - -#define ZEN_USE_SLIST ZEN_PLATFORM_WINDOWS - -#if ZEN_USE_SLIST == 0 -struct FileBufferManager::Impl -{ - RwLock m_Lock; - std::list<IoBuffer> m_FreeBuffers; - - uint64_t m_BufferSize; - uint64_t m_MaxBufferCount; - - Impl(uint64_t BufferSize, uint64_t MaxBuffers) : m_BufferSize(BufferSize), m_MaxBufferCount(MaxBuffers) {} - - IoBuffer AllocBuffer() - { - RwLock::ExclusiveLockScope _(m_Lock); - - if (m_FreeBuffers.empty()) - { - return IoBuffer{m_BufferSize, 64 * 1024}; - } - else - { - IoBuffer Buffer = std::move(m_FreeBuffers.front()); - m_FreeBuffers.pop_front(); - return Buffer; - } - } - - void ReturnBuffer(IoBuffer Buffer) - { - RwLock::ExclusiveLockScope _(m_Lock); - - m_FreeBuffers.push_front(std::move(Buffer)); - } -}; -#else -struct FileBufferManager::Impl -{ - struct BufferItem - { - SLIST_ENTRY ItemEntry; - IoBuffer Buffer; - }; - - SLIST_HEADER m_FreeList; - uint64_t m_BufferSize; - uint64_t m_MaxBufferCount; - - Impl(uint64_t BufferSize, uint64_t MaxBuffers) : m_BufferSize(BufferSize), m_MaxBufferCount(MaxBuffers) - { - InitializeSListHead(&m_FreeList); - } - - ~Impl() - { - while (SLIST_ENTRY* Entry = InterlockedPopEntrySList(&m_FreeList)) - { - BufferItem* Item = reinterpret_cast<BufferItem*>(Entry); - delete Item; - } - } - - IoBuffer AllocBuffer() - { - SLIST_ENTRY* Entry = InterlockedPopEntrySList(&m_FreeList); - - if (Entry == nullptr) - { - return IoBuffer{m_BufferSize, 64 * 1024}; - } - else - { - BufferItem* Item = reinterpret_cast<BufferItem*>(Entry); - IoBuffer Buffer = std::move(Item->Buffer); - delete Item; // Todo: could keep this around in another list - - return Buffer; - } - } - - void ReturnBuffer(IoBuffer Buffer) - { - BufferItem* Item = new BufferItem{nullptr, std::move(Buffer)}; - - InterlockedPushEntrySList(&m_FreeList, &Item->ItemEntry); - } -}; -#endif - -FileBufferManager::FileBufferManager(uint64_t BufferSize, uint64_t MaxBuffers) -{ - m_Impl = new Impl{BufferSize, MaxBuffers}; -} - -FileBufferManager::~FileBufferManager() -{ - delete m_Impl; -} - -IoBuffer -FileBufferManager::AllocBuffer() -{ - return m_Impl->AllocBuffer(); -} - -void -FileBufferManager::ReturnBuffer(IoBuffer Buffer) -{ - return m_Impl->ReturnBuffer(Buffer); -} - -////////////////////////////////////////////////////////////////////////// - -InternalFile::InternalFile() -{ -} - -InternalFile::~InternalFile() -{ - if (m_Memory) - Memory::Free(m_Memory); - -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - if (m_Mmap) - munmap(m_Mmap, GetFileSize()); - if (m_File) - close(int(intptr_t(m_File))); -#endif -} - -size_t -InternalFile::GetFileSize() -{ -#if ZEN_PLATFORM_WINDOWS - ULONGLONG sz; - m_File.GetSize(sz); - return size_t(sz); -#else - int Fd = int(intptr_t(m_File)); - static_assert(sizeof(decltype(stat::st_size)) == sizeof(uint64_t), "fstat() doesn't support large files"); - struct stat Stat; - fstat(Fd, &Stat); - return size_t(Stat.st_size); -#endif -} - -void -InternalFile::OpenWrite(std::filesystem::path FileName, bool IsCreate) -{ - bool Success = false; - -#if ZEN_PLATFORM_WINDOWS - const DWORD dwCreationDisposition = IsCreate ? CREATE_ALWAYS : OPEN_EXISTING; - - HRESULT hRes = m_File.Create(FileName.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, dwCreationDisposition); - Success = SUCCEEDED(hRes); -#else - int OpenFlags = O_RDWR | O_CLOEXEC; - OpenFlags |= IsCreate ? O_CREAT | O_TRUNC : 0; - - int Fd = open(FileName.c_str(), OpenFlags, 0666); - if (Fd >= 0) - { - if (IsCreate) - { - fchmod(Fd, 0666); - } - Success = true; - m_File = (void*)(intptr_t(Fd)); - } -#endif // ZEN_PLATFORM_WINDOWS - - if (Success) - { - ThrowLastError(fmt::format("Failed to open file for writing: '{}'", FileName)); - } -} - -void -InternalFile::OpenRead(std::filesystem::path FileName) -{ - bool Success = false; - -#if ZEN_PLATFORM_WINDOWS - const DWORD dwCreationDisposition = OPEN_EXISTING; - - HRESULT hRes = m_File.Create(FileName.c_str(), GENERIC_READ, FILE_SHARE_READ, dwCreationDisposition); - Success = SUCCEEDED(hRes); -#else - int Fd = open(FileName.c_str(), O_RDONLY); - if (Fd >= 0) - { - Success = true; - m_File = (void*)(intptr_t(Fd)); - } -#endif - - if (Success) - { - ThrowLastError(fmt::format("Failed to open file for reading: '{}'", FileName)); - } -} - -const void* -InternalFile::MemoryMapFile() -{ - auto FileSize = GetFileSize(); - - if (FileSize <= 100 * 1024 * 1024) - { - m_Memory = Memory::Alloc(FileSize, 64); - if (!m_Memory) - { - ThrowOutOfMemory(fmt::format("failed allocating {:#x} bytes aligned to {:#x}", FileSize, 64)); - } - Read(m_Memory, FileSize, 0); - - return m_Memory; - } - -#if ZEN_PLATFORM_WINDOWS - m_Mmap.MapFile(m_File); - return m_Mmap.GetData(); -#else - int Fd = int(intptr_t(m_File)); - m_Mmap = mmap(nullptr, FileSize, PROT_READ, MAP_PRIVATE, Fd, 0); - return m_Mmap; -#endif -} - -void -InternalFile::Read(void* Data, uint64_t Size, uint64_t Offset) -{ - bool Success; - -#if ZEN_PLATFORM_WINDOWS - OVERLAPPED ovl{}; - - ovl.Offset = DWORD(Offset & 0xffff'ffffu); - ovl.OffsetHigh = DWORD(Offset >> 32); - - HRESULT hRes = m_File.Read(Data, gsl::narrow<DWORD>(Size), &ovl); - Success = SUCCEEDED(hRes); -#else - int Fd = int(intptr_t(m_File)); - int BytesRead = pread(Fd, Data, Size, Offset); - Success = (BytesRead > 0); -#endif - - if (Success) - { - std::error_code DummyEc; - ThrowLastError(fmt::format("Failed to read from file '{}'", PathFromHandle(m_File, DummyEc))); - } -} - -void -InternalFile::Write(const void* Data, uint64_t Size, uint64_t Offset) -{ - bool Success; - -#if ZEN_PLATFORM_WINDOWS - OVERLAPPED Ovl{}; - - Ovl.Offset = DWORD(Offset & 0xffff'ffffu); - Ovl.OffsetHigh = DWORD(Offset >> 32); - - HRESULT hRes = m_File.Write(Data, gsl::narrow<DWORD>(Size), &Ovl); - Success = SUCCEEDED(hRes); -#else - int Fd = int(intptr_t(m_File)); - int BytesWritten = pwrite(Fd, Data, Size, Offset); - Success = (BytesWritten > 0); -#endif - - if (Success) - { - std::error_code DummyEc; - ThrowLastError(fmt::format("Failed to write to file '{}'", PathFromHandle(m_File, DummyEc))); - } -} - -} // namespace zen diff --git a/src/zen/internalfile.h b/src/zen/internalfile.h deleted file mode 100644 index 90d370e28..000000000 --- a/src/zen/internalfile.h +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include <zencore/zencore.h> - -#include <zenbase/refcount.h> -#include <zencore/iobuffer.h> -#include <zencore/thread.h> - -#if ZEN_PLATFORM_WINDOWS -# include <zencore/windows.h> -#endif - -#include <filesystem> -#include <list> - -namespace zen { - -////////////////////////////////////////////////////////////////////////// - -class FileBufferManager : public RefCounted -{ -public: - FileBufferManager(uint64_t BufferSize, uint64_t MaxBufferCount); - ~FileBufferManager(); - - IoBuffer AllocBuffer(); - void ReturnBuffer(IoBuffer Buffer); - -private: - struct Impl; - - Impl* m_Impl; -}; - -class InternalFile : public RefCounted -{ -public: - InternalFile(); - ~InternalFile(); - - void OpenRead(std::filesystem::path FileName); - void Read(void* Data, uint64_t Size, uint64_t Offset); - - void OpenWrite(std::filesystem::path FileName, bool isCreate); - void Write(const void* Data, uint64_t Size, uint64_t Offset); - - const void* MemoryMapFile(); - size_t GetFileSize(); - -private: -#if ZEN_PLATFORM_WINDOWS - windows::FileHandle m_File; - windows::FileMapping m_Mmap; -#else - void* m_File = nullptr; - void* m_Mmap = nullptr; -#endif - - void* m_Memory = nullptr; -}; - -} // namespace zen diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp index 21dea44bc..2e9cf6d01 100644 --- a/src/zen/zen.cpp +++ b/src/zen/zen.cpp @@ -10,10 +10,10 @@ #include "cmds/cache_cmd.h" #include "cmds/copy_cmd.h" #include "cmds/dedup_cmd.h" -#include "cmds/hash_cmd.h" #include "cmds/print_cmd.h" #include "cmds/projectstore_cmd.h" #include "cmds/rpcreplay_cmd.h" +#include "cmds/run_cmd.h" #include "cmds/serve_cmd.h" #include "cmds/status_cmd.h" #include "cmds/top_cmd.h" @@ -47,6 +47,20 @@ ZEN_THIRD_PARTY_INCLUDES_END ////////////////////////////////////////////////////////////////////////// +namespace zen { + +ZenCmdCategory DefaultCategory{.Name = "general commands"}; +ZenCmdCategory g_UtilitiesCategory{.Name = "utility commands"}; +ZenCmdCategory g_ProjectStoreCategory{.Name = "project store commands"}; +ZenCmdCategory g_CacheStoreCategory{.Name = "cache store commands"}; +ZenCmdCategory g_StorageCategory{.Name = "storage management commands"}; + +ZenCmdCategory& +ZenCmdBase::CommandCategory() const +{ + return DefaultCategory; +} + bool ZenCmdBase::ParseOptions(int argc, char** argv) { @@ -149,6 +163,62 @@ ZenCmdBase::MapHttpToCommandReturnCode(const cpr::Response& Response) return 1; } +std::string +ZenCmdBase::ResolveTargetHostSpec(const std::string& InHostSpec, uint16_t& OutEffectivePort) +{ + if (InHostSpec.empty()) + { + // If no host is specified then look to see if we have an instance + // running on this host and use that as the default to interact with + + zen::ZenServerState Servers; + + if (Servers.InitializeReadOnly()) + { + std::string ResolvedSpec; + + Servers.Snapshot([&](const zen::ZenServerState::ZenServerEntry& Entry) { + if (ResolvedSpec.empty()) + { + ResolvedSpec = fmt::format("http://localhost:{}", Entry.EffectiveListenPort.load()); + OutEffectivePort = Entry.EffectiveListenPort; + } + }); + + return ResolvedSpec; + } + } + + // Parse out port from the specification provided, to be consistent with + // the auto-discovery logic above. + + std::string_view PortSpec(InHostSpec); + if (size_t PrefixIndex = PortSpec.find_last_of(":"); PrefixIndex != std::string_view::npos) + { + PortSpec.remove_prefix(PrefixIndex + 1); + + std::optional<uint16_t> EffectivePort = zen::ParseInt<uint16_t>(PortSpec); + + if (EffectivePort) + { + OutEffectivePort = EffectivePort.value(); + } + } + + // note: We should consider adding validation/normalization of the provided spec here + + return InHostSpec; +} + +std::string +ZenCmdBase::ResolveTargetHostSpec(const std::string& InHostSpec) +{ + uint16_t Dummy = 0; + return ResolveTargetHostSpec(InHostSpec, /* out */ Dummy); +} + +} // namespace zen + ////////////////////////////////////////////////////////////////////////// // TODO: should make this Unicode-aware so we can pass anything in on the // command line. @@ -157,6 +227,7 @@ int main(int argc, char** argv) { using namespace zen; + using namespace std::literals; #if ZEN_USE_MIMALLOC mi_version(); @@ -200,7 +271,6 @@ main(int argc, char** argv) FlushCommand FlushCmd; GcCommand GcCmd; GcStatusCommand GcStatusCmd; - HashCommand HashCmd; ImportOplogCommand ImportOplogCmd; JobCommand JobCmd; OplogMirrorCommand OplogMirrorCmd; @@ -213,6 +283,7 @@ main(int argc, char** argv) RpcReplayCommand RpcReplayCmd; RpcStartRecordingCommand RpcStartRecordingCmd; RpcStopRecordingCommand RpcStopRecordingCmd; + RunCommand RunCmd; ScrubCommand ScrubCmd; ServeCommand ServeCmd; SnapshotOplogCommand SnapshotOplogCmd; @@ -233,7 +304,6 @@ main(int argc, char** argv) // clang-format off {"attach", &AttachCmd, "Add a sponsor process to a running zen service"}, {"bench", &BenchCmd, "Utility command for benchmarking"}, -// {"chunk", &ChunkCmd, "Perform chunking"}, {"cache-details", &CacheDetailsCmd, "Details on cache"}, {"cache-info", &CacheInfoCmd, "Info on cache, namespace or bucket"}, {"cache-stats", &CacheStatsCmd, "Stats on cache"}, @@ -244,7 +314,6 @@ main(int argc, char** argv) {"drop", &DropCmd, "Drop cache namespace or bucket"}, {"gc-status", &GcStatusCmd, "Garbage collect zen storage status check"}, {"gc", &GcCmd, "Garbage collect zen storage"}, - {"hash", &HashCmd, "Compute file hashes"}, {"jobs", &JobCmd, "Show/cancel zen background jobs"}, {"logs", &LoggingCmd, "Show/control zen logging"}, {"oplog-create", &CreateOplogCmd, "Create a project oplog"}, @@ -265,6 +334,7 @@ main(int argc, char** argv) {"rpc-record-replay", &RpcReplayCmd, "Stops recording of cache rpc requests on a host"}, {"rpc-record-start", &RpcStartRecordingCmd, "Replays a previously recorded session of rpc requests"}, {"rpc-record-stop", &RpcStopRecordingCmd, "Starts recording of cache rpc requests on a host"}, + {"run", &RunCmd, "Run command with special options"}, {"scrub", &ScrubCmd, "Scrub zen storage (verify data integrity)"}, {"serve", &ServeCmd, "Serve files from a directory"}, {"status", &StatusCmd, "Show zen status"}, @@ -287,46 +357,57 @@ main(int argc, char** argv) // Split command line into options, commands and any pass-through arguments std::string Passthrough; - std::vector<std::string> PassthroughV; + std::string PassthroughArgs; + std::vector<std::string> PassthroughArgV; for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--") == 0) + if ("--"sv == argv[i]) { bool IsFirst = true; zen::ExtendableStringBuilder<256> Line; + zen::ExtendableStringBuilder<256> Arguments; for (int j = i + 1; j < argc; ++j) { + auto AppendAscii = [&](auto X) { + Line.Append(X); + if (!IsFirst) + { + Arguments.Append(X); + } + }; + if (!IsFirst) { - Line.AppendAscii(" "); + AppendAscii(" "); } std::string_view ThisArg(argv[j]); - PassthroughV.push_back(std::string(ThisArg)); + PassthroughArgV.push_back(std::string(ThisArg)); const bool NeedsQuotes = (ThisArg.find(' ') != std::string_view::npos); if (NeedsQuotes) { - Line.AppendAscii("\""); + AppendAscii("\""); } - Line.Append(ThisArg); + AppendAscii(ThisArg); if (NeedsQuotes) { - Line.AppendAscii("\""); + AppendAscii("\""); } IsFirst = false; } - Passthrough = Line.c_str(); + Passthrough = Line.c_str(); + PassthroughArgs = Arguments.c_str(); // This will "truncate" the arg vector and terminate the loop - argc = i - 1; + argc = i; } } @@ -354,8 +435,9 @@ main(int argc, char** argv) ZenCliOptions GlobalOptions; - GlobalOptions.PassthroughArgs = Passthrough; - GlobalOptions.PassthroughV = PassthroughV; + GlobalOptions.PassthroughCommandLine = Passthrough; + GlobalOptions.PassthroughArgs = PassthroughArgs; + GlobalOptions.PassthroughArgV = PassthroughArgV; std::string SubCommand = "<None>"; @@ -382,16 +464,26 @@ main(int argc, char** argv) printf("available commands:\n"); - std::map<std::string, std::string> SortedCmds; + std::map<std::string, ZenCmdCategory*> Categories; for (const CommandInfo& CmdInfo : Commands) { - SortedCmds[CmdInfo.CmdName] = CmdInfo.CmdSummary; + ZenCmdCategory& Category = CmdInfo.Cmd->CommandCategory(); + + Categories[Category.Name] = &Category; + Category.SortedCmds[CmdInfo.CmdName] = CmdInfo.CmdSummary; } - for (const auto& Kv : SortedCmds) + for (const auto& CategoryKv : Categories) { - printf(" %-20s %s\n", Kv.first.c_str(), Kv.second.c_str()); + fmt::print(" {}\n\n", CategoryKv.first); + + for (const auto& Kv : CategoryKv.second->SortedCmds) + { + printf(" %-20s %s\n", Kv.first.c_str(), Kv.second.c_str()); + } + + printf("\n"); } exit(0); @@ -446,57 +538,3 @@ main(int argc, char** argv) return 0; } - -std::string -ZenCmdBase::ResolveTargetHostSpec(const std::string& InHostSpec, uint16_t& OutEffectivePort) -{ - if (InHostSpec.empty()) - { - // If no host is specified then look to see if we have an instance - // running on this host and use that as the default to interact with - - zen::ZenServerState Servers; - - if (Servers.InitializeReadOnly()) - { - std::string ResolvedSpec; - - Servers.Snapshot([&](const zen::ZenServerState::ZenServerEntry& Entry) { - if (ResolvedSpec.empty()) - { - ResolvedSpec = fmt::format("http://localhost:{}", Entry.EffectiveListenPort.load()); - OutEffectivePort = Entry.EffectiveListenPort; - } - }); - - return ResolvedSpec; - } - } - - // Parse out port from the specification provided, to be consistent with - // the auto-discovery logic above. - - std::string_view PortSpec(InHostSpec); - if (size_t PrefixIndex = PortSpec.find_last_of(":"); PrefixIndex != std::string_view::npos) - { - PortSpec.remove_prefix(PrefixIndex + 1); - - std::optional<uint16_t> EffectivePort = zen::ParseInt<uint16_t>(PortSpec); - - if (EffectivePort) - { - OutEffectivePort = EffectivePort.value(); - } - } - - // note: We should consider adding validation/normalization of the provided spec here - - return InHostSpec; -} - -std::string -ZenCmdBase::ResolveTargetHostSpec(const std::string& InHostSpec) -{ - uint16_t Dummy = 0; - return ResolveTargetHostSpec(InHostSpec, /* out */ Dummy); -} diff --git a/src/zen/zen.h b/src/zen/zen.h index 7258c10ce..78f22cad6 100644 --- a/src/zen/zen.h +++ b/src/zen/zen.h @@ -13,16 +13,30 @@ namespace cpr { class Response; } +namespace zen { + struct ZenCliOptions { bool IsDebug = false; bool IsVerbose = false; // Arguments after " -- " on command line are passed through and not parsed + std::string PassthroughCommandLine; std::string PassthroughArgs; - std::vector<std::string> PassthroughV; + std::vector<std::string> PassthroughArgV; +}; + +struct ZenCmdCategory +{ + std::string Name; + std::map<std::string, std::string> SortedCmds; }; +extern ZenCmdCategory g_UtilitiesCategory; +extern ZenCmdCategory g_ProjectStoreCategory; +extern ZenCmdCategory g_CacheStoreCategory; +extern ZenCmdCategory g_StorageCategory; + /** Base class for command implementations */ @@ -31,6 +45,7 @@ class ZenCmdBase public: virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) = 0; virtual cxxopts::Options& Options() = 0; + virtual ZenCmdCategory& CommandCategory() const; bool ParseOptions(int argc, char** argv); static std::string FormatHttpResponse(const cpr::Response& Response); @@ -38,3 +53,15 @@ public: static std::string ResolveTargetHostSpec(const std::string& InHostSpec); static std::string ResolveTargetHostSpec(const std::string& InHostSpec, uint16_t& OutEffectivePort); }; + +class StorageCommand : public ZenCmdBase +{ + virtual ZenCmdCategory& CommandCategory() const override { return g_StorageCategory; } +}; + +class CacheStoreCommand : public ZenCmdBase +{ + virtual ZenCmdCategory& CommandCategory() const override { return g_CacheStoreCategory; } +}; + +} // namespace zen diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index b941613ec..f117001b5 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -1558,6 +1558,76 @@ RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles) return Result; } +std::error_code +RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories) +{ + const std::filesystem::path BasePath(DirectoryName.parent_path()); + const std::string Stem(DirectoryName.stem().string()); + + auto GetPathForIndex = [&](size_t Index) -> std::filesystem::path { + if (Index == 0) + { + return BasePath / Stem; + } + return BasePath / fmt::format("{}.{}", Stem, Index); + }; + + auto IsEmpty = [](const std::filesystem::path& Path, std::error_code& Ec) -> bool { return std::filesystem::is_empty(Path, Ec); }; + + std::error_code Result; + const bool BaseIsEmpty = IsEmpty(GetPathForIndex(0), Result); + if (Result) + { + return Result; + } + + if (BaseIsEmpty) + return Result; + + for (std::size_t i = MaxDirectories; i > 0; i--) + { + const std::filesystem::path SourcePath = GetPathForIndex(i - 1); + + if (std::filesystem::exists(SourcePath)) + { + std::filesystem::path TargetPath = GetPathForIndex(i); + + std::error_code DummyEc; + if (std::filesystem::exists(TargetPath, DummyEc)) + { + std::filesystem::remove_all(TargetPath, DummyEc); + } + std::filesystem::rename(SourcePath, TargetPath, DummyEc); + } + } + + return Result; +} + +std::filesystem::path +SearchPathForExecutable(std::string_view ExecutableName) +{ +#if ZEN_PLATFORM_WINDOWS + std::wstring Executable(Utf8ToWide(ExecutableName)); + + DWORD Result = SearchPathW(nullptr, Executable.c_str(), L".exe", 0, nullptr, nullptr); + + if (!Result) + return ExecutableName; + + auto PathBuffer = std::make_unique_for_overwrite<WCHAR[]>(Result); + + Result = SearchPathW(nullptr, Executable.c_str(), L".exe", Result, PathBuffer.get(), nullptr); + + if (!Result) + return ExecutableName; + + return PathBuffer.get(); +#else + return ExecutableName; +#endif +} + ////////////////////////////////////////////////////////////////////////// // // Testing related code follows... @@ -1717,6 +1787,55 @@ TEST_CASE("PathBuilder") # endif } +TEST_CASE("RotateDirectories") +{ + std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".test"; + CleanDirectory(TestBaseDir); + std::filesystem::path RotateDir = TestBaseDir / "rotate_dir" / "dir_to_rotate"; + IoBuffer DummyFileData = IoBufferBuilder::MakeCloneFromMemory("blubb", 5); + + auto NewDir = [&] { + CreateDirectories(RotateDir); + WriteFile(RotateDir / ".placeholder", DummyFileData); + }; + + auto DirWithSuffix = [&](int Index) -> std::filesystem::path { return RotateDir.generic_string().append(fmt::format(".{}", Index)); }; + + const int RotateMax = 10; + + NewDir(); + CHECK(std::filesystem::exists(RotateDir)); + RotateDirectories(RotateDir, RotateMax); + CHECK(!std::filesystem::exists(RotateDir)); + CHECK(std::filesystem::exists(DirWithSuffix(1))); + NewDir(); + CHECK(std::filesystem::exists(RotateDir)); + RotateDirectories(RotateDir, RotateMax); + CHECK(!std::filesystem::exists(RotateDir)); + CHECK(std::filesystem::exists(DirWithSuffix(1))); + CHECK(std::filesystem::exists(DirWithSuffix(2))); + + for (int i = 0; i < RotateMax; ++i) + { + NewDir(); + std::error_code Ec = RotateDirectories(RotateDir, 10); + const bool IsError = !!Ec; + CHECK_EQ(IsError, false); + } + + CHECK(!std::filesystem::exists(RotateDir)); + + for (int i = 0; i < RotateMax; ++i) + { + CHECK(std::filesystem::exists(DirWithSuffix(i + 1))); + } + + for (int i = RotateMax; i < RotateMax + 5; ++i) + { + CHECK(!std::filesystem::exists(DirWithSuffix(RotateMax + i + 1))); + } +} + #endif } // namespace zen diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h index 963890fb9..07eb72879 100644 --- a/src/zencore/include/zencore/filesystem.h +++ b/src/zencore/include/zencore/filesystem.h @@ -211,7 +211,10 @@ void GetDirectoryContent(const std::filesystem::path& RootDir, uint8_t Flags, Di std::string GetEnvVariable(std::string_view VariableName); +std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName); + std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles); +std::error_code RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories); ////////////////////////////////////////////////////////////////////////// diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h index ec598b929..d90a32301 100644 --- a/src/zencore/include/zencore/process.h +++ b/src/zencore/include/zencore/process.h @@ -34,6 +34,9 @@ public: private: void* m_ProcessHandle = nullptr; int m_Pid = 0; +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int m_ExitCode = -1; +#endif }; /** Basic process creation @@ -49,6 +52,7 @@ struct CreateProcOptions const std::filesystem::path* WorkingDirectory = nullptr; uint32_t Flags = 0; + std::filesystem::path StdoutFile; }; #if ZEN_PLATFORM_WINDOWS diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp index 1c208701c..2d0ec2de6 100644 --- a/src/zencore/process.cpp +++ b/src/zencore/process.cpp @@ -185,6 +185,11 @@ ProcessHandle::Wait(int TimeoutMs) int WaitState = 0; waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED); + if (WIFEXITED(WaitState)) + { + m_ExitCode = WEXITSTATUS(WaitState); + } + if (kill(m_Pid, 0) < 0) { int32_t LastError = zen::GetLastError(); @@ -220,6 +225,8 @@ ProcessHandle::WaitExitCode() ZEN_ASSERT(ExitCode != STILL_ACTIVE); return ExitCode; +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + return m_ExitCode; #else ZEN_NOT_IMPLEMENTED(); @@ -278,7 +285,7 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma PROCESS_INFORMATION ProcessInfo{}; STARTUPINFO StartupInfo{.cb = sizeof(STARTUPINFO)}; - const bool InheritHandles = false; + bool InheritHandles = false; void* Environment = nullptr; LPSECURITY_ATTRIBUTES ProcessAttributes = nullptr; LPSECURITY_ATTRIBUTES ThreadAttributes = nullptr; @@ -298,6 +305,42 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma ExtendableWideStringBuilder<256> CommandLineZ; CommandLineZ << CommandLine; + if (!Options.StdoutFile.empty()) + { + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof sa; + sa.lpSecurityDescriptor = nullptr; + sa.bInheritHandle = TRUE; + + StartupInfo.hStdInput = nullptr; + StartupInfo.hStdOutput = CreateFileW(Options.StdoutFile.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ, + &sa, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + const BOOL Success = DuplicateHandle(GetCurrentProcess(), + StartupInfo.hStdOutput, + GetCurrentProcess(), + &StartupInfo.hStdError, + 0, + TRUE, + DUPLICATE_SAME_ACCESS); + + if (Success) + { + StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + InheritHandles = true; + } + else + { + CloseHandle(StartupInfo.hStdOutput); + StartupInfo.hStdOutput = 0; + } + } + BOOL Success = CreateProcessW(Executable.c_str(), CommandLineZ.Data(), ProcessAttributes, @@ -309,6 +352,12 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma &StartupInfo, &ProcessInfo); + if (StartupInfo.dwFlags & STARTF_USESTDHANDLES) + { + CloseHandle(StartupInfo.hStdError); + CloseHandle(StartupInfo.hStdOutput); + } + if (!Success) { return nullptr; @@ -395,6 +444,14 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C ExtendableWideStringBuilder<256> CommandLineZ; CommandLineZ << CommandLine; + ExtendableWideStringBuilder<256> CurrentDirZ; + LPCWSTR WorkingDirectoryPtr = nullptr; + if (Options.WorkingDirectory) + { + CurrentDirZ << Options.WorkingDirectory->native(); + WorkingDirectoryPtr = CurrentDirZ.c_str(); + } + bOk = CreateProcessW(Executable.c_str(), CommandLineZ.Data(), nullptr, @@ -402,7 +459,7 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C FALSE, CreateProcFlags, nullptr, - nullptr, + WorkingDirectoryPtr, &StartupInfo.StartupInfo, &ProcessInfo); if (bOk == FALSE) diff --git a/src/zenserver-test/zenserver-test.cpp b/src/zenserver-test/zenserver-test.cpp index 3cf6e2976..3efa57fdb 100644 --- a/src/zenserver-test/zenserver-test.cpp +++ b/src/zenserver-test/zenserver-test.cpp @@ -101,22 +101,23 @@ int main(int argc, char** argv) { using namespace std::literals; + using namespace zen; # if ZEN_USE_MIMALLOC mi_version(); # endif - zen::zencore_forcelinktests(); - zen::zenhttp_forcelinktests(); - zen::cacherequests_forcelink(); + zencore_forcelinktests(); + zenhttp_forcelinktests(); + cacherequests_forcelink(); zen::logging::InitializeLogging(); zen::logging::SetLogLevel(zen::logging::level::Debug); spdlog::set_formatter(std::make_unique<zen::logging::full_test_formatter>("test", std::chrono::system_clock::now())); - std::filesystem::path ProgramBaseDir = std::filesystem::path(argv[0]).parent_path(); - std::filesystem::path TestBaseDir = ProgramBaseDir.parent_path().parent_path() / ".test"; + std::filesystem::path ProgramBaseDir = GetRunningExecutablePath().parent_path(); + std::filesystem::path TestBaseDir = std::filesystem::current_path() / ".test"; // This is pretty janky because we're passing most of the options through to the test // framework, so we can't just use cxxopts (I think). This should ideally be cleaned up |