// Copyright Epic Games, Inc. All Rights Reserved. #include "config.h" #include "storage/storageconfig.h" #include "config/luaconfig.h" #include "diag/logging.h" #include #include #include #include #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include #include ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_PLATFORM_WINDOWS # include #else # include #endif namespace zen { std::filesystem::path PickDefaultStateDirectory(std::filesystem::path SystemRoot) { if (SystemRoot.empty()) return SystemRoot; return SystemRoot / "Data"; } void EmitCentralManifest(const std::filesystem::path& SystemRoot, Oid Identifier, CbObject Manifest, std::filesystem::path ManifestPath) { CbObjectWriter Cbo; Cbo << "path" << ManifestPath.generic_wstring(); Cbo << "manifest" << Manifest; const std::filesystem::path StatesPath = SystemRoot / "States"; CreateDirectories(StatesPath); WriteFile(StatesPath / fmt::format("{}", Identifier), Cbo.Save().GetBuffer().AsIoBuffer()); } std::vector ReadAllCentralManifests(const std::filesystem::path& SystemRoot) { std::vector Manifests; DirectoryContent Content; GetDirectoryContent(SystemRoot / "States", DirectoryContentFlags::IncludeFiles, Content); for (std::filesystem::path& File : Content.Files) { try { FileContents FileData = ReadFile(File); CbValidateError ValidateError; if (CbObject Manifest = ValidateAndReadCompactBinaryObject(FileData.Flatten(), ValidateError); ValidateError == CbValidateError::None) { Manifests.emplace_back(std::move(Manifest)); } else { ZEN_WARN("failed to load manifest '{}': {}", File, ToString(ValidateError)); } } catch (const std::exception& Ex) { ZEN_WARN("failed to load manifest '{}': {}", File, Ex.what()); } } return Manifests; } void ZenServerConfiguratorBase::ParseEnvVariables(const cxxopts::ParseResult& CmdLineResult) { using namespace std::literals; EnvironmentOptions Options; ZenServerConfig& ServerOptions = m_ServerOptions; bool EnvEnableSentry = !ServerOptions.SentryConfig.Disable; // clang-format off Options.AddOption("UE_ZEN_SENTRY_ALLOWPERSONALINFO"sv, ServerOptions.SentryConfig.AllowPII, "sentry-allow-personal-info"sv); Options.AddOption("UE_ZEN_SENTRY_DSN"sv, ServerOptions.SentryConfig.Dsn, "sentry-dsn"sv); Options.AddOption("UE_ZEN_SENTRY_ENVIRONMENT"sv, ServerOptions.SentryConfig.Environment, "sentry-environment"sv); Options.AddOption("UE_ZEN_SENTRY_ENABLED"sv, EnvEnableSentry, "no-sentry"sv); Options.AddOption("UE_ZEN_SENTRY_DEBUG"sv, ServerOptions.SentryConfig.Debug, "sentry-debug"sv); // clang-format on Options.Parse(CmdLineResult); if (EnvEnableSentry != !ServerOptions.SentryConfig.Disable) { ServerOptions.SentryConfig.Disable = !EnvEnableSentry; } } void ZenServerConfiguratorBase::AddCommonConfigOptions(LuaConfig::Options& LuaOptions) { using namespace std::literals; ZenServerConfig& ServerOptions = m_ServerOptions; // server LuaOptions.AddOption("server.dedicated"sv, ServerOptions.IsDedicated, "dedicated"sv); LuaOptions.AddOption("server.logid"sv, ServerOptions.LogId, "log-id"sv); LuaOptions.AddOption("server.sentry.disable"sv, ServerOptions.SentryConfig.Disable, "no-sentry"sv); LuaOptions.AddOption("server.sentry.allowpersonalinfo"sv, ServerOptions.SentryConfig.AllowPII, "sentry-allow-personal-info"sv); LuaOptions.AddOption("server.sentry.dsn"sv, ServerOptions.SentryConfig.Dsn, "sentry-dsn"sv); LuaOptions.AddOption("server.sentry.environment"sv, ServerOptions.SentryConfig.Environment, "sentry-environment"sv); LuaOptions.AddOption("server.sentry.debug"sv, ServerOptions.SentryConfig.Debug, "sentry-debug"sv); LuaOptions.AddOption("server.systemrootdir"sv, ServerOptions.SystemRootDir, "system-dir"sv); LuaOptions.AddOption("server.datadir"sv, ServerOptions.DataDir, "data-dir"sv); LuaOptions.AddOption("server.contentdir"sv, ServerOptions.ContentDir, "content-dir"sv); LuaOptions.AddOption("server.abslog"sv, ServerOptions.AbsLogFile, "abslog"sv); LuaOptions.AddOption("server.otlpendpoint"sv, ServerOptions.OtelEndpointUri, "otlp-endpoint"sv); LuaOptions.AddOption("server.debug"sv, ServerOptions.IsDebug, "debug"sv); LuaOptions.AddOption("server.clean"sv, ServerOptions.IsCleanStart, "clean"sv); LuaOptions.AddOption("server.quiet"sv, ServerOptions.QuietConsole, "quiet"sv); LuaOptions.AddOption("server.noconsole"sv, ServerOptions.NoConsoleOutput, "noconsole"sv); ////// network LuaOptions.AddOption("network.httpserverclass"sv, ServerOptions.HttpConfig.ServerClass, "http"sv); LuaOptions.AddOption("network.httpserverthreads"sv, ServerOptions.HttpConfig.ThreadCount, "http-threads"sv); LuaOptions.AddOption("network.port"sv, ServerOptions.BasePort, "port"sv); LuaOptions.AddOption("network.forceloopback"sv, ServerOptions.HttpConfig.ForceLoopback, "http-forceloopback"sv); #if ZEN_WITH_HTTPSYS LuaOptions.AddOption("network.httpsys.async.workthreads"sv, ServerOptions.HttpConfig.HttpSys.AsyncWorkThreadCount, "httpsys-async-work-threads"sv); LuaOptions.AddOption("network.httpsys.async.response"sv, ServerOptions.HttpConfig.HttpSys.IsAsyncResponseEnabled, "httpsys-enable-async-response"sv); LuaOptions.AddOption("network.httpsys.requestlogging"sv, ServerOptions.HttpConfig.HttpSys.IsRequestLoggingEnabled, "httpsys-enable-request-logging"sv); #endif #if ZEN_WITH_TRACE ////// trace LuaOptions.AddOption("trace.channels"sv, ServerOptions.TraceCmdLineOptions.Channels, "trace"sv); LuaOptions.AddOption("trace.host"sv, ServerOptions.TraceCmdLineOptions.Host, "tracehost"sv); LuaOptions.AddOption("trace.file"sv, ServerOptions.TraceCmdLineOptions.File, "tracefile"sv); #endif ////// stats LuaOptions.AddOption("stats.enable"sv, ServerOptions.StatsConfig.Enabled, "statsd"sv); LuaOptions.AddOption("stats.host"sv, ServerOptions.StatsConfig.StatsdHost); LuaOptions.AddOption("stats.port"sv, ServerOptions.StatsConfig.StatsdPort); } struct ZenServerCmdLineOptions { // Note to those adding future options; std::filesystem::path-type options // must be read into a std::string first. As of cxxopts-3.0.0 it uses a >> // stream operator to convert argv value into the options type. std::fs::path // expects paths in streams to be quoted but argv paths are unquoted. By // going into a std::string first, paths with whitespace parse correctly. std::string ConfigFile; std::string OutputConfigFile; std::string SystemRootDir; std::string ContentDir; std::string DataDir; std::string AbsLogFile; std::string BaseSnapshotDir; void AddCliOptions(cxxopts::Options& options, ZenServerConfig& ServerOptions); void ApplyOptions(cxxopts::Options& options, ZenServerConfig& ServerOptions); }; void ZenServerCmdLineOptions::AddCliOptions(cxxopts::Options& options, ZenServerConfig& ServerOptions) { const char* DefaultHttp = "asio"; #if ZEN_WITH_HTTPSYS if (!windows::IsRunningOnWine()) { DefaultHttp = "httpsys"; } #endif options.add_options()("dedicated", "Enable dedicated server mode", cxxopts::value(ServerOptions.IsDedicated)->default_value("false")); options.add_options()("d, debug", "Enable debugging", cxxopts::value(ServerOptions.IsDebug)->default_value("false")); options.add_options()("clean", "Clean out all state at startup", cxxopts::value(ServerOptions.IsCleanStart)->default_value("false")); options.add_options()("help", "Show command line help"); options.add_options()("t, test", "Enable test mode", cxxopts::value(ServerOptions.IsTest)->default_value("false")); options.add_options()("data-dir", "Specify persistence root", cxxopts::value(DataDir)); options.add_options()("system-dir", "Specify system root", cxxopts::value(SystemRootDir)); options.add_options()("content-dir", "Frontend content directory", cxxopts::value(ContentDir)); options.add_options()("config", "Path to Lua config file", cxxopts::value(ConfigFile)); options.add_options()("write-config", "Path to output Lua config file", cxxopts::value(OutputConfigFile)); options.add_options()("snapshot-dir", "Specify a snapshot of server state to mirror into the persistence root at startup", cxxopts::value(BaseSnapshotDir)); options.add_options()("no-sentry", "Disable Sentry crash handler", cxxopts::value(ServerOptions.SentryConfig.Disable)->default_value("false")); options.add_options()("sentry-allow-personal-info", "Allow personally identifiable information in sentry crash reports", cxxopts::value(ServerOptions.SentryConfig.AllowPII)->default_value("false")); options.add_options()("sentry-dsn", "Sentry DSN to send events to", cxxopts::value(ServerOptions.SentryConfig.Dsn)); options.add_options()("sentry-environment", "Sentry environment", cxxopts::value(ServerOptions.SentryConfig.Environment)); options.add_options()("sentry-debug", "Enable debug mode for Sentry", cxxopts::value(ServerOptions.SentryConfig.Debug)->default_value("false")); options.add_options()("detach", "Indicate whether zenserver should detach from parent process group", cxxopts::value(ServerOptions.Detach)->default_value("true")); options.add_options()("malloc", "Configure memory allocator subsystem", cxxopts::value(ServerOptions.MemoryOptions)->default_value("mimalloc")); options.add_options()("corelimit", "Limit concurrency", cxxopts::value(ServerOptions.CoreLimit)); options.add_options()("powercycle", "Exit immediately after initialization is complete", cxxopts::value(ServerOptions.IsPowerCycle)); options.add_option("diagnostics", "", "crash", "Simulate a crash", cxxopts::value(ServerOptions.ShouldCrash)->default_value("false"), ""); // clang-format off options.add_options("logging") ("abslog", "Path to log file", cxxopts::value(AbsLogFile)) ("log-id", "Specify id for adding context to log output", cxxopts::value(ServerOptions.LogId)) ("quiet", "Configure console logger output to level WARN", cxxopts::value(ServerOptions.QuietConsole)->default_value("false")) ("noconsole", "Disable console logging", cxxopts::value(ServerOptions.NoConsoleOutput)->default_value("false")) ("log-trace", "Change selected loggers to level TRACE", cxxopts::value(ServerOptions.Loggers[logging::level::Trace])) ("log-debug", "Change selected loggers to level DEBUG", cxxopts::value(ServerOptions.Loggers[logging::level::Debug])) ("log-info", "Change selected loggers to level INFO", cxxopts::value(ServerOptions.Loggers[logging::level::Info])) ("log-warn", "Change selected loggers to level WARN", cxxopts::value(ServerOptions.Loggers[logging::level::Warn])) ("log-error", "Change selected loggers to level ERROR", cxxopts::value(ServerOptions.Loggers[logging::level::Err])) ("log-critical", "Change selected loggers to level CRITICAL", cxxopts::value(ServerOptions.Loggers[logging::level::Critical])) ("log-off", "Change selected loggers to level OFF", cxxopts::value(ServerOptions.Loggers[logging::level::Off])) ("otlp-endpoint", "OpenTelemetry endpoint URI (e.g http://localhost:4318)", cxxopts::value(ServerOptions.OtelEndpointUri)) ; // clang-format on options .add_option("lifetime", "", "owner-pid", "Specify owning process id", cxxopts::value(ServerOptions.OwnerPid), ""); options.add_option("lifetime", "", "child-id", "Specify id which can be used to signal parent", cxxopts::value(ServerOptions.ChildId), ""); #if ZEN_PLATFORM_WINDOWS options.add_option("lifetime", "", "install", "Install zenserver as a Windows service", cxxopts::value(ServerOptions.InstallService), ""); options.add_option("lifetime", "", "uninstall", "Uninstall zenserver as a Windows service", cxxopts::value(ServerOptions.UninstallService), ""); #endif options.add_option("network", "", "http-threads", "Number of http server connection threads", cxxopts::value(ServerOptions.HttpConfig.ThreadCount), ""); options.add_option("network", "p", "port", "Select HTTP port", cxxopts::value(ServerOptions.BasePort)->default_value("8558"), ""); options.add_option("network", "", "http-forceloopback", "Force using local loopback interface", cxxopts::value(ServerOptions.HttpConfig.ForceLoopback)->default_value("false"), ""); #if ZEN_WITH_HTTPSYS options.add_option("httpsys", "", "httpsys-async-work-threads", "Number of HttpSys async worker threads", cxxopts::value(ServerOptions.HttpConfig.HttpSys.AsyncWorkThreadCount), ""); options.add_option("httpsys", "", "httpsys-enable-async-response", "Enables Httpsys async response", cxxopts::value(ServerOptions.HttpConfig.HttpSys.IsAsyncResponseEnabled)->default_value("true"), ""); options.add_option("httpsys", "", "httpsys-enable-request-logging", "Enables Httpsys request logging", cxxopts::value(ServerOptions.HttpConfig.HttpSys.IsRequestLoggingEnabled), ""); #endif options.add_option("network", "", "http", "Select HTTP server implementation (asio|" #if ZEN_WITH_HTTPSYS "httpsys|" #endif "null)", cxxopts::value(ServerOptions.HttpConfig.ServerClass)->default_value(DefaultHttp), ""); #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(ServerOptions.TraceCmdLineOptions.Channels)->default_value(""), ""); options.add_option("ue-trace", "", "tracehost", "Hostname to send the trace to", cxxopts::value(ServerOptions.TraceCmdLineOptions.Host)->default_value(""), ""); options.add_option("ue-trace", "", "tracefile", "Path to write a trace to", cxxopts::value(ServerOptions.TraceCmdLineOptions.File)->default_value(""), ""); #endif // ZEN_WITH_TRACE options.add_option("stats", "", "statsd", "", cxxopts::value(ServerOptions.StatsConfig.Enabled)->default_value("false"), "Enable statsd reporter (localhost:8125)"); } void ZenServerCmdLineOptions::ApplyOptions(cxxopts::Options& options, ZenServerConfig& ServerOptions) { if (!BaseSnapshotDir.empty()) { if (ServerOptions.DataDir.empty()) throw OptionParseException("'--snapshot-dir' requires '--data-dir'", options.help()); if (!IsDir(ServerOptions.BaseSnapshotDir)) throw std::runtime_error(fmt::format("'--snapshot-dir' ('{}') must be a directory", ServerOptions.BaseSnapshotDir)); } ServerOptions.SystemRootDir = MakeSafeAbsolutePath(SystemRootDir); ServerOptions.DataDir = MakeSafeAbsolutePath(DataDir); ServerOptions.ContentDir = MakeSafeAbsolutePath(ContentDir); ServerOptions.AbsLogFile = MakeSafeAbsolutePath(AbsLogFile); ServerOptions.ConfigFile = MakeSafeAbsolutePath(ConfigFile); ServerOptions.BaseSnapshotDir = MakeSafeAbsolutePath(BaseSnapshotDir); } ////////////////////////////////////////////////////////////////////////// ZenServerConfiguratorBase::ZenServerConfiguratorBase(ZenServerConfig& BaseServerOptions) : m_ServerOptions(BaseServerOptions) { } ZenServerConfiguratorBase::~ZenServerConfiguratorBase() { } void ZenServerConfiguratorBase::Configure(int argc, char* argv[]) { for (int i = 0; i < argc; ++i) { if (i) { m_ServerOptions.CommandLine.push_back(' '); } m_ServerOptions.CommandLine += argv[i]; } cxxopts::Options options("zenserver", "Zen Storage Server"); ZenServerCmdLineOptions BaseOptions; BaseOptions.AddCliOptions(options, m_ServerOptions); // Add subclass' command line options AddCliOptions(options); try { cxxopts::ParseResult Result; try { Result = options.parse(argc, argv); } catch (const std::exception& Ex) { throw OptionParseException(Ex.what(), options.help()); } if (Result.count("help")) { ZEN_CONSOLE("{}", options.help()); #if ZEN_PLATFORM_WINDOWS ZEN_CONSOLE("Press any key to exit!"); _getch(); #else // Assume the user's in a terminal on all other platforms and that // they'll use less/more/etc. if need be. #endif exit(0); } #if ZEN_WITH_TRACE if (!m_ServerOptions.HasTraceCommandlineOptions) { // Apply any Lua settings if we don't have them set from the command line TraceConfigure(m_ServerOptions.TraceCmdLineOptions); } #endif if (m_ServerOptions.QuietConsole) { bool HasExplicitConsoleLevel = false; for (int i = 0; i < logging::level::LogLevelCount; ++i) { if (m_ServerOptions.Loggers[i].find("console") != std::string::npos) { HasExplicitConsoleLevel = true; break; } } if (!HasExplicitConsoleLevel) { std::string& WarnLoggers = m_ServerOptions.Loggers[logging::level::Warn]; if (!WarnLoggers.empty()) { WarnLoggers += ","; } WarnLoggers += "console"; } } for (int i = 0; i < logging::level::LogLevelCount; ++i) { logging::ConfigureLogLevels(logging::level::LogLevel(i), m_ServerOptions.Loggers[i]); } logging::RefreshLogLevels(); BaseOptions.ApplyOptions(options, m_ServerOptions); ApplyOptions(options); // Handle any environment variables ParseEnvVariables(Result); ZEN_TRACE_CPU("ConfigParse"); if (!m_ServerOptions.ConfigFile.empty()) { ParseConfigFile(m_ServerOptions.ConfigFile, Result, BaseOptions.OutputConfigFile); } else { ParseConfigFile(m_ServerOptions.DataDir / "zen_cfg.lua", Result, BaseOptions.OutputConfigFile); } ValidateOptions(); // subclass validation } catch (const OptionParseException& e) { ZEN_CONSOLE("{}\n", options.help()); ZEN_CONSOLE_ERROR("Invalid zenserver arguments: {}", e.what()); throw; } if (m_ServerOptions.SystemRootDir.empty()) { m_ServerOptions.SystemRootDir = PickDefaultSystemRootDirectory(); } if (m_ServerOptions.DataDir.empty()) { m_ServerOptions.DataDir = PickDefaultStateDirectory(m_ServerOptions.SystemRootDir); } if (m_ServerOptions.AbsLogFile.empty()) { m_ServerOptions.AbsLogFile = m_ServerOptions.DataDir / "logs" / "zenserver.log"; } m_ServerOptions.HttpConfig.IsDedicatedServer = m_ServerOptions.IsDedicated; } void ZenServerConfiguratorBase::ParseConfigFile(const std::filesystem::path& Path, const cxxopts::ParseResult& CmdLineResult, std::string_view OutputConfigFile) { ZEN_TRACE_CPU("ParseConfigFile"); LuaConfig::Options LuaOptions; AddCommonConfigOptions(LuaOptions); AddConfigOptions(LuaOptions); // subclass options LuaOptions.Parse(Path, CmdLineResult); OnConfigFileParsed(LuaOptions); // subclass hook if (!OutputConfigFile.empty()) { ExtendableStringBuilder<512> ConfigStringBuilder; LuaOptions.Print(ConfigStringBuilder, CmdLineResult); BasicFile Output; std::filesystem::path WritePath(MakeSafeAbsolutePath(OutputConfigFile)); Output.Open(WritePath, BasicFile::Mode::kTruncate); Output.Write(ConfigStringBuilder.Data(), ConfigStringBuilder.Size(), 0); } } } // namespace zen