// Copyright Epic Games, Inc. All Rights Reserved. // Zen command line client utility // #include "zen.h" #include "cmds/admin_cmd.h" #include "cmds/bench_cmd.h" #include "cmds/builds_cmd.h" #include "cmds/cache_cmd.h" #include "cmds/copy_cmd.h" #include "cmds/dedup_cmd.h" #include "cmds/info_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" #include "cmds/trace_cmd.h" #include "cmds/up_cmd.h" #include "cmds/version_cmd.h" #include "cmds/vfs_cmd.h" #include "cmds/wipe_cmd.h" #include "cmds/workspaces_cmd.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if ZEN_WITH_TESTS # define ZEN_TEST_WITH_RUNNER 1 # include #endif ZEN_THIRD_PARTY_INCLUDES_START #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_END #include #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC # include # include #endif ////////////////////////////////////////////////////////////////////////// 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) { return ParseOptions(Options(), argc, argv); } bool ZenCmdBase::ParseOptions(cxxopts::Options& CmdOptions, int argc, char** argv) { cxxopts::ParseResult Result; try { Result = CmdOptions.parse(argc, argv); } catch (const std::exception& Ex) { throw zen::OptionParseException(Ex.what()); } CmdOptions.show_positional_help(); if (Result.count("help")) { printf("%s\n", CmdOptions.help().c_str()); return false; } if (!Result.unmatched().empty()) { zen::ExtendableStringBuilder<64> StringBuilder; for (bool First = true; const auto& Param : Result.unmatched()) { if (!First) { StringBuilder.Append(", "); } StringBuilder.Append('"'); StringBuilder.Append(Param); StringBuilder.Append('"'); First = false; } throw zen::OptionParseException(fmt::format("Invalid arguments: {}", StringBuilder.ToView())); } return true; } // Get the number of args including the sub command // Build an array for sub command to parse int ZenCmdBase::GetSubCommand(cxxopts::Options&, int argc, char** argv, std::span SubOptions, cxxopts::Options*& OutSubOption, std::vector& OutSubCommandArguments) { for (int i = 1; i < argc; ++i) { if (auto It = std::find_if(SubOptions.begin(), SubOptions.end(), [&](cxxopts::Options* SubOption) { return SubOption->program() == argv[i]; }); It != SubOptions.end()) { OutSubOption = (*It); OutSubCommandArguments.push_back(argv[0]); std::copy(&argv[i + 1], &argv[argc], std::back_inserter(OutSubCommandArguments)); return i + 1; } } // No Sub command found OutSubOption = nullptr; return argc; } std::string ZenCmdBase::FormatHttpResponse(const cpr::Response& Response) { if (Response.error.code != cpr::ErrorCode::OK) { if (Response.error.message.empty()) { return fmt::format("Request '{}' failed, error code {}", Response.url.str(), static_cast(Response.error.code)); } return fmt::format("Request '{}' failed. Reason: '{}' ({})", Response.url.str(), Response.error.message, static_cast(Response.error.code)); } std::string Content; if (auto It = Response.header.find("Content-Type"); It != Response.header.end()) { zen::HttpContentType ContentType = zen::ParseContentType(It->second); if (ContentType == zen::HttpContentType::kText) { Content = Response.text; } else if (ContentType == zen::HttpContentType::kJSON) { Content = fmt::format("\n{}", Response.text); } else if (!Response.text.empty()) { Content = fmt::format("[{}]", MapContentTypeToString(ContentType)); } } std::string_view ResponseString = zen::ReasonStringForHttpResultCode( Response.status_code == static_cast(zen::HttpResponseCode::NoContent) ? static_cast(zen::HttpResponseCode::OK) : Response.status_code); if (Content.empty()) { return std::string(ResponseString); } return fmt::format("{}: {}", ResponseString, Content); } int ZenCmdBase::MapHttpToCommandReturnCode(const cpr::Response& Response) { if (zen::IsHttpSuccessCode(Response.status_code)) { return 0; } if (Response.error.code != cpr::ErrorCode::OK) { return static_cast(Response.error.code); } 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 EffectivePort = zen::ParseInt(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); } #if ZEN_PLATFORM_WINDOWS static HANDLE GetConsoleHandle() { static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); return hStdOut; } #endif static bool CheckStdoutTty() { #if ZEN_PLATFORM_WINDOWS HANDLE hStdOut = GetConsoleHandle(); DWORD dwMode = 0; static bool IsConsole = ::GetConsoleMode(hStdOut, &dwMode); return IsConsole; #else return isatty(fileno(stdout)); #endif } static bool IsStdoutTty() { static bool StdoutIsTty = CheckStdoutTty(); return StdoutIsTty; } static void OutputToConsoleRaw(const char* String, size_t Length) { #if ZEN_PLATFORM_WINDOWS HANDLE hStdOut = GetConsoleHandle(); #endif #if ZEN_PLATFORM_WINDOWS if (IsStdoutTty()) { WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0); } else { ::WriteFile(hStdOut, (LPCVOID)String, (DWORD)Length, 0, 0); } #else fwrite(String, 1, Length, stdout); #endif } static void OutputToConsoleRaw(const std::string& String) { OutputToConsoleRaw(String.c_str(), String.length()); } static void OutputToConsoleRaw(const StringBuilderBase& SB) { OutputToConsoleRaw(SB.c_str(), SB.Size()); } static uint32_t GetConsoleColumns() { #if ZEN_PLATFORM_WINDOWS HANDLE hStdOut = GetConsoleHandle(); CONSOLE_SCREEN_BUFFER_INFO csbi; if (GetConsoleScreenBufferInfo(hStdOut, &csbi) == TRUE) { return (uint32_t)(csbi.srWindow.Right - csbi.srWindow.Left + 1); } #else struct winsize w; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) { return (uint32_t)w.ws_col; } #endif return 1024; } void ProgressBar::SetLogOperationName(Mode InMode, std::string_view Name) { if (InMode == Mode::Log) { std::string String = fmt::format("@progress {}\n", Name); OutputToConsoleRaw(String); } } void ProgressBar::SetLogOperationProgress(Mode InMode, uint32_t StepIndex, uint32_t StepCount) { if (InMode == Mode::Log) { const size_t PercentDone = StepCount > 0u ? gsl::narrow((100 * StepIndex) / StepCount) : 0u; std::string String = fmt::format("@progress {}%\n", PercentDone); OutputToConsoleRaw(String); } } ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask) : m_Mode((!IsStdoutTty() && InMode == Mode::Pretty) ? Mode::Plain : InMode) , m_LastUpdateMS((uint64_t)-1) , m_PausedMS(0) , m_SubTask(InSubTask) { if (!m_SubTask.empty() && InMode == Mode::Log) { std::string String = fmt::format("@progress push {}\n", m_SubTask); OutputToConsoleRaw(String); } } ProgressBar::~ProgressBar() { try { ForceLinebreak(); if (!m_SubTask.empty() && m_Mode == Mode::Log) { const std::string String("@progress pop\n"); OutputToConsoleRaw(String); } } catch (const std::exception& Ex) { ZEN_ERROR("ProgressBar::~ProgressBar() failed with {}", Ex.what()); } } void ProgressBar::UpdateState(const State& NewState, bool DoLinebreak) { ZEN_ASSERT(NewState.TotalCount >= NewState.RemainingCount); if (DoLinebreak == false && m_State == NewState) { return; } uint64_t ElapsedTimeMS = m_SW.GetElapsedTimeMs(); if (m_LastUpdateMS != (uint64_t)-1) { if (!DoLinebreak && (NewState.Status == m_State.Status) && (NewState.Task == m_State.Task) && ((m_LastUpdateMS + 200) > ElapsedTimeMS)) { return; } if (m_State.Status == State::EStatus::Paused) { uint64_t ElapsedSinceLast = ElapsedTimeMS - m_LastUpdateMS; m_PausedMS += ElapsedSinceLast; } } m_LastUpdateMS = ElapsedTimeMS; std::string Task = NewState.Task; switch (NewState.Status) { case State::EStatus::Aborted: Task = "Aborting"; break; case State::EStatus::Paused: Task = "Paused"; break; default: break; } if (NewState.Task.length() > Task.length()) { Task += std::string(NewState.Task.length() - Task.length(), ' '); } const size_t PercentDone = NewState.TotalCount > 0u ? gsl::narrow((100 * (NewState.TotalCount - NewState.RemainingCount)) / NewState.TotalCount) : 0u; if (m_Mode == Mode::Plain) { const std::string Details = (!NewState.Details.empty()) ? fmt::format(": {}", NewState.Details) : ""; const std::string Output = fmt::format("{} {}% ({}){}\n", Task, PercentDone, NiceTimeSpanMs(ElapsedTimeMS), Details); OutputToConsoleRaw(Output); } else if (m_Mode == Mode::Pretty) { size_t ProgressBarSize = 20; size_t ProgressBarCount = (ProgressBarSize * PercentDone) / 100; uint64_t Completed = NewState.TotalCount - NewState.RemainingCount; uint64_t ETAElapsedMS = ElapsedTimeMS -= m_PausedMS; uint64_t ETAMS = (NewState.Status == State::EStatus::Running) && (PercentDone > 5) ? (ETAElapsedMS * NewState.RemainingCount) / Completed : 0; uint32_t ConsoleColumns = GetConsoleColumns(); const std::string PercentString = fmt::format("{:#3}%", PercentDone); const std::string ProgressBarString = fmt::format(": |{}{}|", std::string(ProgressBarCount, '#'), std::string(ProgressBarSize - ProgressBarCount, ' ')); const std::string ElapsedString = fmt::format(": {}", NiceTimeSpanMs(ElapsedTimeMS)); const std::string ETAString = (ETAMS > 0) ? fmt::format(" ETA {}", NiceTimeSpanMs(ETAMS)) : ""; const std::string DetailsString = (!NewState.Details.empty()) ? fmt::format(". {}", NewState.Details) : ""; ExtendableStringBuilder<256> OutputBuilder; OutputBuilder << "\r" << Task << PercentString; if (OutputBuilder.Size() + 1 < ConsoleColumns) { size_t RemainingSpace = ConsoleColumns - (OutputBuilder.Size() + 1); bool ElapsedFits = RemainingSpace >= ElapsedString.length(); RemainingSpace -= ElapsedString.length(); bool ETAFits = ElapsedFits && RemainingSpace >= ETAString.length(); RemainingSpace -= ETAString.length(); bool DetailsFits = ETAFits && RemainingSpace >= DetailsString.length(); RemainingSpace -= DetailsString.length(); bool ProgressBarFits = DetailsFits && RemainingSpace >= ProgressBarString.length(); RemainingSpace -= ProgressBarString.length(); if (ProgressBarFits) { OutputBuilder << ProgressBarString; } if (ElapsedFits) { OutputBuilder << ElapsedString; } if (ETAFits) { OutputBuilder << ETAString; } if (DetailsFits) { OutputBuilder << DetailsString; } } std::string_view Output = OutputBuilder.ToView(); std::string::size_type EraseLength = m_LastOutputLength > Output.length() ? (m_LastOutputLength - Output.length()) : 0; ExtendableStringBuilder<256> LineToPrint; if (Output.length() + EraseLength >= ConsoleColumns) { if (m_LastOutputLength > 0) { LineToPrint << "\n"; } LineToPrint << Output.substr(1); DoLinebreak = true; } else { LineToPrint << Output << std::string(EraseLength, ' '); } if (DoLinebreak) { LineToPrint << "\n"; } OutputToConsoleRaw(LineToPrint); m_LastOutputLength = DoLinebreak ? 0 : Output.length(); m_State = NewState; } else if (m_Mode == Mode::Log) { if (m_State.Task != NewState.Task || m_State.Details != NewState.Details) // TODO: Should we output just because details change? Will this spam the log collector? { const std::string Details = (!NewState.Details.empty()) ? fmt::format(": {}", NewState.Details) : ""; const std::string Message = fmt::format("@progress {} ({}){}\n", NewState.Task, NiceTimeSpanMs(ElapsedTimeMS), Details); OutputToConsoleRaw(Message); } const size_t OldPercentDone = m_State.TotalCount > 0u ? gsl::narrow((100 * (m_State.TotalCount - m_State.RemainingCount)) / m_State.TotalCount) : 0u; if (OldPercentDone != PercentDone) { const std::string Progress = fmt::format("@progress {}%\n", PercentDone); OutputToConsoleRaw(Progress); } m_State = NewState; } } void ProgressBar::ForceLinebreak() { if (m_LastOutputLength > 0) { State NewState = m_State; UpdateState(NewState, /*DoLinebreak*/ true); } } void ProgressBar::Finish() { if (m_LastOutputLength > 0 || m_State.RemainingCount > 0) { State NewState = m_State; NewState.RemainingCount = 0; NewState.Details = ""; UpdateState(NewState, /*DoLinebreak*/ true); } m_State = State{}; m_LastOutputLength = 0; m_SW.Reset(); } bool ProgressBar::IsSameTask(std::string_view Task) const { return Task == m_State.Task; } bool ProgressBar::HasActiveTask() const { return !m_State.Task.empty(); } } // namespace zen ////////////////////////////////////////////////////////////////////////// // TODO: should make this Unicode-aware so we can pass anything in on the // command line. int main(int argc, char** argv) { zen::SetCurrentThreadName("main"); std::vector Args; #if ZEN_PLATFORM_WINDOWS LPWSTR RawCommandLine = GetCommandLine(); std::string CommandLine = zen::WideToUtf8(RawCommandLine); Args = zen::ParseCommandLine(CommandLine); #else Args.reserve(argc); for (int I = 0; I < argc; I++) { std::string Arg(argv[I]); if ((!Arg.empty()) && (Arg != " ")) { Args.emplace_back(std::move(Arg)); } } #endif std::vector RawArgs = zen::StripCommandlineQuotes(Args); argc = gsl::narrow(RawArgs.size()); argv = RawArgs.data(); using namespace zen; using namespace std::literals; AttachCommand AttachCmd; BenchCommand BenchCmd; BuildsCommand BuildsCmd; CacheDetailsCommand CacheDetailsCmd; CacheGetCommand CacheGetCmd; CacheGenerateCommand CacheGenerateCmd; CacheInfoCommand CacheInfoCmd; CacheStatsCommand CacheStatsCmd; CopyCommand CopyCmd; CopyStateCommand CopyStateCmd; CreateOplogCommand CreateOplogCmd; CreateProjectCommand CreateProjectCmd; DedupCommand DedupCmd; DownCommand DownCmd; DropCommand DropCmd; DropProjectCommand ProjectDropCmd; ExportOplogCommand ExportOplogCmd; FlushCommand FlushCmd; GcCommand GcCmd; GcStatusCommand GcStatusCmd; GcStopCommand GcStopCmd; ImportOplogCommand ImportOplogCmd; InfoCommand InfoCmd; JobCommand JobCmd; OplogMirrorCommand OplogMirrorCmd; SnapshotOplogCommand SnapshotOplogCmd; OplogValidateCommand OplogValidateCmd; PrintCommand PrintCmd; PrintPackageCommand PrintPkgCmd; ProjectOpDetailsCommand ProjectOpDetailsCmd; ProjectInfoCommand ProjectInfoCmd; ProjectStatsCommand ProjectStatsCmd; PsCommand PsCmd; RpcReplayCommand RpcReplayCmd; RpcStartRecordingCommand RpcStartRecordingCmd; RpcStopRecordingCommand RpcStopRecordingCmd; RunCommand RunCmd; ScrubCommand ScrubCmd; ServeCommand ServeCmd; StatusCommand StatusCmd; LoggingCommand LoggingCmd; TopCommand TopCmd; TraceCommand TraceCmd; UpCommand UpCmd; VersionCommand VersionCmd; VfsCommand VfsCmd; WipeCommand WipeCmd; WorkspaceCommand WorkspaceCmd; WorkspaceShareCommand WorkspaceShareCmd; const struct CommandInfo { const char* CmdName; ZenCmdBase* Cmd; const char* CmdSummary; } Commands[] = { // clang-format off {"attach", &AttachCmd, "Add a sponsor process to a running zen service"}, {"bench", &BenchCmd, "Utility command for benchmarking"}, {BuildsCommand::Name, &BuildsCmd, BuildsCommand::Description}, {"cache-details", &CacheDetailsCmd, "Details on cache"}, {"cache-info", &CacheInfoCmd, "Info on cache, namespace or bucket"}, {CacheGetCommand::Name, &CacheGetCmd, CacheGetCommand::Description}, {CacheGenerateCommand::Name, &CacheGenerateCmd, CacheGenerateCommand::Description}, {"cache-stats", &CacheStatsCmd, "Stats on cache"}, {"copy", &CopyCmd, "Copy file(s)"}, {"copy-state", &CopyStateCmd, "Copy zen server disk state"}, {"dedup", &DedupCmd, "Dedup files"}, {"down", &DownCmd, "Bring zen server down"}, {"drop", &DropCmd, "Drop cache namespace or bucket"}, {"gc-status", &GcStatusCmd, "Garbage collect zen storage status check"}, {"gc-stop", &GcStopCmd, "Request cancel of running garbage collection in zen storage"}, {"gc", &GcCmd, "Garbage collect zen storage"}, {"info", &InfoCmd, "Show high level Zen server information"}, {"jobs", &JobCmd, "Show/cancel zen background jobs"}, {"logs", &LoggingCmd, "Show/control zen logging"}, {"oplog-create", &CreateOplogCmd, "Create a project oplog"}, {"oplog-export", &ExportOplogCmd, "Export project store oplog"}, {"oplog-import", &ImportOplogCmd, "Import project store oplog"}, {"oplog-mirror", &OplogMirrorCmd, "Mirror project store oplog to file system"}, {"oplog-snapshot", &SnapshotOplogCmd, "Snapshot project store oplog"}, {"oplog-validate", &OplogValidateCmd, "Validate oplog for missing references"}, {"print", &PrintCmd, "Print compact binary object"}, {"printpackage", &PrintPkgCmd, "Print compact binary package"}, {"project-create", &CreateProjectCmd, "Create a project"}, {"project-op-details", &ProjectOpDetailsCmd, "Detail info on ops inside a project store oplog"}, {"project-drop", &ProjectDropCmd, "Drop project or project oplog"}, {"project-info", &ProjectInfoCmd, "Info on project or project oplog"}, {"project-stats", &ProjectStatsCmd, "Stats on project store"}, {"ps", &PsCmd, "Enumerate running zen server instances"}, {"rpc-record-replay", &RpcReplayCmd, "Replays a previously recorded session of rpc requests"}, {"rpc-record-start", &RpcStartRecordingCmd, "Starts recording of cache rpc requests on a host"}, {"rpc-record-stop", &RpcStopRecordingCmd, "Stops 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"}, {"top", &TopCmd, "Monitor zen server activity"}, {"trace", &TraceCmd, "Control zen realtime tracing"}, {"up", &UpCmd, "Bring zen server up"}, {VersionCommand::Name, &VersionCmd, VersionCommand::Description}, {"vfs", &VfsCmd, "Manage virtual file system"}, {"flush", &FlushCmd, "Flush storage"}, {WipeCommand::Name, &WipeCmd, WipeCommand::Description}, {WorkspaceCommand::Name, &WorkspaceCmd, WorkspaceCommand::Description}, {WorkspaceShareCommand::Name, &WorkspaceShareCmd, WorkspaceShareCommand::Description}, // clang-format on }; // Build set containing available commands std::unordered_set CommandSet; for (const auto& Cmd : Commands) CommandSet.insert(Cmd.CmdName); // Split command line into options, commands and any pass-through arguments std::string Passthrough; std::string PassthroughArgs; std::vector PassthroughArgV; for (int i = 1; i < argc; ++i) { 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) { AppendAscii(" "); } std::string_view ThisArg(argv[j]); PassthroughArgV.push_back(std::string(ThisArg)); const bool NeedsQuotes = (ThisArg.find(' ') != std::string_view::npos) && !(ThisArg.starts_with("\"") && ThisArg.ends_with("\"")); if (NeedsQuotes) { AppendAscii("\""); } AppendAscii(ThisArg); if (NeedsQuotes) { AppendAscii("\""); } IsFirst = false; } Passthrough = Line.c_str(); PassthroughArgs = Arguments.c_str(); // This will "truncate" the arg vector and terminate the loop argc = i; } } // Split command line into global vs command options. We do this by simply // scanning argv for a string we recognise as a command and split it there std::vector CommandArgVec; CommandArgVec.push_back(argv[0]); for (int i = 1; i < argc; ++i) { if (CommandSet.find(argv[i]) != CommandSet.end()) { int commandArgCount = /* exec name */ 1 + argc - (i + 1); CommandArgVec.resize(commandArgCount); std::copy(argv + i + 1, argv + argc, CommandArgVec.begin() + 1); argc = i + 1; break; } } // Parse global CLI arguments ZenCliOptions GlobalOptions; GlobalOptions.PassthroughCommandLine = Passthrough; GlobalOptions.PassthroughArgs = PassthroughArgs; GlobalOptions.PassthroughArgV = PassthroughArgV; std::string MemoryOptions; std::string SubCommand = ""; cxxopts::Options Options("zen", "Zen management tool"); Options.add_options()("d, debug", "Enable debugging", cxxopts::value(GlobalOptions.IsDebug)); Options.add_options()("v, verbose", "Enable verbose logging", cxxopts::value(GlobalOptions.IsVerbose)); Options.add_options()("malloc", "Configure memory allocator subsystem", cxxopts::value(MemoryOptions)->default_value("mimalloc")); Options.add_options()("help", "Show command line help"); Options.add_options()("c, command", "Sub command", cxxopts::value(SubCommand)); #if ZEN_WITH_TRACE std::string TraceChannels; std::string TraceHost; std::string TraceFile; Options.add_option("ue-trace", "", "trace", "Specify which trace channels should be enabled", cxxopts::value(TraceChannels)->default_value(""), ""); Options.add_option("ue-trace", "", "tracehost", "Hostname to send the trace to", cxxopts::value(TraceHost)->default_value(""), ""); Options .add_option("ue-trace", "", "tracefile", "Path to write a trace to", cxxopts::value(TraceFile)->default_value(""), ""); #endif // ZEN_WITH_TRACE #if ZEN_USE_SENTRY SentryIntegration::Config SentryConfig; bool NoSentry = false; Options .add_option("sentry", "", "no-sentry", "Disable Sentry crash handler", cxxopts::value(NoSentry)->default_value("false"), ""); Options.add_option("sentry", "", "sentry-allow-personal-info", "Allow personally identifiable information in sentry crash reports", cxxopts::value(SentryConfig.AllowPII)->default_value("false"), ""); Options.add_option("sentry", "", "sentry-dsn", "Sentry DSN to send events to", cxxopts::value(SentryConfig.Dsn), ""); Options.add_option("sentry", "", "sentry-environment", "Sentry environment", cxxopts::value(SentryConfig.Environment), ""); Options.add_options()("sentry-debug", "Enable debug mode for Sentry", cxxopts::value(SentryConfig.Debug)->default_value("false")); #endif Options.parse_positional({"command"}); const bool IsNullInvoke = (argc == 1); // If no arguments are passed we want to print usage information try { cxxopts::ParseResult ParseResult = Options.parse(argc, argv); if (ParseResult.count("help") || IsNullInvoke == 1) { std::string Help = Options.help(); printf("%s\n", Help.c_str()); printf("available commands:\n"); std::map Categories; for (const CommandInfo& CmdInfo : Commands) { ZenCmdCategory& Category = CmdInfo.Cmd->CommandCategory(); Categories[Category.Name] = &Category; Category.SortedCmds[CmdInfo.CmdName] = CmdInfo.CmdSummary; } for (const auto& CategoryKv : Categories) { 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); } #if ZEN_USE_SENTRY { EnvironmentOptions EnvOptions; EnvOptions.AddOption("UE_ZEN_SENTRY_DSN"sv, SentryConfig.Dsn, "sentry-dsn"sv); EnvOptions.AddOption("UE_ZEN_SENTRY_ALLOWPERSONALINFO"sv, SentryConfig.AllowPII, "sentry-allow-personal-info"sv); EnvOptions.AddOption("UE_ZEN_SENTRY_ENVIRONMENT"sv, SentryConfig.Environment, "sentry-environment"sv); bool EnvEnableSentry = !NoSentry; EnvOptions.AddOption("UE_ZEN_SENTRY_ENABLED"sv, EnvEnableSentry, "no-sentry"sv); EnvOptions.AddOption("UE_ZEN_SENTRY_DEBUG"sv, SentryConfig.Debug, "sentry-debug"sv); EnvOptions.Parse(ParseResult); if (EnvEnableSentry != !NoSentry) { NoSentry = !EnvEnableSentry; } } SentryIntegration Sentry; if (NoSentry == false) { std::string SentryDatabasePath = (std::filesystem::temp_directory_path() / ".zen-sentry-native").string(); ExtendableStringBuilder<512> SB; for (int i = 0; i < argc; ++i) { if (i) { SB.Append(' '); } SB.Append(argv[i]); } SentryConfig.DatabasePath = SentryDatabasePath; Sentry.Initialize(SentryConfig, SB.ToString()); SentryIntegration::ClearCaches(); } #endif zen::LoggingOptions LogOptions; LogOptions.IsDebug = GlobalOptions.IsDebug; LogOptions.IsVerbose = GlobalOptions.IsVerbose; LogOptions.AllowAsync = false; zen::InitializeLogging(LogOptions); std::set_terminate([]() { void* Frames[8]; uint32_t FrameCount = GetCallstack(2, 8, Frames); CallstackFrames* Callstack = CreateCallstack(FrameCount, Frames); ZEN_CRITICAL("Program exited abnormally via std::terminate()\n{}", CallstackToString(Callstack, " ")); FreeCallstack(Callstack); }); zen::MaximizeOpenFileCount(); ////////////////////////////////////////////////////////////////////////// auto _ = zen::MakeGuard([] { zen::ShutdownLogging(); }); #if ZEN_WITH_TRACE if (TraceHost.size()) { TraceStart("zen", TraceHost.c_str(), TraceType::Network); } else if (TraceFile.size()) { TraceStart("zen", TraceFile.c_str(), TraceType::File); } else { TraceInit("zen"); } #endif // ZEN_WITH_TRACE #if ZEN_WITH_MEMTRACK FMalloc* TraceMalloc = MemoryTrace_Create(GMalloc); if (TraceMalloc != GMalloc) { GMalloc = TraceMalloc; MemoryTrace_Initialize(); } #endif for (const CommandInfo& CmdInfo : Commands) { if (StrCaseCompare(SubCommand.c_str(), CmdInfo.CmdName) == 0) { cxxopts::Options& VerbOptions = CmdInfo.Cmd->Options(); try { return CmdInfo.Cmd->Run(GlobalOptions, (int)CommandArgVec.size(), CommandArgVec.data()); } catch (const OptionParseException& Ex) { std::string help = VerbOptions.help(); printf("Error parsing arguments for command '%s': %s\n\n%s", SubCommand.c_str(), Ex.what(), help.c_str()); exit(11); } } } printf("Unknown command specified: '%s', exiting\n", SubCommand.c_str()); } catch (const OptionParseException& Ex) { std::string HelpMessage = Options.help(); printf("Error parsing program arguments: %s\n\n%s", Ex.what(), HelpMessage.c_str()); return 9; } catch (const std::system_error& Ex) { printf("System Error: %s\n", Ex.what()); return Ex.code() ? Ex.code().value() : 10; } catch (const std::exception& Ex) { printf("Error: %s\n", Ex.what()); return 11; } return 0; }