// Copyright Epic Games, Inc. All Rights Reserved. // Zen command line client utility // #include "zenmaster.h" #include #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 ZEN_THIRD_PARTY_INCLUDES_END #include ////////////////////////////////////////////////////////////////////////// #if ZEN_PLATFORM_WINDOWS # include #else # include # include # include #endif namespace zen { #if ZEN_PLATFORM_WINDOWS int getch(void) { return _getch(); } #else int getch(void) { char buf = 0; struct termios old = {0}; fflush(stdout); if (tcgetattr(0, &old) < 0) perror("tcsetattr()"); old.c_lflag &= ~ICANON; old.c_lflag &= ~ECHO; old.c_cc[VMIN] = 1; old.c_cc[VTIME] = 0; if (tcsetattr(0, TCSANOW, &old) < 0) perror("tcsetattr ICANON"); if (read(0, &buf, 1) < 0) perror("read()"); old.c_lflag |= ICANON; old.c_lflag |= ECHO; if (tcsetattr(0, TCSADRAIN, &old) < 0) perror("tcsetattr ~ICANON"); return buf; } #endif } // namespace zen 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; #if ZEN_WITH_TRACE TraceInit("zen"); TraceOptions TraceCommandlineOptions; if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) { TraceConfigure(TraceCommandlineOptions); } #endif // ZEN_WITH_TRACE // 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; } } // Parse global CLI arguments ZenMasterCliOptions GlobalOptions; GlobalOptions.PassthroughCommandLine = Passthrough; GlobalOptions.PassthroughArgs = PassthroughArgs; GlobalOptions.PassthroughArgV = PassthroughArgV; std::string MemoryOptions; std::string SubCommand = ""; cxxopts::Options Options("zenmaster", "Zen master orchestration 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"); int ServerSpawnCount = 100; Options.add_options()("count", "Number of servers to spawn", cxxopts::value(ServerSpawnCount)); #if ZEN_WITH_TRACE // We only have this in options for command line help purposes - we parse these argument separately earlier using // GetTraceOptionsFromCommandline() Options.add_option("ue-trace", "", "trace", "Specify which trace channels should be enabled", cxxopts::value(TraceCommandlineOptions.Channels)->default_value(""), ""); Options.add_option("ue-trace", "", "tracehost", "Hostname to send the trace to", cxxopts::value(TraceCommandlineOptions.Host)->default_value(""), ""); Options.add_option("ue-trace", "", "tracefile", "Path to write a trace to", cxxopts::value(TraceCommandlineOptions.File)->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 try { cxxopts::ParseResult ParseResult = Options.parse(argc, argv); if (ParseResult.count("help")) { std::string Help = Options.help(); printf("%s\n", Help.c_str()); 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([] { ShutdownWorkerPools(); ShutdownLogging(); }); // Spawn some zenserver processes zen::ZenServerEnvironment TestEnv; std::filesystem::path ProgramBaseDir = GetRunningExecutablePath().parent_path(); std::filesystem::path TestBaseDir = std::filesystem::current_path() / ".test"; const std::string ServerClass; TestEnv.InitializeForTest(ProgramBaseDir, TestBaseDir, ServerClass); auto TimedBlock = [&](const std::string_view Tag, auto&& Fun) { Stopwatch t; ZEN_INFO("BEGIN {}", Tag); Fun(); ZEN_INFO("END {}, took {}", Tag, NiceTimeSpanMs(t.GetElapsedTimeMs())); }; std::vector> Instances; TimedBlock(fmt::format("Spawning {} instances", ServerSpawnCount), [&] { TimedBlock("Spawning instances", [&] { for (int i = 0; i < ServerSpawnCount; ++i) { auto& Instance = Instances.emplace_back(std::make_unique(TestEnv)); std::filesystem::path TestDir1 = TestEnv.CreateNewTestDir(); Instance->SetTestDir(TestDir1); Instance->SpawnServer("--malloc=ansi --corelimit=4"); } }); TimedBlock("Waiting for instances", [&] { for (int i = 0; i < ServerSpawnCount; ++i) { auto& Instance = Instances[i]; const uint16_t PortNum = Instance->WaitUntilReady(); ZEN_INFO("Instance #{} UP - port {}", i, PortNum); } }); }); ZEN_INFO("press any key to tear instances down"); zen::getch(); TimedBlock("Shutting down instances", [&] { for (int i = 0; i < ServerSpawnCount; ++i) { auto& Instance = Instances[i]; ZEN_INFO("Shutting down instance #{}...", i); Instance->Shutdown(); ZEN_INFO("Instance #{} DOWN", i); } }); } 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; }