// Copyright Epic Games, Inc. All Rights Reserved. #include "Experimental/ZenServerInterface.h" #include "ZenBackendUtils.h" #include "ZenSerialization.h" #include "ZenServerHttp.h" #include "ZenServerState.h" #include "ZenVersion.h" #include "AnalyticsEventAttribute.h" #include "Async/Async.h" #include "Async/UniqueLock.h" #include "Containers/AnsiString.h" #include "Dom/JsonValue.h" #include "HAL/FileManager.h" #include "HAL/Platform.h" #include "HAL/PlatformMisc.h" #include "HAL/PlatformProcess.h" #include "HAL/PlatformTime.h" #include "Logging/LogScopedCategoryAndVerbosityOverride.h" #include "Logging/StructuredLog.h" #include "Misc/App.h" #include "Misc/ConfigCacheIni.h" #include "Misc/ConfigContext.h" #include "Misc/FileHelper.h" #include "Misc/MonitoredProcess.h" #include "Misc/Paths.h" #include "Misc/PathViews.h" #include "Misc/ScopedSlowTask.h" #include "Misc/ScopeExit.h" #include "Serialization/MemoryReader.h" #include "Serialization/CompactBinarySerialization.h" #include "Serialization/CompactBinaryValue.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "String/LexFromString.h" #include "Serialization/CompactBinaryWriter.h" #if PLATFORM_WINDOWS # include "Windows/AllowWindowsPlatformTypes.h" # include # include "Windows/HideWindowsPlatformTypes.h" #endif #define ALLOW_SETTINGS_OVERRIDE_FROM_COMMANDLINE (UE_SERVER || !(UE_BUILD_SHIPPING)) namespace UE::Zen { DEFINE_LOG_CATEGORY_STATIC(LogZenServiceInstance, Log, All); FZenServiceEndpoint::FZenServiceEndpoint(FStringView InName, uint16 InPort) : Name(InName) , Port(InPort) { if (InName.EndsWith(TEXT(".sock"))) { checkf(Port == 0, TEXT("Host name appears to be a Unix socket path (contains slashes) and therefore port should be 0")); SocketType = ESocketType::Unix; } } FStringView FZenServiceEndpoint::GetHostName() const { return (GetSocketType() == ESocketType::Tcp) ? Name : TEXTVIEW("localhost"); } FString FZenServiceEndpoint::GetURL() const { TStringBuilder<128> Builder; Builder << TEXTVIEW("http://"); Builder << GetHostName(); if (Port > 0) { Builder << TEXT(":") << Port; } return FString(Builder); } struct FZenServiceLink { FString ServicePath; FString UtilityPath; FZenVersion Version; operator bool() const { return !ServicePath.IsEmpty() && !UtilityPath.IsEmpty() && Version; } static FZenServiceLink Read(const FString& Filename) { FString JsonText; if (FFileHelper::LoadFileToString(JsonText, *Filename)) { TSharedPtr JsonObject; TSharedRef> Reader = TJsonReaderFactory::Create(JsonText); if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid()) { FString ServicePath = JsonObject->Values.FindRef(TEXT("ServicePath"))->AsString(); FString UtilityPath = JsonObject->Values.FindRef(TEXT("UtilityPath"))->AsString(); auto VersionObject = JsonObject->Values.FindRef(TEXT("Version"))->AsObject(); if (VersionObject) { uint32_t MajorVersion = static_cast(VersionObject->TryGetField(TEXT("Major"))->AsNumber()); uint32_t MinorVersion = static_cast(VersionObject->TryGetField(TEXT("Minor"))->AsNumber()); uint32_t PatchVersion = static_cast(VersionObject->TryGetField(TEXT("Patch"))->AsNumber()); FString Details = VersionObject->TryGetField(TEXT("Details"))->AsString(); return FZenServiceLink{ .ServicePath = ServicePath, .UtilityPath = UtilityPath, .Version = FZenVersion{ .MajorVersion = MajorVersion, .MinorVersion = MinorVersion, .PatchVersion = PatchVersion, .Details = Details} }; } } } return {}; } static bool Write(const FZenServiceLink& Link, const FString& Filename) { FString JsonTcharText; TSharedRef>> Writer = TJsonWriterFactory>::Create(&JsonTcharText); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("ServicePath"), Link.ServicePath); Writer->WriteValue(TEXT("UtilityPath"), Link.UtilityPath); Writer->WriteObjectStart(TEXT("Version")); Writer->WriteValue(TEXT("Major"), Link.Version.MajorVersion); Writer->WriteValue(TEXT("Minor"), Link.Version.MinorVersion); Writer->WriteValue(TEXT("Patch"), Link.Version.PatchVersion); Writer->WriteValue(TEXT("Details"), Link.Version.Details); Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); Writer->Close(); if (!FFileHelper::SaveStringToFile(JsonTcharText, *Filename)) { return false; } return true; } }; static FString GetLocalZenRootPath() { return FPaths::ConvertRelativePathToFull(FPaths::Combine(FPlatformProcess::UserSettingsDir(), *FApp::GetEpicProductIdentifier(), TEXT("Common")) + TEXT("/")); } static FString GetServiceExecutableName() { return #if PLATFORM_WINDOWS TEXT("zenserver.exe") #else TEXT("zenserver") #endif ; } static FString GetUtilityExecutableName() { return #if PLATFORM_WINDOWS TEXT("zen.exe") #else TEXT("zen") #endif ; } static FString GetLocalInstallPath() { return FPaths::ConvertRelativePathToFull(FPaths::Combine(GetLocalZenRootPath(), TEXT("Zen\\Install"))); } static FString GetServiceLinkPath() { return FPaths::Combine(GetLocalInstallPath(), TEXT("zen.link")); } static FString GetServicePluginsConfigPath() { const int PluginsConfigVersion = 1; return FPaths::Combine(GetLocalInstallPath(), FString::Printf(TEXT("zen_plugins_v%d.json"), PluginsConfigVersion)); } static void CleanOutOfDateServicePluginConfigs() { IFileManager::Get().IterateDirectory(*GetLocalInstallPath(), [](const TCHAR* Pathname, bool bIsDirectory) { if (!bIsDirectory) { FString Path = Pathname; FString FileName = FPaths::GetCleanFilename(Path); if (FileName.EndsWith(TEXT(".json")) && FileName.StartsWith("zen_plugins") && FileName != FPaths::GetCleanFilename(GetServicePluginsConfigPath())) { IFileManager::Get().Delete(Pathname, false /*RequireExists*/, false /*EvenReadOnly*/, true /*Quiet*/); } } return true; }); } static FString GetServiceCopyInstallPath() { return FPaths::ConvertRelativePathToFull(FPaths::Combine(GetLocalInstallPath(), GetServiceExecutableName())); } static FString GetUtilityCopyInstallPath() { return FPaths::ConvertRelativePathToFull(FPaths::Combine(GetLocalInstallPath(), GetUtilityExecutableName())); } static FString GetInstallVersionCachePath() { FString InstallUtilityPath = GetUtilityCopyInstallPath(); FString InstallVersionCache = FPaths::SetExtension(InstallUtilityPath, TEXT("version")); return InstallVersionCache; } static FString GetInTreeVersionCache() { return FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::EngineSavedDir(), TEXT("Zen"), TEXT("zen.version"))); } static FString GetServiceRunContextPath() { return FPaths::SetExtension(GetServiceCopyInstallPath(), TEXT(".runcontext")); } static FString GetInTreeUtilityPath() { return FPaths::ConvertRelativePathToFull(FPlatformProcess::GenerateApplicationPath(TEXT("zen"), EBuildConfiguration::Development)); } static FString GetInTreeServicePath() { return FPaths::ConvertRelativePathToFull(FPlatformProcess::GenerateApplicationPath(TEXT("zenserver"), EBuildConfiguration::Development)); } static FString GetInTreeCrashpadHandlerFilePath() { return FPaths::ConvertRelativePathToFull(FPlatformProcess::GenerateApplicationPath(TEXT("crashpad_handler"), EBuildConfiguration::Development)); } static FString GetInstallCrashpadHandlerFilePath(const FString& InTreePath) { return FPaths::ConvertRelativePathToFull(FPaths::Combine(GetLocalInstallPath(), FString(FPathViews::GetCleanFilename(InTreePath)))); } static bool GetZenVersion(const FString& UtilityPath, const FString& ServicePath, FZenVersion& OutVersion) { FString AbsoluteUtilityPath = FPaths::ConvertRelativePathToFull(UtilityPath); FMonitoredProcess MonitoredUtilityProcess(AbsoluteUtilityPath, TEXT("version --detailed"), FPaths::GetPath(UtilityPath), true); bool bLaunched = MonitoredUtilityProcess.Launch(); if (!bLaunched) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to launch zen utility to gather version data: '%s'."), *UtilityPath); return false; } const uint64 StartTime = FPlatformTime::Cycles64(); while (MonitoredUtilityProcess.Update()) { double Duration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StartTime); if (Duration > 10.0) { MonitoredUtilityProcess.Cancel(true); UE_LOG(LogZenServiceInstance, Warning, TEXT("Cancelled launch of zen utility for gathering version data: '%s' due to timeout."), *UtilityPath); // Wait for execution to be terminated while (MonitoredUtilityProcess.Update()) { Duration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StartTime); if (Duration > 15.0) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Cancelled launch of zen utility for gathering version data: '%s'. Failed waiting for termination."), *UtilityPath); break; } FPlatformProcess::Sleep(0.2f); } FString OutputString = MonitoredUtilityProcess.GetFullOutputWithoutDelegate(); UE_LOG(LogZenServiceInstance, Warning, TEXT("Launch of zen utility for gathering version data: '%s' failed. Output: '%s'"), *UtilityPath, *OutputString); return false; } FPlatformProcess::Sleep(0.1f); } FString OutputString = MonitoredUtilityProcess.GetFullOutputWithoutDelegate(); if (MonitoredUtilityProcess.GetReturnCode() != 0) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Unexpected return code after launch of zen utility for gathering version data: '%s' (%d). Output: '%s'"), *UtilityPath, MonitoredUtilityProcess.GetReturnCode(), *OutputString); return false; } FString VersionOutputString = OutputString.TrimStartAndEnd(); if (!OutVersion.TryParse(*VersionOutputString)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Invalid version information after launch of zen utility for gathering version data: '%s' (`%s`)"), *UtilityPath, *VersionOutputString); return false; } return true; } static FZenVersion GetZenVersion(const FString& UtilityPath, const FString& ServicePath, const FString& VersionCachePath) { IFileManager& FileManager = IFileManager::Get(); FDateTime UtilityExecutableModificationTime = FileManager.GetTimeStamp(*UtilityPath); FDateTime ServiceExecutableModificationTime = FileManager.GetTimeStamp(*ServicePath); FDateTime VersionCacheModificationTime = FileManager.GetTimeStamp(*VersionCachePath); bool VersionCacheIsOlderThanUtilityExecutable = VersionCacheModificationTime < UtilityExecutableModificationTime; bool VersionCacheIsOlderThanServerExecutable = VersionCacheModificationTime < ServiceExecutableModificationTime; bool VersionCacheIsUpToDate = (!VersionCacheIsOlderThanUtilityExecutable) && (!VersionCacheIsOlderThanServerExecutable); if (VersionCacheIsUpToDate) { FString VersionFileContents; if (FFileHelper::LoadFileToString(VersionFileContents, *VersionCachePath)) { FZenVersion CachedVersion; if (CachedVersion.TryParse(*VersionFileContents)) { return CachedVersion; } } } auto GetFallbackVersion = [UtilityExecutableModificationTime, ServiceExecutableModificationTime]() { FZenVersion FallbackVersion; if (UtilityExecutableModificationTime > ServiceExecutableModificationTime) { FallbackVersion.Details = UtilityExecutableModificationTime.ToString(); return FallbackVersion; } FallbackVersion.Details = ServiceExecutableModificationTime.ToString(); return FallbackVersion; }; FZenVersion Version; if (!GetZenVersion(UtilityPath, ServicePath, Version)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Unable to determine version using zen utility executable path: '%s'. Retrying..."), *UtilityPath); if (!GetZenVersion(UtilityPath, ServicePath, Version)) { checkf(false, TEXT("Unable to determine version using zen utility executable path: '%s'."), *UtilityPath); Version = GetFallbackVersion(); } } FFileHelper::SaveStringToFile(Version.ToString(), *VersionCachePath); return Version; } static void PromptUserToSyncInTreeVersion(const FString& ServerFilePath) { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FText ZenSyncSourcePromptTitle = NSLOCTEXT("Zen", "Zen_SyncSourcePromptTitle", "Failed to launch"); FText ZenSyncSourcePromptText = FText::Format(NSLOCTEXT("Zen", "Zen_SyncSourcePromptText", "Unreal Zen Storage Server can not verify installation. Please make sure your source installation in properly synced at '{0}'"), FText::FromString(FPaths::GetPath(ServerFilePath))); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenSyncSourcePromptText.ToString(), *ZenSyncSourcePromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Display, TEXT("Unreal Zen Storage Server can not verify installation. Please make sure your source installation in properly synced at '%s'"), *FPaths::GetPath(ServerFilePath)); } } static bool IsInstallVersionOutOfDate(const FString& InTreeUtilityPath, const FString& InstallUtilityPath, const FString& InTreeServicePath, const FString& InstallServicePath, const FString& InTreeVersionCache, const FString& InstallVersionCache) { IFileManager& FileManager = IFileManager::Get(); if (!FileManager.FileExists(*InTreeUtilityPath) || !FileManager.FileExists(*InTreeServicePath)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("InTree version at '%s' is invalid"), *InTreeServicePath); PromptUserToSyncInTreeVersion(InTreeServicePath); return false; } // Always get the InTree utility path so cached version information is up to date FZenVersion InTreeVersion = GetZenVersion(InTreeUtilityPath, InTreeServicePath, InTreeVersionCache); UE_LOG(LogZenServiceInstance, Log, TEXT("InTree version at '%s' is '%s'"), *InTreeServicePath, *InTreeVersion.ToString()); if (!FileManager.FileExists(*InstallUtilityPath) || !FileManager.FileExists(*InstallServicePath)) { UE_LOG(LogZenServiceInstance, Log, TEXT("No installation found at '%s'"), *InstallServicePath); return true; } FZenVersion InstallVersion = GetZenVersion(InstallUtilityPath, InstallServicePath, InstallVersionCache); UE_LOG(LogZenServiceInstance, Log, TEXT("Installed version at '%s' is '%s'"), *InstallServicePath, *InstallVersion.ToString()); if (InstallVersion < InTreeVersion) { UE_LOG(LogZenServiceInstance, Log, TEXT("Installed version at '%s' (%s) is older than '%s' (%s)"), *InstallServicePath, *InstallVersion.ToString(), *InTreeServicePath, *InTreeVersion.ToString()); return true; } if (FParse::Param(FCommandLine::Get(), TEXT("ForceZenInstall"))) { UE_LOG(LogZenServiceInstance, Display, TEXT("Forcing install from '%s' (%s) over '%s' (%s)"), *InTreeServicePath, *InTreeVersion.ToString(), *InstallServicePath, *InstallVersion.ToString()); return true; } return false; } static bool AttemptFileCopyWithRetries(const TCHAR* Dst, const TCHAR* Src, double RetryDurationSeconds) { IFileManager& FileManager = IFileManager::Get(); uint32 CopyResult = FileManager.Copy(Dst, Src, true, true, false); uint64 CopyWaitStartTime = FPlatformTime::Cycles64(); while (CopyResult != COPY_OK) { double CopyWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - CopyWaitStartTime); if (CopyWaitDuration < RetryDurationSeconds) { FPlatformProcess::Sleep(0.01f); } else { break; } CopyResult = FileManager.Copy(Dst, Src, true, true, false); } if (CopyResult == COPY_OK) { return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("copy from '%s' to '%s', '%s'"), Src, Dst, CopyResult == COPY_Fail ? TEXT("Failed to copy file") : TEXT("Cancelled file copy")); return false; } static bool AttemptFileDeleteWithRetries(const TCHAR* Path, double RetryDurationSeconds) { IFileManager& FileManager = IFileManager::Get(); bool DeleteResult = FileManager.Delete(Path, false, false, true); uint64 DeleteWaitStartTime = FPlatformTime::Cycles64(); while (!DeleteResult) { double DeleteWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DeleteWaitStartTime); if (DeleteWaitDuration < RetryDurationSeconds) { FPlatformProcess::Sleep(0.01f); } else { break; } DeleteResult = FileManager.Delete(Path, false, false, true); } if (DeleteResult) { return true; } return false; } static void EnsureEditorSettingsConfigLoaded() { #if !WITH_EDITOR if (GEditorSettingsIni.IsEmpty()) { FConfigContext Context = FConfigContext::ReadIntoGConfig(); Context.GeneratedConfigDir = FPaths::EngineEditorSettingsDir(); Context.Load(TEXT("EditorSettings"), GEditorSettingsIni); } #endif } static void DetermineLocalDataCachePath(const TCHAR* ConfigSection, FString& DataPath) { FString DataPathEnvOverride; if (GConfig->GetString(ConfigSection, TEXT("LocalDataCachePathEnvOverride"), DataPathEnvOverride, GEngineIni)) { FString DataPathEnvOverrideValue = FPlatformMisc::GetEnvironmentVariable(*DataPathEnvOverride); if (!DataPathEnvOverrideValue.IsEmpty()) { DataPath = DataPathEnvOverrideValue; UE_LOG(LogZenServiceInstance, Log, TEXT("Found environment variable %s=%s"), *DataPathEnvOverride, *DataPathEnvOverrideValue); } if (FPlatformMisc::GetStoredValue(TEXT("Epic Games"), TEXT("GlobalDataCachePath"), *DataPathEnvOverride, DataPathEnvOverrideValue)) { if (!DataPathEnvOverrideValue.IsEmpty()) { DataPath = DataPathEnvOverrideValue; UE_LOG(LogZenServiceInstance, Log, TEXT("Found registry key GlobalDataCachePath %s=%s"), *DataPathEnvOverride, *DataPath); } } } FString DataPathCommandLineOverride; if (GConfig->GetString(ConfigSection, TEXT("LocalDataCachePathCommandLineOverride"), DataPathCommandLineOverride, GEngineIni)) { FString DataPathCommandLineOverrideValue; if (FParse::Value(FCommandLine::Get(), *(DataPathCommandLineOverride + TEXT("=")), DataPathCommandLineOverrideValue)) { DataPath = DataPathCommandLineOverrideValue; UE_LOG(LogZenServiceInstance, Log, TEXT("Found command line override %s=%s"), *DataPathCommandLineOverride, *DataPath); } } // Paths starting with a '?' are looked up from config if (DataPath.StartsWith(TEXT("?")) && !GConfig->GetString(TEXT("DerivedDataCacheSettings"), *DataPath + 1, DataPath, GEngineIni)) { DataPath.Empty(); } FString DataPathEditorOverrideSetting; if (GConfig->GetString(ConfigSection, TEXT("LocalDataCachePathEditorOverrideSetting"), DataPathEditorOverrideSetting, GEngineIni)) { EnsureEditorSettingsConfigLoaded(); FString Setting = GConfig->GetStr(TEXT("/Script/UnrealEd.EditorSettings"), *DataPathEditorOverrideSetting, GEditorSettingsIni); if (!Setting.IsEmpty()) { FString SettingPath; if (FParse::Value(*Setting, TEXT("Path="), SettingPath)) { SettingPath.TrimQuotesInline(); SettingPath.ReplaceEscapedCharWithCharInline(); if (!SettingPath.IsEmpty()) { DataPath = SettingPath; UE_LOG(LogZenServiceInstance, Log, TEXT("Found editor setting /Script/UnrealEd.EditorSettings.Path=%s"), *DataPath); } } } } } static bool DetermineDataPath(const TCHAR* ConfigSection, FString& DataPath, bool& bHasInvalidPathConfigurations, bool& bIsDefaultDataPath) { auto ValidateDataPath = [](const FString& InDataPath) { if (InDataPath.IsEmpty()) { return FString{}; } IFileManager& FileManager = IFileManager::Get(); FString FinalPath = FPaths::ConvertRelativePathToFull(InDataPath); FPaths::NormalizeDirectoryName(FinalPath); FFileStatData StatData = FileManager.GetStatData(*InDataPath); if (StatData.bIsValid && StatData.bIsDirectory) { FString TestFilePath = FinalPath / FString::Printf(TEXT(".zen-startup-test-file-%d"), FPlatformProcess::GetCurrentProcessId()); FArchive* TestFile = FileManager.CreateFileWriter(*TestFilePath, FILEWRITE_Silent); if (!TestFile) { return FString{}; } TestFile->Close(); delete TestFile; FileManager.Delete(*TestFilePath); return FinalPath; } if (FileManager.MakeDirectory(*InDataPath, true)) { return FinalPath; } return FString{}; }; // Zen commandline FString CommandLineOverrideValue; if (FParse::Value(FCommandLine::Get(), TEXT("ZenDataPath="), CommandLineOverrideValue) && !CommandLineOverrideValue.IsEmpty()) { if (FString Path = ValidateDataPath(CommandLineOverrideValue); !Path.IsEmpty()) { DataPath = Path; UE_LOG(LogZenServiceInstance, Log, TEXT("Found command line override ZenDataPath=%s"), *CommandLineOverrideValue); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Skipping command line override ZenDataPath=%s due to an invalid path"), *CommandLineOverrideValue); bHasInvalidPathConfigurations = true; } // Zen subprocess environment if (FString SubprocessDataPathEnvOverrideValue = FPlatformMisc::GetEnvironmentVariable(TEXT("UE-ZenSubprocessDataPath")); !SubprocessDataPathEnvOverrideValue.IsEmpty()) { if (FString Path = ValidateDataPath(SubprocessDataPathEnvOverrideValue); !Path.IsEmpty()) { DataPath = Path; UE_LOG(LogZenServiceInstance, Log, TEXT("Found subprocess environment variable UE-ZenSubprocessDataPath=%s"), *SubprocessDataPathEnvOverrideValue); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Skipping subprocess environment variable UE-ZenSubprocessDataPath=%s due to an invalid path"), *SubprocessDataPathEnvOverrideValue); bHasInvalidPathConfigurations = true; } // Zen registry/stored FString DataPathEnvOverrideValue; if (FPlatformMisc::GetStoredValue(TEXT("Epic Games"), TEXT("Zen"), TEXT("DataPath"), DataPathEnvOverrideValue) && !DataPathEnvOverrideValue.IsEmpty()) { if (FString Path = ValidateDataPath(DataPathEnvOverrideValue); !Path.IsEmpty()) { DataPath = Path; UE_LOG(LogZenServiceInstance, Log, TEXT("Found registry key Zen DataPath=%s"), *DataPathEnvOverrideValue); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Skipping registry key Zen DataPath=%s due to an invalid path"), *DataPathEnvOverrideValue); bHasInvalidPathConfigurations = true; } // Zen environment if (FString ZenDataPathEnvOverrideValue = FPlatformMisc::GetEnvironmentVariable(TEXT("UE-ZenDataPath")); !ZenDataPathEnvOverrideValue.IsEmpty()) { if (FString Path = ValidateDataPath(ZenDataPathEnvOverrideValue); !Path.IsEmpty()) { DataPath = Path; UE_LOG(LogZenServiceInstance, Log, TEXT("Found environment variable UE-ZenDataPath=%s"), *ZenDataPathEnvOverrideValue); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Skipping environment variable UE-ZenDataPath=%s due to an invalid path"), *ZenDataPathEnvOverrideValue); bHasInvalidPathConfigurations = true; } // Follow local DDC (if outside workspace) FString LocalDataCachePath; DetermineLocalDataCachePath(ConfigSection, LocalDataCachePath); if (!LocalDataCachePath.IsEmpty() && (LocalDataCachePath != TEXT("None")) && !FPaths::IsUnderDirectory(LocalDataCachePath, FPaths::RootDir())) { FString ZenLocalDataCachePath = FPaths::Combine(LocalDataCachePath, TEXT("Zen")); if (FString Path = ValidateDataPath(ZenLocalDataCachePath); !Path.IsEmpty()) { DataPath = Path; UE_LOG(LogZenServiceInstance, Log, TEXT("Found local data cache path=%s"), *LocalDataCachePath); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Skipping local data cache path=%s due to an invalid path"), *LocalDataCachePath); bHasInvalidPathConfigurations = true; } // Zen config default FString ConfigDefaultPath; GConfig->GetString(ConfigSection, TEXT("DataPath"), ConfigDefaultPath, GEngineIni); if (!ConfigDefaultPath.IsEmpty()) { ConfigDefaultPath.ReplaceInline(TEXT("%ENGINEVERSIONAGNOSTICINSTALLEDUSERDIR%"), *GetLocalZenRootPath()); if (FString Path = ValidateDataPath(ConfigDefaultPath); !Path.IsEmpty()) { DataPath = Path; bIsDefaultDataPath = true; UE_LOG(LogZenServiceInstance, Log, TEXT("Found Zen config default=%s"), *ConfigDefaultPath); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Skipping Zen config default=%s due to an invalid path"), *ConfigDefaultPath); bHasInvalidPathConfigurations = true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Unable to determine a valid Zen data path")); return false; } static void ReadUInt16FromConfig(const TCHAR* Section, const TCHAR* Key, uint16& Value, const FString& ConfigFile) { int32 ValueInt32 = Value; GConfig->GetInt(Section, Key, ValueInt32, ConfigFile); Value = (uint16)ValueInt32; } static bool IsLocalHost(const FString& Host) { if (Host.Compare(FString(TEXT("localhost")), ESearchCase::IgnoreCase) == 0) { return true; } if (Host.Compare(FString(TEXT("127.0.0.1"))) == 0) { return true; } if (Host.Compare(FString(TEXT("[::1]"))) == 0) { return true; } return false; } static void ApplyProcessLifetimeOverride(bool& bLimitProcessLifetime) { FString LimitProcessLifetime = FPlatformMisc::GetEnvironmentVariable(TEXT("UE-ZenLimitProcessLifetime")); if (!LimitProcessLifetime.IsEmpty()) { bLimitProcessLifetime = LimitProcessLifetime.ToBool(); } } static void PromptUserUnableToDetermineValidDataPath() { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FString LogDirPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()); FText ZenInvalidDataPathPromptTitle = NSLOCTEXT("Zen", "Zen_InvalidDataPathPromptTitle", "No Valid Data Path Configuration"); FText ZenInvalidDataPathPromptText = FText::Format(NSLOCTEXT("Zen", "Zen_InvalidDataPathPromptText", "Unreal Zen Storage Server can not determine a valid data path.\nPlease check the log in '{0}' for details.\nUpdate your configuration and restart."), FText::FromString(LogDirPath)); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenInvalidDataPathPromptText.ToString(), *ZenInvalidDataPathPromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Warning, TEXT("Unreal Zen Storage Server is unable to determine a valid data path")); } } static void PromptUserAboutInvalidValidDataPathConfiguration(const FString& UsedDataPath) { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FString LogDirPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()); FText ZenInvalidValidDataPathConfigurationPromptTitle = NSLOCTEXT("Zen", "Zen_InvalidValidDataPathConfigurationPromptTitle", "Invalid Data Paths"); FText ZenInvalidValidDataPathConfigurationPromptText = FText::Format(NSLOCTEXT("Zen", "Zen_InvalidValidDataPathConfigurationPromptText", "Unreal Zen Storage Server has detected invalid data path configuration.\nPlease check the log in '{0}' for details.\n\nFalling back to using '{1}' as data path."), FText::FromString(LogDirPath), FText::FromString(UsedDataPath)); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenInvalidValidDataPathConfigurationPromptText.ToString(), *ZenInvalidValidDataPathConfigurationPromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Warning, TEXT("Unreal Zen Storage Server has detected invalid data path configuration. Falling back to '%s'"), *UsedDataPath); } } #if PLATFORM_WINDOWS static void PromptUserIsUsingGoogleDriveAsDataPath() { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FString LogDirPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()); FText ZenInvalidDataPathPromptTitle = NSLOCTEXT("Zen", "Zen_GoogleDriveDataPathPromptTitle", "Using Google Drive as a data path"); FText ZenInvalidDataPathPromptText = FText::Format(NSLOCTEXT("Zen", "Zen_GoogleDriveDataPathPromptText", "Unreal Zen Storage Server is configured to use Google Drive as a data path, this is highly inadvisable.\nPlease use a data path on a local physical drive.\nCheck the log in '{0}' for details.\nUpdate your configuration and restart."), FText::FromString(LogDirPath)); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenInvalidDataPathPromptText.ToString(), *ZenInvalidDataPathPromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Warning, TEXT("Unreal Zen Storage Server is configured to use Google Drive as a data path, this is highly inadvisable. Please use a path on a local physical drive.")); } } #endif // PLATFORM_WINDOWS static void ReadCbField(FCbFieldView Field, UE::Zen::FZenSizeStats& OutValue) { FCbObjectView ObjectView = Field.AsObjectView(); OutValue.Disk = ObjectView["disk"].AsDouble(); OutValue.Memory = ObjectView["memory"].AsDouble(); } static void ReadCbField(FCbFieldView Field, UE::Zen::FZenCIDSizeStats& OutValue) { FCbObjectView ObjectView = Field.AsObjectView(); OutValue.Tiny = ObjectView["tiny"].AsInt64(); OutValue.Small = ObjectView["small"].AsInt64(); OutValue.Large = ObjectView["large"].AsInt64(); OutValue.Total = ObjectView["total"].AsInt64(); } static void ReadCbField(FCbFieldView Field, UE::Zen::FZenCIDStats& OutValue) { FCbObjectView ObjectView = Field.AsObjectView(); ReadCbField(ObjectView["size"], OutValue.Size); } static FServiceAutoLaunchSettings::EInstallMode ZenGetInstallModeFromString(const FString& InstallMode) { if (InstallMode.IsEmpty() || FCString::Stricmp(*InstallMode, TEXT("auto")) == 0) { return FApp::IsEngineInstalled() ? FServiceAutoLaunchSettings::EInstallMode::Link : FServiceAutoLaunchSettings::EInstallMode::Copy; } else if (FCString::Stricmp(*InstallMode, TEXT("copy")) == 0) { return FServiceAutoLaunchSettings::EInstallMode::Copy; } else if (FCString::Stricmp(*InstallMode, TEXT("link")) == 0) { return FServiceAutoLaunchSettings::EInstallMode::Link; } else { checkf(false, TEXT("Invalid zenserver install mode: {InstallMode}"), *InstallMode); return FServiceAutoLaunchSettings::EInstallMode::Copy; } } static FString ZenGetInstallModeToString(FServiceAutoLaunchSettings::EInstallMode InstallMode) { switch (InstallMode) { case FServiceAutoLaunchSettings::EInstallMode::Copy: return TEXT("copy"); case FServiceAutoLaunchSettings::EInstallMode::Link: return TEXT("link"); } checkf(false, TEXT("Invalid zenserver install mode: {%d}"), static_cast(InstallMode)); return ""; } bool FServicePluginSettings::ReadFromConfig(const FString& InPluginName) { const FString PluginSectionName = FString::Format(TEXT("Zen.Plugin.{0}"), {*InPluginName}); const FConfigSection* PluginSection = GConfig->GetSection(*PluginSectionName, false, GEngineIni); if (!PluginSection) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Unable to find config section '%s'"), *PluginSectionName); return false; } bool bHasName = false; for (FConfigSection::TConstIterator It(*PluginSection); It; ++It) { if (It.Key() == TEXT("Name")) { Name = It.Value().GetValue(); // TODO load plugins from project dir FString AbsPathRoot = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::RootDir(), *Name)); if (IFileManager::Get().FileExists(*AbsPathRoot)) { AbsPath = AbsPathRoot; } else { UE_LOG(LogZenServiceInstance, Warning, TEXT("Can't find file for plugin '%s', tried '%s'"), *Name, *AbsPathRoot); return false; } bHasName = true; } else { Options.Add(It.Key(), It.Value().GetValue()); } } if (!bHasName) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Settings for plugin defined in section '%s' are missing 'Name' value"), *PluginSectionName); } return bHasName; } bool FServicePluginSettings::ReadFromCompactBinary(FCbFieldView Field) { if (!Field.IsObject()) { return false; } bool bValid = true; Name = FString(Field["Name"].AsString()); bValid &= !Name.IsEmpty(); AbsPath = FString(Field["AbsPath"].AsString()); bValid &= !AbsPath.IsEmpty(); if (FCbObjectView OptionsObject = Field["Options"].AsObjectView()) { for (FCbFieldViewIterator It = OptionsObject.CreateViewIterator(); It;) { if (!It->IsString()) { bValid = false; break; } FString OptionName = FString(It->AsString()); ++It; if (!It->IsString()) { bValid = false; break; } FString OptionValue = FString(It->AsString()); Options.Add(FName(OptionName), OptionValue); } } return bValid; } void FServicePluginSettings::WriteToCompactBinary(FCbWriter& Writer) const { Writer.BeginObject(); Writer << "Name" << Name; Writer << "AbsPath" << AbsPath; if (!Options.IsEmpty()) { Writer.BeginObject("Options"); for (const TPair& Option: Options) { Writer << Option.Key << Option.Value; } Writer.EndObject(); } Writer.EndObject(); } bool FServiceSettings::ReadFromConfig() { check(GConfig && GConfig->IsReadyForUse()); const TCHAR* ConfigSection = TEXT("Zen"); bool bAutoLaunch = true; GConfig->GetBool(ConfigSection, TEXT("AutoLaunch"), bAutoLaunch, GEngineIni); if (bAutoLaunch) { if (!TryApplyAutoLaunchOverride()) { // AutoLaunch settings const TCHAR* AutoLaunchConfigSection = TEXT("Zen.AutoLaunch"); SettingsVariant.Emplace(); FServiceAutoLaunchSettings& AutoLaunchSettings = SettingsVariant.Get(); bool bHasInvalidPathConfigurations = false; if (!DetermineDataPath(AutoLaunchConfigSection, AutoLaunchSettings.DataPath, bHasInvalidPathConfigurations, AutoLaunchSettings.bIsDefaultDataPath)) { PromptUserUnableToDetermineValidDataPath(); return false; } else if (bHasInvalidPathConfigurations) { PromptUserAboutInvalidValidDataPathConfiguration(AutoLaunchSettings.DataPath); } #if PLATFORM_WINDOWS { int32 DriveEnd = 0; if (AutoLaunchSettings.DataPath.FindChar(':', DriveEnd)) { FString DrivePath = AutoLaunchSettings.DataPath.Left(DriveEnd + 1); TCHAR VolumeName[128]; BOOL OK = GetVolumeInformation( *DrivePath, VolumeName, 127, NULL, NULL, NULL, NULL, NULL); if (OK) { VolumeName[127] = 0; if (FString(VolumeName) == TEXT("Google Drive")) { PromptUserIsUsingGoogleDriveAsDataPath(); } } } } #endif // PLATFORM_WINDOWS GConfig->GetString(AutoLaunchConfigSection, TEXT("ExtraArgs"), AutoLaunchSettings.ExtraArgs, GEngineIni); ReadUInt16FromConfig(AutoLaunchConfigSection, TEXT("DesiredPort"), AutoLaunchSettings.DesiredPort, GEngineIni); AutoLaunchSettings.Plugins.Empty(); TArray PluginNames; GConfig->GetArray(AutoLaunchConfigSection, TEXT("Plugins"), PluginNames, GEngineIni); for (FString PluginName : PluginNames) { FServicePluginSettings PluginSettings = {}; if (PluginSettings.ReadFromConfig(PluginName)) { AutoLaunchSettings.Plugins.Add(PluginSettings); } } GConfig->GetBool(AutoLaunchConfigSection, TEXT("ShowConsole"), AutoLaunchSettings.bShowConsole, GEngineIni); GConfig->GetBool(AutoLaunchConfigSection, TEXT("LimitProcessLifetime"), AutoLaunchSettings.bLimitProcessLifetime, GEngineIni); ApplyProcessLifetimeOverride(AutoLaunchSettings.bLimitProcessLifetime); GConfig->GetBool(AutoLaunchConfigSection, TEXT("AllowRemoteNetworkService"), AutoLaunchSettings.bAllowRemoteNetworkService, GEngineIni); FString InstallMode; if (GConfig->GetString(AutoLaunchConfigSection, TEXT("InstallMode"), InstallMode, GEngineIni)) { AutoLaunchSettings.InstallMode = ZenGetInstallModeFromString(InstallMode); } EnsureEditorSettingsConfigLoaded(); GConfig->GetBool(TEXT("/Script/UnrealEd.CrashReportsPrivacySettings"), TEXT("bSendUnattendedBugReports"), AutoLaunchSettings.bSendUnattendedBugReports, GEditorSettingsIni); } } else { // ConnectExisting settings const TCHAR* ConnectExistingConfigSection = TEXT("Zen.ConnectExisting"); SettingsVariant.Emplace(); FServiceConnectSettings& ConnectExistingSettings = SettingsVariant.Get(); GConfig->GetString(ConnectExistingConfigSection, TEXT("HostName"), ConnectExistingSettings.HostName, GEngineIni); ReadUInt16FromConfig(ConnectExistingConfigSection, TEXT("Port"), ConnectExistingSettings.Port, GEngineIni); } return true; } bool FServiceSettings::ReadFromCompactBinary(FCbFieldView Field) { if (Field.IsObject()) { if (bool bAutoLaunchValue = Field["bAutoLaunch"].AsBool()) { if (!TryApplyAutoLaunchOverride()) { SettingsVariant.Emplace(); FServiceAutoLaunchSettings& AutoLaunchSettings = SettingsVariant.Get(); if (FCbObjectView AutoLaunchSettingsObject = Field["AutoLaunchSettings"].AsObjectView()) { AutoLaunchSettings.DataPath = FString(AutoLaunchSettingsObject["DataPath"].AsString()); AutoLaunchSettings.ExtraArgs = FString(AutoLaunchSettingsObject["ExtraArgs"].AsString()); AutoLaunchSettings.DesiredPort = AutoLaunchSettingsObject["DesiredPort"].AsInt16(); if (FCbArrayView PluginsView = AutoLaunchSettingsObject["Plugins"].AsArrayView()) { AutoLaunchSettings.Plugins.Empty(); for (FCbFieldView& PluginView : PluginsView) { FServicePluginSettings PluginSettings = {}; if (PluginSettings.ReadFromCompactBinary(PluginView)) { AutoLaunchSettings.Plugins.Add(PluginSettings); } } } AutoLaunchSettings.bShowConsole = AutoLaunchSettingsObject["ShowConsole"].AsBool(); AutoLaunchSettings.bIsDefaultDataPath = AutoLaunchSettingsObject["IsDefaultDataPath"].AsBool(); AutoLaunchSettings.bLimitProcessLifetime = AutoLaunchSettingsObject["LimitProcessLifetime"].AsBool(); ApplyProcessLifetimeOverride(AutoLaunchSettings.bLimitProcessLifetime); AutoLaunchSettings.bAllowRemoteNetworkService = AutoLaunchSettingsObject["AllowRemoteNetworkService"].AsBool(); AutoLaunchSettings.bSendUnattendedBugReports = AutoLaunchSettingsObject["SendUnattendedBugReports"].AsBool(); AutoLaunchSettings.bIsDefaultSharedRunContext = AutoLaunchSettingsObject["IsDefaultSharedRunContext"].AsBool(AutoLaunchSettings.bIsDefaultSharedRunContext); AutoLaunchSettings.InstallMode = ZenGetInstallModeFromString(FString(AutoLaunchSettingsObject["InstallMode"].AsString())); } } } else { SettingsVariant.Emplace(); FServiceConnectSettings& ConnectExistingSettings = SettingsVariant.Get(); if (FCbObjectView ConnectExistingSettingsObject = Field["ConnectExistingSettings"].AsObjectView()) { ConnectExistingSettings.HostName = FString(ConnectExistingSettingsObject["HostName"].AsString()); ConnectExistingSettings.Port = ConnectExistingSettingsObject["Port"].AsInt16(); } } return true; } return false; } bool FServiceSettings::ReadFromURL(FStringView InstanceURL) { SettingsVariant.Emplace(); FServiceConnectSettings& ConnectExistingSettings = SettingsVariant.Get(); if (InstanceURL.StartsWith(TEXT("http://"))) { InstanceURL.RightChopInline(7); } // Is the given URL perhaps a Unix domain socket path? if (InstanceURL.EndsWith(TEXT(".sock"))) { ConnectExistingSettings.HostName = InstanceURL; ConnectExistingSettings.Port = 0; return true; } int32 PortDelimIndex = INDEX_NONE; InstanceURL.FindLastChar(TEXT(':'), PortDelimIndex); if (PortDelimIndex != INDEX_NONE) { ConnectExistingSettings.HostName = InstanceURL.Left(PortDelimIndex); LexFromString(ConnectExistingSettings.Port, InstanceURL.RightChop(PortDelimIndex + 1)); } else { ConnectExistingSettings.HostName = InstanceURL; ConnectExistingSettings.Port = 8558; } return true; } void FServiceSettings::WriteToCompactBinary(FCbWriter& Writer) const { bool bAutoLaunch = IsAutoLaunch(); Writer << "bAutoLaunch" << bAutoLaunch; if (bAutoLaunch) { const FServiceAutoLaunchSettings& AutoLaunchSettings = SettingsVariant.Get(); Writer.BeginObject("AutoLaunchSettings"); Writer << "DataPath" << AutoLaunchSettings.DataPath; Writer << "ExtraArgs" <(); Writer.BeginObject("ConnectExistingSettings"); Writer << "HostName" << ConnectExistingSettings.HostName; Writer << "Port" << ConnectExistingSettings.Port; Writer.EndObject(); } } bool FServiceSettings::TryApplyAutoLaunchOverride() { #if ALLOW_SETTINGS_OVERRIDE_FROM_COMMANDLINE if (FParse::Param(FCommandLine::Get(), TEXT("NoZenAutoLaunch"))) { SettingsVariant.Emplace(); FServiceConnectSettings& ConnectExistingSettings = SettingsVariant.Get(); ConnectExistingSettings.HostName = TEXT("[::1]"); ConnectExistingSettings.Port = 8558; return true; } FString Host; if (FParse::Value(FCommandLine::Get(), TEXT("-NoZenAutoLaunch="), Host)) { SettingsVariant.Emplace(); FServiceConnectSettings& ConnectExistingSettings = SettingsVariant.Get(); int32 PortDelimIndex = INDEX_NONE; if (Host.FindChar(TEXT(':'), PortDelimIndex)) { ConnectExistingSettings.HostName = Host.Left(PortDelimIndex); LexFromString(ConnectExistingSettings.Port, Host.RightChop(PortDelimIndex + 1)); } else { ConnectExistingSettings.HostName = Host; ConnectExistingSettings.Port = 8558; } return true; } #endif return false; } #if UE_WITH_ZEN uint16 FZenServiceInstance::AutoLaunchedPort = 0; uint32 FZenServiceInstance::AutoLaunchedPid = 0; static bool IsZenProcessUsingEffectivePort(uint16 EffectiveListenPort) { ZenSharedEvent ShutDownEvent(ZenSharedEvent::GetShutdownEventName(EffectiveListenPort)); return ShutDownEvent.Exists(); } static bool RequestZenShutdownOnEffectivePort(uint16 EffectiveListenPort) { ZenSharedEvent ShutDownEvent(ZenSharedEvent::GetShutdownEventName(EffectiveListenPort)); if (!ShutDownEvent.Open()) { return false; } if (!ShutDownEvent.Set()) { return false; } return true; } static bool ShutdownZenServerProcess(int Pid, double MaximumWaitDurationSeconds = 25.0) { const ZenServerState ServerState(/* ReadOnly */true); const ZenServerState::ZenServerEntry* Entry = ServerState.LookupByPid(Pid); if (Entry) { uint16 EffectivePort = Entry->EffectiveListenPort.load(std::memory_order_relaxed); UE_LOG(LogZenServiceInstance, Display, TEXT("Requesting shut down of zenserver process %d runnning on effective port %u"), Pid, EffectivePort); if (RequestZenShutdownOnEffectivePort(EffectivePort)) { uint64 ZenShutdownWaitStartTime = FPlatformTime::Cycles64(); while (ZenServerState::IsProcessRunning(Pid)) { double ZenShutdownWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenShutdownWaitStartTime); if (ZenShutdownWaitDuration < MaximumWaitDurationSeconds) { FPlatformProcess::Sleep(0.01f); } else { UE_LOG(LogZenServiceInstance, Warning, TEXT("Timed out waiting for shut down of running service with pid %d"), Pid); break; } } } } if (ZenServerState::IsProcessRunning(Pid)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Attempting termination of zenserver process with pid %d"), Pid); if (!ZenServerState::Terminate(Pid)) { if (ZenServerState::IsProcessRunning(Pid)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to terminate zenserver process with pid %d"), Pid); return false; } } } UE_LOG(LogZenServiceInstance, Display, TEXT("Successfully shut down zenserver process with pid %d"), Pid); return true; } static bool ShutDownZenServerProcessExecutable(const FString& ExecutablePath, double MaximumWaitDurationSeconds = 25.0) { uint64 ZenShutdownWaitStartTime = FPlatformTime::Cycles64(); uint32_t Pid = 0; while (ZenServerState::FindRunningProcessId(*ExecutablePath, &Pid)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Attempting to shut down of zenserver executable '%s' process with pid %d"), *ExecutablePath, Pid); double ZenShutdownWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenShutdownWaitStartTime); if (ShutdownZenServerProcess(Pid, MaximumWaitDurationSeconds - ZenShutdownWaitDuration)) { return true; } else { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to shut down zenserver executable '%s' process with pid %d"), *ExecutablePath, Pid); return false; } } return true; } static bool ShutDownZenServerProcessLockingDataDir(const FString& DataPath, double MaximumWaitDurationSeconds = 25.0) { const FString LockFilePath = FPaths::Combine(DataPath, TEXT(".lock")); uint64 ZenShutdownWaitStartTime = FPlatformTime::Cycles64(); if (!ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Lock file '%s' is not active, nothing to do"), *LockFilePath); return true; } ZenLockFileData LockFileState = ZenLockFileData::ReadCbLockFile(*LockFilePath); if (!LockFileState.IsValid) { while (true) { if (!ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { return true; } uint32_t Pid = 0; if (!ZenServerState::FindRunningProcessId(*GetServiceExecutableName(), &Pid)) { if (!ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Lock file '%s' is no longer active, nothing to do"), *LockFilePath); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to find zenserver process locking file '%s'"), *LockFilePath); return false; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Found locked but invalid lock file at '%s', attempting shut down of zenserver process with pid %d"), *LockFilePath, Pid); double ZenShutdownWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenShutdownWaitStartTime); if (!ShutdownZenServerProcess(Pid, MaximumWaitDurationSeconds - ZenShutdownWaitDuration)) { break; } } if (!ZenLockFileData::IsLockFileLocked(*LockFilePath)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Successfully shut down zenserver using lock file '%s'"), *LockFilePath); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to shut down zenserver process locking file '%s'"), *LockFilePath); return false; } uint16 EffectivePort = LockFileState.EffectivePort; const ZenServerState ServerState(/* ReadOnly */true); const ZenServerState::ZenServerEntry* Entry = ServerState.LookupByEffectiveListenPort(EffectivePort); if (Entry) { UE_LOG(LogZenServiceInstance, Display, TEXT("Requesting shut down of zenserver process using lock file '%s' with effective port %d"), *LockFilePath, EffectivePort); if (RequestZenShutdownOnEffectivePort(EffectivePort)) { while (ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { double ZenShutdownWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenShutdownWaitStartTime); if (ZenShutdownWaitDuration < MaximumWaitDurationSeconds) { FPlatformProcess::Sleep(0.01f); } else { UE_LOG(LogZenServiceInstance, Warning, TEXT("Timed out waiting for shut down of zensever process using lock file '%s' with effective port %u"), *LockFilePath, EffectivePort); break; } } if (!ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Successfully shut down zenserver process using lock file '%s' with effective port %u"), *LockFilePath, EffectivePort); return true; } } } while (true) { if (!ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { return true; } uint32_t Pid = 0; if (!ZenServerState::FindRunningProcessId(LockFileState.ExecutablePath.IsEmpty() ? *GetServiceExecutableName() : *LockFileState.ExecutablePath, &Pid)) { if (!ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Lock file '%s' is no longer active, nothing to do"), *LockFilePath); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to find zenserver process locking file '%s'"), *LockFilePath); return false; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Found locked but invalid lock file at '%s', attempting shut down of zenserver process with pid %d"), *LockFilePath, Pid); double ZenShutdownWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenShutdownWaitStartTime); if (!ShutdownZenServerProcess(Pid, MaximumWaitDurationSeconds - ZenShutdownWaitDuration)) { break; } } if (!ZenLockFileData::IsLockFileLocked(*LockFilePath)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Successfully shut down zenserver using lock file '%s'"), *LockFilePath); return true; } UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to shut down zenserver process locking file '%s'"), *LockFilePath); return false; } static bool IsZenProcessUsingDataDir(const TCHAR* LockFilePath, ZenLockFileData* OutLockFileData) { if (ZenLockFileData::IsLockFileLocked(LockFilePath, true)) { if (OutLockFileData) { // If an instance is running with this data path, check if we can use it and what port it is on *OutLockFileData = ZenLockFileData::ReadCbLockFile(LockFilePath); } return true; } return false; } static FString DetermineCmdLineWithoutTransientComponents(const FServiceAutoLaunchSettings& InSettings, int16 OverrideDesiredPort) { FString PlatformDataPath(InSettings.DataPath); FPaths::MakePlatformFilename(PlatformDataPath); FString Parms; Parms.Appendf(TEXT("--port %d --data-dir \"%s\""), OverrideDesiredPort, *PlatformDataPath); if (!InSettings.ExtraArgs.IsEmpty()) { Parms.AppendChar(TEXT(' ')); Parms.Append(InSettings.ExtraArgs); } if (!InSettings.Plugins.IsEmpty()) { Parms.AppendChar(TEXT(' ')); Parms.Appendf(TEXT("--plugins-config \"%s\""), *GetServicePluginsConfigPath()); } FString LogCommandLineOverrideValue; if (FParse::Value(FCommandLine::Get(), TEXT("ZenLogPath="), LogCommandLineOverrideValue)) { if (!LogCommandLineOverrideValue.IsEmpty()) { Parms.Appendf(TEXT(" --abslog \"%s\""), *FPaths::ConvertRelativePathToFull(LogCommandLineOverrideValue)); } } FString CfgCommandLineOverrideValue; if (FParse::Value(FCommandLine::Get(), TEXT("ZenCfgPath="), CfgCommandLineOverrideValue)) { if (!CfgCommandLineOverrideValue.IsEmpty()) { Parms.Appendf(TEXT(" --config \"%s\""), *FPaths::ConvertRelativePathToFull(CfgCommandLineOverrideValue)); } } if (!InSettings.bSendUnattendedBugReports) { Parms.Append(TEXT(" --no-sentry")); } if (!InSettings.bAllowRemoteNetworkService) { Parms.Append(TEXT(" --http-forceloopback")); } return Parms; } bool Private::IsLocalAutoLaunched(FStringView InstanceURL) { if (!InstanceURL.IsEmpty() && !InstanceURL.Equals(TEXT(""))) { FString TempURL(InstanceURL); return IsLocalHost(TempURL); } return true; } bool Private::GetLocalDataCachePathOverride(FString& OutDataPath) { const TCHAR* AutoLaunchConfigSection = TEXT("Zen.AutoLaunch"); FString DataPath; DetermineLocalDataCachePath(AutoLaunchConfigSection, DataPath); if (DataPath.IsEmpty()) { return false; } OutDataPath = DataPath; return true; } bool TryGetLocalServiceRunContext(FZenLocalServiceRunContext& OutContext) { return OutContext.ReadFromJsonFile(*GetServiceRunContextPath()); } bool FZenLocalServiceRunContext::ReadFromJson(FJsonObject& JsonObject) { Executable = JsonObject.Values.FindRef(TEXT("Executable"))->AsString(); CommandlineArguments = JsonObject.Values.FindRef(TEXT("CommandlineArguments"))->AsString(); WorkingDirectory = JsonObject.Values.FindRef(TEXT("WorkingDirectory"))->AsString(); DataPath = JsonObject.Values.FindRef(TEXT("DataPath"))->AsString(); bShowConsole = JsonObject.Values.FindRef(TEXT("ShowConsole"))->AsBool(); if (!JsonObject.TryGetBoolField(TEXT("LimitProcessLifetime"), bLimitProcessLifetime)) { bLimitProcessLifetime = false; } return true; } void FZenLocalServiceRunContext::WriteToJson(TJsonWriter>& Writer) const { Writer.WriteValue(TEXT("Executable"), Executable); Writer.WriteValue(TEXT("CommandlineArguments"), CommandlineArguments); Writer.WriteValue(TEXT("WorkingDirectory"), WorkingDirectory); Writer.WriteValue(TEXT("DataPath"), DataPath); Writer.WriteValue(TEXT("ShowConsole"), bShowConsole); Writer.WriteValue(TEXT("LimitProcessLifetime"), bLimitProcessLifetime); } bool FZenLocalServiceRunContext::ReadFromJsonFile(const TCHAR* Filename) { FString JsonText; if (!FFileHelper::LoadFileToString(JsonText, Filename)) { return false; } TSharedPtr JsonObject; TSharedRef> Reader = TJsonReaderFactory::Create(JsonText); if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) { return false; } return ReadFromJson(*JsonObject); } bool FZenLocalServiceRunContext::WriteToJsonFile(const TCHAR* Filename) const { FString JsonTcharText; TSharedRef>> Writer = TJsonWriterFactory>::Create(&JsonTcharText); Writer->WriteObjectStart(); WriteToJson(*Writer); Writer->WriteObjectEnd(); Writer->Close(); if (!FFileHelper::SaveStringToFile(JsonTcharText, Filename)) { return false; } return true; } bool IsLocalServiceRunning(const TCHAR* DataPath, uint16* OutEffectivePort) { const FString LockFilePath = FPaths::Combine(DataPath, TEXT(".lock")); ZenLockFileData LockFileState; if (IsZenProcessUsingDataDir(*LockFilePath, &LockFileState)) { if (OutEffectivePort != nullptr && LockFileState.IsValid && LockFileState.IsReady) { *OutEffectivePort = LockFileState.EffectivePort; } return true; } return false; } FProcHandle StartLocalService(const FZenLocalServiceRunContext& Context, FStringView TransientArgs) { FString Parms = Context.GetCommandlineArguments(); if (Context.GetLimitProcessLifetime()) { Parms.Appendf(TEXT(" --owner-pid %d"), FPlatformProcess::GetCurrentProcessId()); } if (!TransientArgs.IsEmpty()) { Parms.Appendf(TEXT(" %.*s"), TransientArgs.Len(), TransientArgs.GetData()); } UE_LOG(LogZenServiceInstance, Display, TEXT("Launching executable '%s', working dir '%s', data dir '%s', args '%s'"), *Context.GetExecutable(), *Context.GetWorkingDirectory(), *Context.GetDataPath(), *Parms); FProcHandle Proc; #if PLATFORM_WINDOWS FString PlatformExecutable = Context.GetExecutable(); FPaths::MakePlatformFilename(PlatformExecutable); FString PlatformWorkingDirectory = Context.GetWorkingDirectory(); FPaths::MakePlatformFilename(PlatformWorkingDirectory); { // We could switch to FPlatformProcess::CreateProc for Windows as well if we are able to add the CREATE_BREAKAWAY_FROM_JOB flag // as that is needed on CI to stop Horde from terminating the zenserver process STARTUPINFO StartupInfo = { sizeof(STARTUPINFO), NULL, NULL, NULL, (::DWORD)CW_USEDEFAULT, (::DWORD)CW_USEDEFAULT, (::DWORD)CW_USEDEFAULT, (::DWORD)CW_USEDEFAULT, (::DWORD)0, (::DWORD)0, (::DWORD)0, (::DWORD)STARTF_USESHOWWINDOW, (::WORD)(Context.GetShowConsole() ? SW_SHOWMINNOACTIVE : SW_HIDE), 0, NULL, HANDLE(nullptr), HANDLE(nullptr), HANDLE(nullptr) }; FString CommandLine = FString::Printf(TEXT("\"%s\" %s"), *PlatformExecutable, *Parms); ::DWORD CreationFlagsArray[] = { NORMAL_PRIORITY_CLASS | DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB, // Try with the breakaway flag first NORMAL_PRIORITY_CLASS | DETACHED_PROCESS // If that fails (access denied), try without the breakaway flag next }; for (::DWORD CreationFlags : CreationFlagsArray) { PROCESS_INFORMATION ProcInfo; if (CreateProcess(NULL, CommandLine.GetCharArray().GetData(), nullptr, nullptr, false, CreationFlags, nullptr, PlatformWorkingDirectory.GetCharArray().GetData(), &StartupInfo, &ProcInfo)) { ::CloseHandle(ProcInfo.hThread); Proc = FProcHandle(ProcInfo.hProcess); break; } } if (!Proc.IsValid()) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed launching %s status: %d."), *CommandLine, GetLastError()); } } #else { bool bLaunchDetached = true; bool bLaunchHidden = true; bool bLaunchReallyHidden = !Context.GetShowConsole(); uint32* OutProcessID = nullptr; int32 PriorityModifier = 0; void* PipeWriteChild = nullptr; void* PipeReadChild = nullptr; Proc = FPlatformProcess::CreateProc( *Context.GetExecutable(), *Parms, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, OutProcessID, PriorityModifier, *Context.GetWorkingDirectory(), PipeWriteChild, PipeReadChild); } #endif return Proc; } bool StartLocalService(const FZenLocalServiceRunContext& Context) { FString TransientParms; FString StartupEventName = ZenSharedEvent::GetStartupEventName(); ZenSharedEvent StartupEvent(StartupEventName); if (!StartupEvent.Create()) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to create startup event when launcing executable '%s'"), *Context.GetExecutable()); return false; } TransientParms.Appendf(TEXT(" --child-id %s"), *StartupEventName); FProcHandle Proc = StartLocalService(Context, TransientParms); if (Proc.IsValid()) { bool ZenServerIsReady = false; FScopedSlowTask WaitForZenReadySlowTask(0, NSLOCTEXT("Zen", "Zen_WaitingForReady", "Waiting for ZenServer to be ready")); uint64 ZenWaitStartTime = FPlatformTime::Cycles64(); enum class EWaitDurationPhase { Short, Medium, Long } DurationPhase = EWaitDurationPhase::Short; while (FPlatformProcess::IsProcRunning(Proc)) { if (StartupEvent.Wait(5000)) { ZenServerIsReady = true; break; } double ZenWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenWaitStartTime); if (ZenWaitDuration >= 10.0) { if (DurationPhase == EWaitDurationPhase::Short) { if (!FPlatformProcess::IsProcRunning(Proc)) { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FText ZenLaunchFailurePromptTitle = NSLOCTEXT("Zen", "Zen_LaunchFailurePromptTitle", "Failed to launch"); FFormatNamedArguments FormatArguments; FString LogFilePath = FPaths::Combine(Context.GetDataPath(), TEXT("logs"), TEXT("zenserver.log")); FPaths::MakePlatformFilename(LogFilePath); FormatArguments.Add(TEXT("LogFilePath"), FText::FromString(LogFilePath)); FText ZenLaunchFailurePromptText = FText::Format(NSLOCTEXT("Zen", "Zen_LaunchFailurePromptText", "Unreal Zen Storage Server failed to launch. Please check the ZenServer log file for details:\n{LogFilePath}"), FormatArguments); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenLaunchFailurePromptText.ToString(), *ZenLaunchFailurePromptTitle.ToString()); break; } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Warning, TEXT("Unreal Zen Storage Server did not launch in the expected duration")); break; } } // Note that the dialog may not show up when zenserver is needed early in the launch cycle, but this will at least ensure // the splash screen is refreshed with the appropriate text status message. WaitForZenReadySlowTask.MakeDialog(true, false); UE_LOG(LogZenServiceInstance, Display, TEXT("Waiting for ZenServer to be ready...")); DurationPhase = EWaitDurationPhase::Medium; } #if !IS_PROGRAM else if (!(FApp::IsUnattended() || IsRunningCommandlet() || GIsRunningUnattendedScript) && ZenWaitDuration > 20.0 && (DurationPhase == EWaitDurationPhase::Medium)) { FText ZenLongWaitPromptTitle = NSLOCTEXT("Zen", "Zen_LongWaitPromptTitle", "Wait for ZenServer?"); FText ZenLongWaitPromptText = NSLOCTEXT("Zen", "Zen_LongWaitPromptText", "Unreal Zen Storage Server is taking a long time to launch. It may be performing maintenance. Keep waiting?"); if (FPlatformMisc::MessageBoxExt(EAppMsgType::YesNo, *ZenLongWaitPromptText.ToString(), *ZenLongWaitPromptTitle.ToString()) == EAppReturnType::No) { break; } DurationPhase = EWaitDurationPhase::Long; } #endif if (WaitForZenReadySlowTask.ShouldCancel()) { break; } } } if (!ZenServerIsReady) { if (FPlatformProcess::IsProcRunning(Proc)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Terminating unresponsive process for executable '%s'"), *Context.GetExecutable()); FPlatformProcess::TerminateProc(Proc, true); } } FPlatformProcess::CloseProc(Proc); return ZenServerIsReady; } return false; } bool StopLocalService(const TCHAR* DataPath, double MaximumWaitDurationSeconds) { const FString LockFilePath = FPaths::Combine(DataPath, TEXT(".lock")); ZenLockFileData LockFileState; if (ZenLockFileData::IsLockFileLocked(*LockFilePath, true)) { return ShutDownZenServerProcessLockingDataDir(DataPath, MaximumWaitDurationSeconds); } return true; } FString GetLocalServiceInstallPath() { if (FZenServiceLink Link = FZenServiceLink::Read(GetServiceLinkPath()); Link) { return Link.ServicePath; } else { return GetServiceCopyInstallPath(); } } FString GetLocalInstallUtilityPath() { if (FZenServiceLink Link = FZenServiceLink::Read(GetServiceLinkPath()); Link) { return Link.UtilityPath; } else { return GetUtilityCopyInstallPath(); } } FString GetLocalServiceInstallVersion(bool bDetailed) { IFileManager& FileManager = IFileManager::Get(); if (FZenServiceLink Link = FZenServiceLink::Read(GetServiceLinkPath()); Link) { if (!FileManager.FileExists(*Link.ServicePath)) { return FZenVersion().ToString(bDetailed); } return Link.Version.ToString(bDetailed); } else { const FString ServicePath = GetServiceCopyInstallPath(); if (!FileManager.FileExists(*ServicePath)) { return FZenVersion().ToString(bDetailed); } FZenVersion InstallVersion = GetZenVersion(GetUtilityCopyInstallPath(), ServicePath, GetInstallVersionCachePath()); return InstallVersion.ToString(bDetailed); } } static bool GIsDefaultServicePresent = false; FZenServiceInstance& GetDefaultServiceInstance() { static FZenServiceInstance DefaultServiceInstance; GIsDefaultServicePresent = true; return DefaultServiceInstance; } bool IsDefaultServicePresent() { return GIsDefaultServicePresent; } FScopeZenService::FScopeZenService() : FScopeZenService(FStringView()) { } FScopeZenService::FScopeZenService(FStringView InstanceURL) { if (!InstanceURL.IsEmpty() && !InstanceURL.Equals(TEXT(""))) { UniqueNonDefaultInstance = MakeUnique(InstanceURL); ServiceInstance = UniqueNonDefaultInstance.Get(); } else { ServiceInstance = &GetDefaultServiceInstance(); } } FScopeZenService::FScopeZenService(FServiceSettings&& InSettings) { UniqueNonDefaultInstance = MakeUnique(MoveTemp(InSettings)); ServiceInstance = UniqueNonDefaultInstance.Get(); } FScopeZenService::~FScopeZenService() {} FZenServiceInstance::FZenServiceInstance() : FZenServiceInstance(FStringView()) { } FZenServiceInstance::FZenServiceInstance(FStringView InstanceURL) { if (InstanceURL.IsEmpty()) { Settings.ReadFromConfig(); if (Settings.IsAutoLaunch()) { // Ensure that the zen data path is inherited by subprocesses FPlatformMisc::SetEnvironmentVar(TEXT("UE-ZenSubprocessDataPath"), *Settings.SettingsVariant.Get().DataPath); } } else { Settings.ReadFromURL(InstanceURL); } Initialize(); } FZenServiceInstance::FZenServiceInstance(FServiceSettings&& InSettings) : Settings(MoveTemp(InSettings)) { Initialize(); } FZenServiceInstance::~FZenServiceInstance() { } const FString FZenServiceInstance::GetPath() const { if (Settings.IsAutoLaunch()) { return Settings.SettingsVariant.Get().DataPath; } return GetEndpoint().GetURL(); } bool FZenServiceInstance::IsServiceRunning() { return !Settings.IsAutoLaunch() || bHasLaunchedLocal; } bool FZenServiceInstance::IsServiceReady() { return PingService(2); } bool FZenServiceInstance::PingService(uint32_t AttemptCount) { uint32 Attempt = 0; while (IsServiceRunning()) { Attempt++; Zen::FZenHttpRequest Request(Endpoint, false, 500); Zen::FZenHttpRequest::Result Result = Request.PerformBlockingDownload(TEXTVIEW("health/ready"), nullptr, Zen::EContentType::Text); if (Result == Zen::FZenHttpRequest::Result::Success && Zen::IsSuccessCode(Request.GetResponseCode())) { UE_LOGFMT(LogZenServiceInstance, Display, "Unreal Zen Storage Server HTTP service at {Domain} status: {Response}.", Endpoint.GetURL(), Request.GetResponseAsString()); return true; } if (IsServiceRunningLocally()) { if (Attempt == AttemptCount) { UE_LOGFMT(LogZenServiceInstance, Warning, "Unable to reach Unreal Zen Storage Server HTTP service at {Domain}. Status: {Code}. Response: {Response}", Endpoint.GetURL(), Request.GetResponseCode(), Request.GetResponseAsString()); break; } } else { UE_LOGFMT(LogZenServiceInstance, Display, "Unable to reach Unreal Zen Storage Server HTTP service at {Domain}. Status: {Code}. Response: {Response}", Endpoint.GetURL(), Request.GetResponseCode(), Request.GetResponseAsString()); break; } } return false; } bool FZenServiceInstance::TryRecovery() { if (!bHasLaunchedLocal) { return false; } const FString ExecutablePath = GetLocalServiceInstallPath(); const FString ExecutionContextFilePath = GetServiceRunContextPath(); static std::atomic LastRecoveryTicks; static bool bLastRecoveryResult = false; const FTimespan MaximumWaitForHealth = FTimespan::FromSeconds(30); const FTimespan MinimumDurationSinceLastRecovery = FTimespan::FromMinutes(2); FTimespan TimespanSinceLastRecovery = FDateTime::UtcNow() - FDateTime(LastRecoveryTicks.load(std::memory_order_relaxed)); if (TimespanSinceLastRecovery > MinimumDurationSinceLastRecovery) { FSystemWideCriticalSection RecoveryCriticalSection(TEXT("ZenServerLaunch"), MaximumWaitForHealth); if (!RecoveryCriticalSection.IsValid()) { // A recovery is already in progress but did not complete in time, we assume we failed and let recovery continue on a different thread return false; } // We test if the service is healthy as a different process might already have triggered a recovery bLastRecoveryResult = PingService(1); if (bLastRecoveryResult) { UE_LOG(LogZenServiceInstance, Display, TEXT("Local ZenServer status: Healthy. Skipping recovery")); } else { // Update timespan since it may have changed since we waited to enter the crit section TimespanSinceLastRecovery = FDateTime::UtcNow() - FDateTime(LastRecoveryTicks.load(std::memory_order_relaxed)); if (TimespanSinceLastRecovery > MinimumDurationSinceLastRecovery) { UE_LOG(LogZenServiceInstance, Display, TEXT("Local ZenServer recovery being attempted...")); uint16 Port = GetEndpoint().GetPort(); bool bShutdownExistingInstance = true; { const ZenServerState ServerState(/* ReadOnly */true); const ZenServerState::ZenServerEntry* Entry = ServerState.LookupByEffectiveListenPort(Port); if (Entry) { if (Entry->Pid.load(std::memory_order_relaxed) != AutoLaunchedPid) { // The running process pid is not the same as the one we launched. The process was relaunched elsewhere. Avoid shutting it down again. bShutdownExistingInstance = false; } } } if (bShutdownExistingInstance && !ShutdownZenServerProcess((int)AutoLaunchedPid)) // !ShutdownRunningServiceUsingEffectivePort(Port)) { return false; } AutoLaunch(Settings.SettingsVariant.Get(), ExecutablePath, ExecutionContextFilePath, Endpoint); Port = GetEndpoint().GetPort(); FDateTime StartedWaitingForHealth = FDateTime::UtcNow(); bLastRecoveryResult = PingService(1); while (!bLastRecoveryResult) { FTimespan WaitForHealth = FDateTime::UtcNow() - StartedWaitingForHealth; if (WaitForHealth > MaximumWaitForHealth) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Local ZenServer recovery timed out waiting for service to become healthy")); break; } FPlatformProcess::Sleep(0.5f); if (!IsZenProcessUsingEffectivePort(Port)) { AutoLaunch(Settings.SettingsVariant.Get(), ExecutablePath, ExecutionContextFilePath, Endpoint); } bLastRecoveryResult = PingService(1); } LastRecoveryTicks.store(FDateTime::UtcNow().GetTicks(), std::memory_order_relaxed); UE_LOG(LogZenServiceInstance, Display, TEXT("Local ZenServer recovery finished.")); if (bLastRecoveryResult) { UE_LOG(LogZenServiceInstance, Display, TEXT("Local ZenServer post recovery status: Healthy")); } else { UE_LOG(LogZenServiceInstance, Display, TEXT("Local ZenServer post recovery status: NOT healthy")); } } } } return bLastRecoveryResult; } bool FZenServiceInstance::AddSponsorProcessIDs(TArrayView SponsorProcessIDs) { uint16 Port = GetEndpoint().GetPort(); ZenServerState State(/*bReadOnly*/ false); ZenServerState::ZenServerEntry* Entry = State.LookupByEffectiveListenPort(Port); if (Entry) { bool bAllAdded = true; for (uint32 SponsorProcessID : SponsorProcessIDs) { if (!Entry->AddSponsorProcess(SponsorProcessID)) { bAllAdded = false; } } return bAllAdded; } return false; } uint16 FZenServiceInstance::GetAutoLaunchedPort() { return AutoLaunchedPort; } void FZenServiceInstance::Initialize() { if (Settings.IsAutoLaunch()) { uint64 ZenAutoLaunchStartTime = FPlatformTime::Cycles64(); const FServiceAutoLaunchSettings& AutoLaunchSettings = Settings.SettingsVariant.Get(); bool ServiceIsInstalled = ConditionalUpdateLocalInstall(AutoLaunchSettings.InstallMode); if (ServiceIsInstalled) { const FString ExecutablePath = GetLocalServiceInstallPath(); const FString ExecutionContextFilePath = GetServiceRunContextPath(); int LaunchAttempts = 0; const FTimespan MaximumWaitForHealth = FTimespan::FromSeconds(20); FDateTime StartedWaitingForHealth = FDateTime::UtcNow(); while (true) { { FSystemWideCriticalSection RecoveryCriticalSection(TEXT("ZenServerLaunch"), FTimespan::FromSeconds(5)); if (!RecoveryCriticalSection.IsValid()) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Local ZenServer AutoLaunch initialization timed out waiting for other process to complete startup/recovery")); } else { uint16 Port = GetEndpoint().GetPort(); bHasLaunchedLocal = AutoLaunch(AutoLaunchSettings, ExecutablePath, ExecutionContextFilePath, Endpoint); if (bHasLaunchedLocal) { const ZenServerState State(/*ReadOnly*/true); const ZenServerState::ZenServerEntry* RunningEntry = State.LookupByEffectiveListenPort(Port); if (RunningEntry != nullptr) { AutoLaunchedPid = RunningEntry->Pid.load(std::memory_order_relaxed); } AutoLaunchedPort = GetEndpoint().GetPort(); bIsRunningLocally = true; if (PingService(2)) { break; } } } } ++LaunchAttempts; FTimespan WaitForHealth = FDateTime::UtcNow() - StartedWaitingForHealth; if ((WaitForHealth > MaximumWaitForHealth) && (LaunchAttempts > 1)) { bHasLaunchedLocal = false; bIsRunningLocally = false; UE_LOG(LogZenServiceInstance, Warning, TEXT("Local ZenServer AutoLaunch initialization timed out waiting for service to become healthy")); break; } UE_LOG(LogZenServiceInstance, Log, TEXT("Awaiting ZenServer readiness")); FPlatformProcess::Sleep(0.5f); } } double ZenAutoLaunchDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenAutoLaunchStartTime); UE_LOG(LogZenServiceInstance, Log, TEXT("Local ZenServer AutoLaunch initialization completed in %.3lf seconds"), ZenAutoLaunchDuration); } else { const FServiceConnectSettings& ConnectExistingSettings = Settings.SettingsVariant.Get(); bIsRunningLocally = IsLocalHost(ConnectExistingSettings.HostName); Endpoint = FZenServiceEndpoint(ConnectExistingSettings.HostName, ConnectExistingSettings.Port); } } static void PromptUserToStopRunningServerInstanceForUpdate(const FString& ServerFilePath) { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FText ZenUpdatePromptTitle = NSLOCTEXT("Zen", "Zen_UpdatePromptTitle", "Update required"); FText ZenUpdatePromptText = FText::Format(NSLOCTEXT("Zen", "Zen_UpdatePromptText", "Unreal Zen Storage Server needs to be updated to a new version. Please shut down Unreal Editor and any tools that are using the ZenServer at '{0}'"), FText::FromString(ServerFilePath)); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenUpdatePromptText.ToString(), *ZenUpdatePromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Display, TEXT("Unreal Zen Storage Server needs to be updated to a new version. Please shut down any tools that are using the ZenServer at '%s'"), *ServerFilePath); } } static void PromptUserOfLockedDataFolder(const FString& DataPath) { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FText ZenLaunchFailurePromptTitle = NSLOCTEXT("Zen", "Zen_NonLocalProcessUsesDataDirPromptTitle", "Failed to launch"); FText ZenLaunchFailurePromptText = FText::Format(NSLOCTEXT("Zen", "Zen_NonLocalProcessUsesDataDirPromptText", "Unreal Zen Storage Server Failed to auto launch, an unknown process is locking the data folder '{0}'"), FText::FromString(DataPath)); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenLaunchFailurePromptText.ToString(), *ZenLaunchFailurePromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Warning, TEXT("Unreal Zen Storage Server Failed to auto launch, an unknown process is locking the data folder '%s'"), *DataPath); } } static void PromptUserOfFailedShutDownOfExistingProcess(uint16 Port) { #if !IS_PROGRAM if (!FApp::IsUnattended() && !IsRunningCommandlet() && !GIsRunningUnattendedScript) { FText ZenLaunchFailurePromptTitle = NSLOCTEXT("Zen", "Zen_ShutdownFailurePromptTitle", "Failed to launch"); FText ZenLaunchFailurePromptText = FText::Format(NSLOCTEXT("Zen", "Zen_ShutdownFailurePromptText", "Unreal Zen Storage Server Failed to auto launch, failed to shut down currently running service using port '{0}'"), FText::AsNumber(Port, &FNumberFormattingOptions::DefaultNoGrouping())); FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *ZenLaunchFailurePromptText.ToString(), *ZenLaunchFailurePromptTitle.ToString()); } else #endif { // Just log as there is no one to show a message UE_LOG(LogZenServiceInstance, Warning, TEXT("Unreal Zen Storage Server Failed to auto launch, failed to shut down currently running service using port %u"), Port); } } bool FZenServiceInstance::ConditionalUpdateLocalInstall(FServiceAutoLaunchSettings::EInstallMode InstallMode) { // Normally plugins config is written only if we update zen local install, // extra command line provided to force plugins update. const bool bForceZenPluginsInstall = FParse::Param(FCommandLine::Get(), TEXT("ForceZenPluginsInstall")); // If current settings require plugins, we will pass "--plugins-config" command line argument to zen server, // at that point plugins config file should exist, so create one if it doesn't. const bool bMissingZenPluginsConfig = Settings.IsRequirePlugins() && !IFileManager::Get().FileExists(*GetServicePluginsConfigPath()); if (bForceZenPluginsInstall || bMissingZenPluginsConfig) { WriteLocalPluginsConfig(); CleanOutOfDateServicePluginConfigs(); } if (InstallMode == FServiceAutoLaunchSettings::EInstallMode::Link) { const FString LinkPath = GetServiceLinkPath(); IFileManager& FileManager = IFileManager::Get(); bool LinkIsValid = false; const FZenServiceLink Link = FZenServiceLink::Read(LinkPath); if (Link) { if (!FileManager.FileExists(*Link.ServicePath)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Found service link file '%s' pointing to unreachable service executable '%s'"), *LinkPath, *Link.ServicePath); } else if (!FileManager.FileExists(*Link.UtilityPath)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Found service link file '%s' pointing to unreachable utility executable '%s'"), *LinkPath, *Link.UtilityPath); } else { LinkIsValid = true; } } else if (FileManager.FileExists(*LinkPath)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Found invalid service link file '%s', ignoring it"), *LinkPath); } FString InTreeServicePath = GetInTreeServicePath(); if (LinkIsValid && InTreeServicePath == Link.ServicePath) { // If the running process already points to our executable and we have a valid link file we are good to go uint32_t Pid = 0; if (ZenServerState::FindRunningProcessId(*InTreeServicePath, &Pid)) { UE_LOG(LogZenServiceInstance, Log, TEXT("Service link '%s' pointing to '%s', version %s is up and running"), *LinkPath, *Link.ServicePath, *Link.Version.ToString(false)); return true; } } FString InTreeUtilityPath = GetInTreeUtilityPath(); FZenVersion InTreeVersion; if (!GetZenVersion(InTreeUtilityPath, InTreeServicePath, InTreeVersion)) { checkf(false, TEXT("Unable to determine version using zen utility executable path: '%s'."), *InTreeUtilityPath); return false; } if (LinkIsValid) { if (Link.Version < InTreeVersion) { UE_LOG(LogZenServiceInstance, Display, TEXT("Installing service link '%s' to '%s', version %s"), *LinkPath, *InTreeServicePath, *InTreeVersion.ToString(false)); } else { // If the instance is running, assume it is valid and up to date uint32_t Pid = 0; if (ZenServerState::FindRunningProcessId(*Link.ServicePath, &Pid)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Service link '%s' pointing to '%s', version %s is up to date and running"), *LinkPath, *Link.ServicePath, *Link.Version.ToString(false)); return true; } // Verify that the executable pointed at is runnable and is of matching version FZenVersion LinkedVersion; if (GetZenVersion(Link.UtilityPath, Link.ServicePath, LinkedVersion)) { if (LinkedVersion == Link.Version) { UE_LOG(LogZenServiceInstance, Display, TEXT("Service link '%s' pointing to '%s', version %s is up to date"), *LinkPath, *Link.ServicePath, *Link.Version.ToString(false)); return true; } else { UE_LOG(LogZenServiceInstance, Display, TEXT("Updating service link '%s' to '%s', version %s (link '%s', version %s does not match executable version %s)"), *LinkPath, *InTreeServicePath, *InTreeVersion.ToString(false), *Link.ServicePath, *Link.Version.ToString(false), *LinkedVersion.ToString(false)); } } else { UE_LOG(LogZenServiceInstance, Display, TEXT("Updating service link '%s' to '%s', version %s (link '%s', version %s pointing to invalid executable)"), *LinkPath, *InTreeServicePath, *InTreeVersion.ToString(false), *Link.ServicePath, *Link.Version.ToString(false)); } } } FZenServiceLink NewLink{ .ServicePath = InTreeServicePath, .UtilityPath = InTreeUtilityPath, .Version = InTreeVersion }; if (!FZenServiceLink::Write(NewLink, LinkPath)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed to update service link file '%s'"), *LinkPath); return false; } WriteLocalPluginsConfig(); FString ServiceCopyInstallPath = GetServiceCopyInstallPath(); if (FileManager.FileExists(*ServiceCopyInstallPath)) { ShutDownZenServerProcessExecutable(ServiceCopyInstallPath); } TArray FilesToCleanUp{ GetUtilityCopyInstallPath(), ServiceCopyInstallPath, GetInstallVersionCachePath(), GetInstallVersionCachePath() }; #if PLATFORM_WINDOWS FilesToCleanUp.Add(FPaths::SetExtension(FilesToCleanUp[0], TEXT("pdb"))); FilesToCleanUp.Add(FPaths::SetExtension(FilesToCleanUp[1], TEXT("pdb"))); #endif // PLATFORM_WINDOWS for (const FString& FileToCleanUp : FilesToCleanUp) { // If zenserver is still running we may fail to clean up a file. Not critical, just try again next startup if (!AttemptFileDeleteWithRetries(*FileToCleanUp, 1.0)) { UE_LOG(LogZenServiceInstance, Log, TEXT("Failed cleaning up file {%s} (not critical)"), *FileToCleanUp); } } return true; } else if (InstallMode == FServiceAutoLaunchSettings::EInstallMode::Copy) { FString InTreeUtilityPath = GetInTreeUtilityPath(); FString InstallUtilityPath = GetUtilityCopyInstallPath(); FString InTreeVersionCache = GetInTreeVersionCache(); FString InTreeServicePath = GetInTreeServicePath(); FString InstallServicePath = GetServiceCopyInstallPath(); FString InstallVersionCache = GetInstallVersionCachePath(); IFileManager& FileManager = IFileManager::Get(); bool bMainExecutablesUpdated = false; if (IsInstallVersionOutOfDate(InTreeUtilityPath, InstallUtilityPath, InTreeServicePath, InstallServicePath, InTreeVersionCache, InstallVersionCache)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Installing service from '%s' to '%s'"), *InTreeServicePath, *InstallServicePath); if (!ShutDownZenServerProcessExecutable(InstallServicePath)) { PromptUserToStopRunningServerInstanceForUpdate(InstallServicePath); return false; } // Even after waiting for the process to shut down we have a tolerance for failure when overwriting the target files if (!AttemptFileCopyWithRetries(*InstallServicePath, *InTreeServicePath, 5.0)) { PromptUserToStopRunningServerInstanceForUpdate(InstallServicePath); return false; } if (!AttemptFileCopyWithRetries(*InstallUtilityPath, *InTreeUtilityPath, 5.0)) { PromptUserToStopRunningServerInstanceForUpdate(InstallServicePath); return false; } AttemptFileCopyWithRetries(*InstallVersionCache, *InTreeVersionCache, 1.0); WriteLocalPluginsConfig(); bMainExecutablesUpdated = true; } #if PLATFORM_WINDOWS struct FZenExecutable { FString& InTreeFilePath; FString& InstallFilePath; }; const FZenExecutable ZenExecutables[] = { // Service executable (zenserver.exe) {InTreeServicePath, InstallServicePath}, // Utility executable (zen.exe) {InTreeUtilityPath, InstallUtilityPath}, }; for (const FZenExecutable& Executable : ZenExecutables) { FString InTreeSymbolFilePath = FPaths::SetExtension(Executable.InTreeFilePath, TEXT("pdb")); FString InstallSymbolFilePath = FPaths::SetExtension(Executable.InstallFilePath, TEXT("pdb")); if (FileManager.FileExists(*InTreeSymbolFilePath) && (bMainExecutablesUpdated || !FileManager.FileExists(*InstallSymbolFilePath))) { AttemptFileCopyWithRetries(*InstallSymbolFilePath, *InTreeSymbolFilePath, 1.0); } } #endif FString InTreeCrashpadHandlerFilePath = GetInTreeCrashpadHandlerFilePath(); FString InstallCrashpadHandlerFilePath = GetInstallCrashpadHandlerFilePath(InTreeCrashpadHandlerFilePath); if (FileManager.FileExists(*InTreeCrashpadHandlerFilePath) && (bMainExecutablesUpdated || !FileManager.FileExists(*InstallCrashpadHandlerFilePath))) { AttemptFileCopyWithRetries(*InstallCrashpadHandlerFilePath, *InTreeCrashpadHandlerFilePath, 1.0); } const FString LinkPath = GetServiceLinkPath(); if (FZenServiceLink Link = FZenServiceLink::Read(LinkPath); Link) { ShutDownZenServerProcessExecutable(Link.ServicePath); if (!AttemptFileDeleteWithRetries(*LinkPath, 1.0)) { UE_LOG(LogZenServiceInstance, Log, TEXT("Failed cleaning up file {%s} (not critical)"), *LinkPath); } } return true; } else { return false; } } bool FZenServiceInstance::WriteLocalPluginsConfig() { if (!Settings.IsRequirePlugins()) { return false; } TConstArrayView PluginsSettings = Settings.SettingsVariant.Get().Plugins; TArray> JsonArray; // replace this with UStructToJsonObject or similar in the future for (const FServicePluginSettings& PluginSettings : PluginsSettings) { TSharedPtr JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField(TEXT("name"), PluginSettings.AbsPath); // zen server can treat name as absolute path for (const TPair& Option: PluginSettings.Options) { JsonObject->SetStringField(Option.Key.ToString(), Option.Value); } TSharedPtr JsonValueObject = MakeShareable(new FJsonValueObject(MoveTemp(JsonObject))); JsonArray.Add(JsonValueObject); } FString OutputString; TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); if (!FJsonSerializer::Serialize(JsonArray, Writer)) { return false; } const FString OutputPath = GetServicePluginsConfigPath(); UE_LOG(LogZenServiceInstance, Display, TEXT("Writing plugin configuration to '%s'"), *OutputPath); return FFileHelper::SaveStringToFile(OutputString, *OutputPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); } bool FZenServiceInstance::AutoLaunch(const FServiceAutoLaunchSettings& InSettings, const FString& ExecutablePath, const FString& ExecutionContextFilePath, FZenServiceEndpoint& OutEndpoint) { IFileManager& FileManager = IFileManager::Get(); const FString LockFilePath = FPaths::Combine(InSettings.DataPath, TEXT(".lock")); FString WorkingDirectory = FPaths::GetPath(ExecutablePath); ZenLockFileData LockFileState; uint64 ZenWaitForRunningProcessReadyStartTime = FPlatformTime::Cycles64(); while (IsZenProcessUsingDataDir(*LockFilePath, &LockFileState) && LockFileState.IsValid && !LockFileState.IsReady) { // Server is starting up, wait for it to get ready double ZenWaitDuration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ZenWaitForRunningProcessReadyStartTime); if (ZenWaitDuration > 5.0) { break; } FPlatformProcess::Sleep(0.1f); LockFileState = ZenLockFileData(); } bool bShutDownExistingInstanceForDataPath = true; uint32 ShutdownExistingInstanceForPid = 0; bool bLaunchNewInstance = true; if (LockFileState.IsReady) { const ZenServerState State(/*ReadOnly*/true); if (State.LookupByPid(LockFileState.ProcessId) == nullptr && IsZenProcessUsingDataDir(*LockFilePath, nullptr)) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Found locked valid lock file '%s' but can't find registered process (Pid: %d), will attempt shut down"), *LockFilePath, LockFileState.ProcessId); bShutDownExistingInstanceForDataPath = true; } else { if (InSettings.bIsDefaultSharedRunContext) { FZenLocalServiceRunContext DesiredRunContext; DesiredRunContext.Executable = ExecutablePath; DesiredRunContext.CommandlineArguments = DetermineCmdLineWithoutTransientComponents(InSettings, InSettings.DesiredPort); DesiredRunContext.WorkingDirectory = WorkingDirectory; DesiredRunContext.DataPath = InSettings.DataPath; DesiredRunContext.bShowConsole = InSettings.bShowConsole; DesiredRunContext.bLimitProcessLifetime = InSettings.bLimitProcessLifetime; FZenLocalServiceRunContext CurrentRunContext; bool ReadCurrentContextOK = CurrentRunContext.ReadFromJsonFile(*ExecutionContextFilePath); if (ReadCurrentContextOK && (DesiredRunContext == CurrentRunContext)) { UE_LOG(LogZenServiceInstance, Log, TEXT("Found existing instance running on port %u matching our settings, no actions needed"), InSettings.DesiredPort); bLaunchNewInstance = false; bShutDownExistingInstanceForDataPath = false; } else { FString JsonTcharText; { TSharedRef>> Writer = TJsonWriterFactory>::Create(&JsonTcharText); Writer->WriteObjectStart(); Writer->WriteObjectStart("Current"); CurrentRunContext.WriteToJson(*Writer); Writer->WriteObjectEnd(); Writer->WriteObjectStart("Desired"); DesiredRunContext.WriteToJson(*Writer); Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); Writer->Close(); } UE_LOG(LogZenServiceInstance, Log, TEXT("Found existing instance running on port %u with different run context, will attempt shut down\n{%s}"), InSettings.DesiredPort, *JsonTcharText); bShutDownExistingInstanceForDataPath = true; bLaunchNewInstance = true; } } else { UE_LOG(LogZenServiceInstance, Log, TEXT("Found existing instance running on port %u when not using shared context, will use it"), InSettings.DesiredPort); bShutDownExistingInstanceForDataPath = false; bLaunchNewInstance = false; } } } else { const ZenServerState State(/*ReadOnly*/true); const ZenServerState::ZenServerEntry* RunningEntry = State.LookupByDesiredListenPort(InSettings.DesiredPort); if (RunningEntry != nullptr) { // It is necessary to tear down an existing zenserver running on our desired port but in a different data path because: // 1. zenserver won't accept port collision with itself, and will instead say "Exiting since there is already a process listening to port ..." // 2. When UE is changing data directories (eg: DDC path config change) we don't want to leave zenservers running on the past directories for no reason // Unlike other shutdown scenarios, this one can't be done based on our desired data path because the zenserver we want to shut down is running in a different data path UE_LOG(LogZenServiceInstance, Log, TEXT("Found existing instance running on port %u with different data directory, will attempt shutdown"), InSettings.DesiredPort); ShutdownExistingInstanceForPid = RunningEntry->Pid; } else { UE_LOG(LogZenServiceInstance, Log, TEXT("No current process using the data dir found, launching a new instance")); } bShutDownExistingInstanceForDataPath = false; bLaunchNewInstance = true; } if (bShutDownExistingInstanceForDataPath) { if (!ShutDownZenServerProcessLockingDataDir(InSettings.DataPath)) { PromptUserOfFailedShutDownOfExistingProcess(InSettings.DesiredPort); return false; } } if (ShutdownExistingInstanceForPid != 0) { if (!ShutdownZenServerProcess(ShutdownExistingInstanceForPid)) { PromptUserOfFailedShutDownOfExistingProcess(InSettings.DesiredPort); return false; } } if (bLaunchNewInstance) { if (InSettings.bIsDefaultDataPath && InSettings.bIsDefaultSharedRunContext) { // See if the default data path is migrating, and if so, clean up after the old one. // Non-default data paths don't do the same thing because users are free to switch them back and forth // and expext the contents to remain when they change. Only the default one cleans up after itself // to avoid a situation wherey the accumulate over time as the default location changes in config. // This cleanup is best-effort and may fail if an instance is unexpectedly still using the previous path. EnsureEditorSettingsConfigLoaded(); FString InUseDefaultDataPath; if (!GConfig->GetString(TEXT("/Script/UnrealEd.ZenServerSettings"), TEXT("InUseDefaultDataPath"), InUseDefaultDataPath, GEditorSettingsIni)) { InUseDefaultDataPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPlatformProcess::ApplicationSettingsDir(), TEXT("Zen\\Data"))); } if (!InUseDefaultDataPath.IsEmpty()) { const FString InUseLockFilePath = FPaths::Combine(InUseDefaultDataPath, TEXT(".lock")); if (!FPaths::IsSamePath(InUseDefaultDataPath, InSettings.DataPath) && !IsZenProcessUsingDataDir(*InUseLockFilePath, nullptr)) { UE_LOG(LogZenServiceInstance, Display, TEXT("Migrating default data path from '%s' to '%s'. Old location will be deleted."), *InUseDefaultDataPath, *InSettings.DataPath); IFileManager::Get().DeleteDirectory(*InUseDefaultDataPath, false, true); } } } FString ParmsWithoutTransients = DetermineCmdLineWithoutTransientComponents(InSettings, InSettings.DesiredPort); FZenLocalServiceRunContext EffectiveRunContext; EffectiveRunContext.Executable = ExecutablePath; EffectiveRunContext.CommandlineArguments = ParmsWithoutTransients; EffectiveRunContext.WorkingDirectory = WorkingDirectory; EffectiveRunContext.DataPath = InSettings.DataPath; EffectiveRunContext.bShowConsole = InSettings.bShowConsole; EffectiveRunContext.bLimitProcessLifetime = InSettings.bLimitProcessLifetime; if (StartLocalService(EffectiveRunContext)) { // Only write run context if we're using the default shared run context if (InSettings.bIsDefaultSharedRunContext) { EffectiveRunContext.WriteToJsonFile(*ExecutionContextFilePath); } } else { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed launch service using executable '%s' on port %u"), *ExecutablePath, InSettings.DesiredPort); return false; } } else if (InSettings.bLimitProcessLifetime) { ZenServerState State(/*ReadOnly*/ false); ZenServerState::ZenServerEntry* RunningEntry = State.LookupByDesiredListenPort(InSettings.DesiredPort); if (RunningEntry == nullptr) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed attach as sponsor process to executable '%s' on port %u, can't find entry in shared state"), *ExecutablePath, InSettings.DesiredPort); } else if (!RunningEntry->AddSponsorProcess(FPlatformProcess::GetCurrentProcessId())) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Failed attach as sponsor process to executable '%s' on port %u, too many sponsored processes attached already or zenserver is unresponsive"), *ExecutablePath, InSettings.DesiredPort); } } if (InSettings.bIsDefaultDataPath && InSettings.bIsDefaultSharedRunContext) { GConfig->SetString(TEXT("/Script/UnrealEd.ZenServerSettings"), TEXT("InUseDefaultDataPath"), *InSettings.DataPath, GEditorSettingsIni); } FStringView HostName = TEXT("[::1]"); OutEndpoint = FZenServiceEndpoint(HostName, InSettings.DesiredPort); ZenLockFileData RunningLockFileState = ZenLockFileData::ReadCbLockFile(*LockFilePath); if (!RunningLockFileState.IsValid) { return false; } if (!RunningLockFileState.IsReady) { return false; } OutEndpoint = FZenServiceEndpoint(HostName, RunningLockFileState.EffectivePort); return true; } bool FZenServiceInstance::GetCacheStats(FZenCacheStats& Stats) { { TUniqueLock Lock(LastCacheStatsMutex); // If we've already requested stats and they are ready then grab them if ( CacheStatsRequest.IsReady() == true ) { LastCacheStats = CacheStatsRequest.Get(); LastCacheStatsTime = FPlatformTime::Cycles64(); CacheStatsRequest.Reset(); } // Make a copy of the last updated stats Stats = LastCacheStats; const uint64 CurrentTime = FPlatformTime::Cycles64(); constexpr double MinTimeBetweenRequestsInSeconds = 0.5; const double DeltaTimeInSeconds = FPlatformTime::ToSeconds64(CurrentTime - LastCacheStatsTime); if (CacheStatsRequest.IsValid() || DeltaTimeInSeconds <= MinTimeBetweenRequestsInSeconds) { return Stats.bIsValid; } } #if WITH_EDITOR EAsyncExecution ThreadPool = EAsyncExecution::LargeThreadPool; #else EAsyncExecution ThreadPool = EAsyncExecution::ThreadPool; #endif // We've not got any requests in flight and we've met a given time requirement for requests CacheStatsRequest = Async(ThreadPool, [&Endpoint = Endpoint] { UE::Zen::FZenHttpRequest Request(Endpoint, false); TArray64 GetBuffer; FZenHttpRequest::Result Result = Request.PerformBlockingDownload(TEXTVIEW("/stats/z$"), &GetBuffer, Zen::EContentType::CbObject); FZenCacheStats Stats; if (Result == Zen::FZenHttpRequest::Result::Success && Request.GetResponseCode() == 200) { FCbFieldView RootView(GetBuffer.GetData()); Stats.bIsValid = LoadFromCompactBinary(RootView, Stats); } return Stats; }); return Stats.bIsValid; } bool FZenServiceInstance::GetProjectStats(FZenProjectStats& Stats) { { TUniqueLock Lock(LastProjectStatsMutex); // If we've already requested stats and they are ready then grab them if ( ProjectStatsRequest.IsReady() == true ) { LastProjectStats = ProjectStatsRequest.Get(); LastProjectStatsTime = FPlatformTime::Cycles64(); ProjectStatsRequest.Reset(); } // Make a copy of the last updated stats Stats = LastProjectStats; const uint64 CurrentTime = FPlatformTime::Cycles64(); constexpr double MinTimeBetweenRequestsInSeconds = 0.5; const double DeltaTimeInSeconds = FPlatformTime::ToSeconds64(CurrentTime - LastProjectStatsTime); if (ProjectStatsRequest.IsValid() || DeltaTimeInSeconds <= MinTimeBetweenRequestsInSeconds) { return Stats.bIsValid; } } #if WITH_EDITOR EAsyncExecution ThreadPool = EAsyncExecution::LargeThreadPool; #else EAsyncExecution ThreadPool = EAsyncExecution::ThreadPool; #endif // We've not got any requests in flight and we've met a given time requirement for requests ProjectStatsRequest = Async(ThreadPool, [&Endpoint = Endpoint] { UE::Zen::FZenHttpRequest Request(Endpoint, false); TArray64 GetBuffer; FZenHttpRequest::Result Result = Request.PerformBlockingDownload(TEXTVIEW("/stats/prj"), &GetBuffer, Zen::EContentType::CbObject); FZenProjectStats Stats; if (Result == Zen::FZenHttpRequest::Result::Success && Request.GetResponseCode() == 200) { FCbFieldView RootView(GetBuffer.GetData()); Stats.bIsValid = LoadFromCompactBinary(RootView, Stats); } return Stats; }); return Stats.bIsValid; } bool FZenServiceInstance::GetGCStatus(FGCStatus& Status) { check(IsInGameThread()); // If we've already requested status and it is ready then grab it if (GCStatusRequest.IsReady() == true ) { LastGCStatus = GCStatusRequest.Get(); LastGCStatusTime = FPlatformTime::Cycles64(); GCStatusRequest.Reset(); } // Make a copy of the last updated status if (LastGCStatus.IsSet()) { Status = LastGCStatus.GetValue(); } const uint64 CurrentTime = FPlatformTime::Cycles64(); constexpr double MinTimeBetweenRequestsInSeconds = 0.5; const double DeltaTimeInSeconds = FPlatformTime::ToSeconds64(CurrentTime - LastGCStatusTime); if (!GCStatusRequest.IsValid() && DeltaTimeInSeconds > MinTimeBetweenRequestsInSeconds) { #if WITH_EDITOR EAsyncExecution ThreadPool = EAsyncExecution::LargeThreadPool; #else EAsyncExecution ThreadPool = EAsyncExecution::ThreadPool; #endif // We've not got any requests in flight and we've met a given time requirement for requests GCStatusRequest = Async(ThreadPool, [this] { UE::Zen::FZenHttpRequest Request(Endpoint, false); TArray64 GetBuffer; FZenHttpRequest::Result Result = Request.PerformBlockingDownload(TEXTVIEW("/admin/gc"), &GetBuffer, Zen::EContentType::CbObject); TOptional GCStatus; if (Result == Zen::FZenHttpRequest::Result::Success && Request.GetResponseCode() == 200) { FCbObjectView RootObjectView(GetBuffer.GetData()); GCStatus.Emplace(); GCStatus->Description = FString(RootObjectView["Status"].AsString()); } return GCStatus; }); } return LastGCStatus.IsSet(); } bool FZenServiceInstance::RequestGC(const bool* OverrideCollectSmallObjects, const uint32* OverrideMaxCacheDuration) { UE::Zen::FZenHttpRequest Request(Endpoint, /*bLogErrors*/ false); TCHAR Separators[] = {TEXT('?'), TEXT('&')}; int32 SeparatorIndex = 0; TStringBuilder<128> Query; Query << TEXTVIEW("/admin/gc"); if (OverrideCollectSmallObjects) { Query << Separators[SeparatorIndex] << TEXT("smallobjects=") << ::LexToString(*OverrideCollectSmallObjects); SeparatorIndex = FMath::Min(SeparatorIndex + 1, (int32)UE_ARRAY_COUNT(Separators)); } if (OverrideMaxCacheDuration) { Query << Separators[SeparatorIndex] << TEXT("maxcacheduration=") << ::LexToString(*OverrideMaxCacheDuration); SeparatorIndex = FMath::Min(SeparatorIndex + 1, (int32)UE_ARRAY_COUNT(Separators)); } FZenHttpRequest::Result Result = Request.PerformBlockingPost(Query, FMemoryView()); if (Result == Zen::FZenHttpRequest::Result::Success && Request.GetResponseCode() == 200) { FCbObjectView ResponseObject = FCbObjectView(Request.GetResponseBuffer().GetData()); FUtf8StringView ResponseStatus = ResponseObject["status"].AsString(); return (ResponseStatus == "Started") || (ResponseStatus == "Running"); } return false; } bool FZenServiceInstance::GatherAnalytics(TArray& Attributes) { FZenCacheStats ZenCacheStats; FZenProjectStats ZenProjectStats; if (GetCacheStats(ZenCacheStats) == false) return false; if (GetProjectStats(ZenProjectStats) == false) return false; const FString BaseName = TEXT("Zen_"); { FString AttrName = BaseName + TEXT("Enabled"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.bIsValid && ZenProjectStats.bIsValid); } ///////////// Cache { FString AttrName = BaseName + TEXT("Cache_Size_Disk"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.Size.Disk); } { FString AttrName = BaseName + TEXT("Cache_Size_Memory"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.Size.Memory); } { FString AttrName = BaseName + TEXT("Cache_Hits"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.Hits); } { FString AttrName = BaseName + TEXT("Cache_Misses"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.Misses); } { FString AttrName = BaseName + TEXT("Cache_Writes"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.Writes); } { FString AttrName = BaseName + TEXT("Cache_HitRatio"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.HitRatio); } { FString AttrName = BaseName + TEXT("Cache_Cas_Hits"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.CidHits); } { FString AttrName = BaseName + TEXT("Cache_Cas_Misses"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.CidMisses); } { FString AttrName = BaseName + TEXT("Cache_Cas_Writes"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.CidWrites); } { FString AttrName = BaseName + TEXT("Cache_Requests"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Request.Count); } { FString AttrName = BaseName + TEXT("Cache_BadRequests"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.General.BadRequestCount); } { FString AttrName = BaseName + TEXT("Cache_Requests_Count"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Request.Count); } { FString AttrName = BaseName + TEXT("Cache_Requests_RateMean"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Request.RateMean); } { FString AttrName = BaseName + TEXT("Cache_Requests_TAverage"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Request.TAverage); } { FString AttrName = BaseName + TEXT("Cache_Requests_TMin"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Request.TMin); } { FString AttrName = BaseName + TEXT("Cache_Requests_TMax"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Request.TMax); } { FString AttrName = BaseName + TEXT("Cache_TotalUploadedMB"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Upstream.TotalUploadedMB); } { FString AttrName = BaseName + TEXT("Upstream_TotalDownloadedMB"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Upstream.TotalDownloadedMB); } { FString AttrName = BaseName + TEXT("Upstream_TotalUploadedMB"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.Upstream.TotalUploadedMB); } { FString AttrName = BaseName + TEXT("Upstream_Requests_Count"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.UpstreamRequest.Count); } { FString AttrName = BaseName + TEXT("Upstream_Requests_RateMean"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.UpstreamRequest.RateMean); } { FString AttrName = BaseName + TEXT("Upstream_Requests_TAverage"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.UpstreamRequest.TAverage); } { FString AttrName = BaseName + TEXT("Upstream_Requests_TMin"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.UpstreamRequest.TMin); } { FString AttrName = BaseName + TEXT("Upstream_Requests_TMax"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.UpstreamRequest.TMax); } { FString AttrName = BaseName + TEXT("Cas_Size_Large"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.CID.Size.Large); } { FString AttrName = BaseName + TEXT("Cas_Size_Small"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.CID.Size.Small); } { FString AttrName = BaseName + TEXT("Cas_Size_Tiny"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.CID.Size.Tiny); } { FString AttrName = BaseName + TEXT("Cas_Size_Total"); Attributes.Emplace(MoveTemp(AttrName), ZenCacheStats.CID.Size.Total); } ///////////// Project { FString AttrName = BaseName + TEXT("Project_Size_Disk"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Size.Disk); } { FString AttrName = BaseName + TEXT("Project_Size_Memory"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Size.Memory); } { FString AttrName = BaseName + TEXT("Project_WriteCount"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Project.WriteCount); } { FString AttrName = BaseName + TEXT("Project_ReadCount"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Project.ReadCount); } { FString AttrName = BaseName + TEXT("Project_DeleteCount"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Project.DeleteCount); } { FString AttrName = BaseName + TEXT("Project_Oplog_WriteCount"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Oplog.WriteCount); } { FString AttrName = BaseName + TEXT("Project_Oplog_ReadCount"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Oplog.ReadCount); } { FString AttrName = BaseName + TEXT("Project_Oplog_DeleteCount"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Oplog.DeleteCount); } { FString AttrName = BaseName + TEXT("Project_Op_Hits"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Op.HitCount); } { FString AttrName = BaseName + TEXT("Project_Op_Misses"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Op.MissCount); } { FString AttrName = BaseName + TEXT("Project_Op_Writes"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Op.WriteCount); } { FString AttrName = BaseName + TEXT("Project_Chunk_Hits"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Chunk.HitCount); } { FString AttrName = BaseName + TEXT("Project_Chunk_Misses"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Chunk.MissCount); } { FString AttrName = BaseName + TEXT("Project_Chunk_Writes"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.Chunk.WriteCount); } { FString AttrName = BaseName + TEXT("Project_Requests"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.RequestCount); } { FString AttrName = BaseName + TEXT("Project_BadRequests"); Attributes.Emplace(MoveTemp(AttrName), ZenProjectStats.General.BadRequestCount); } { FString AttrName = BaseName + TEXT("Project_Op_HitRatio"); double Total = static_cast(ZenProjectStats.General.Op.HitCount + ZenProjectStats.General.Op.MissCount); Attributes.Emplace(MoveTemp(AttrName), Total > 0 ? static_cast(ZenProjectStats.General.Op.HitCount) / Total : 0.0); } { FString AttrName = BaseName + TEXT("Project_Chunk_HitRatio"); double Total = static_cast(ZenProjectStats.General.Chunk.HitCount + ZenProjectStats.General.Chunk.MissCount); Attributes.Emplace(MoveTemp(AttrName), Total > 0 ? static_cast(ZenProjectStats.General.Chunk.HitCount) / Total : 0.0); } return true; } bool FZenServiceInstance::GetWorkspaces(FZenWorkspaces& Workspaces) const { #if WITH_EDITOR EAsyncExecution ThreadPool = EAsyncExecution::LargeThreadPool; #else EAsyncExecution ThreadPool = EAsyncExecution::ThreadPool; #endif WorkspacesRequest = Async(ThreadPool, [&Endpoint = Endpoint] { UE::Zen::FZenHttpRequest Request(Endpoint, false); FZenHttpRequest::Result Result = Request.PerformBlockingDownload(TEXTVIEW("/ws"), nullptr, Zen::EContentType::CbObject); FZenWorkspaces Workspaces; if (Result == Zen::FZenHttpRequest::Result::Success && Request.GetResponseCode() == 200) { FMemoryReaderView Reader(Request.GetResponseBuffer()); FCbObject ResponseObj = LoadCompactBinary(Reader).AsObject(); FCbArrayView WorkspacesArray = ResponseObj["workspaces"].AsArrayView(); for (FCbFieldView WorkspaceField : WorkspacesArray) { FCbObjectView WorkspaceObject = WorkspaceField.AsObjectView(); FZenWorkspaces::Workspace Workspace = { .Id = *WriteToString<64>(WorkspaceObject["id"].AsObjectId()), .BaseDir = FString(WorkspaceObject["root_path"].AsString()), .bDynamicShare = WorkspaceObject["allow_share_creation_from_http"].AsBool() }; if (!Workspace.Id.IsEmpty()) { FCbArrayView WorkspaceShares = WorkspaceObject["shares"].AsArrayView(); UE_LOG(LogZenServiceInstance, Warning, TEXT("Number of shares %d"), WorkspaceShares.Num()); for (int32 Idx = 0; Idx < WorkspaceShares.Num(); ++Idx) { UE_LOG(LogZenServiceInstance, Warning, TEXT("Iterating through share %d"), Idx); } for (FCbFieldView ShareField : WorkspaceShares) { FCbObjectView ShareObject = ShareField.AsObjectView(); FZenWorkspaces::Share Share = { .Id = *WriteToString<64>(ShareObject["id"].AsObjectId()), .Dir = FString(ShareObject["share_path"].AsString()), .Alias = FString(ShareObject["alias"].AsString()) }; if (!Share.Id.IsEmpty()) { Workspace.WorkspaceShares.Add(Share); } } Workspaces.ZenWorkspaces.Add(Workspace); } } Workspaces.bIsValid = true; } return Workspaces; }); WorkspacesRequest.Wait(); Workspaces = WorkspacesRequest.Get(); return Workspaces.bIsValid; } int32 FZenServiceInstance::GetWorkspaceCount() const { FZenWorkspaces Workspaces; if (!GetWorkspaces(Workspaces)) { return 0; } return Workspaces.ZenWorkspaces.Num(); } #endif // UE_WITH_ZEN }