// Copyright Epic Games, Inc. All Rights Reserved. using AutomationTool; using EpicGames.Core; using EpicGames.Horde.Jobs; using EpicGames.ProjectStore; using EpicGames.Serialization; using IdentityModel.OidcClient; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.WebSockets; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using UnrealBuildBase; using UnrealBuildTool; namespace AutomationUtils { public class ZenWorkspace { public string id { get; set; } public string root_path { get; set; } public bool allow_share_creation_from_http { get; set; } public List shares { get; set; } } public class ZenWorkspaceShare { public string id { get; set; } public string share_path { get; set; } } public class ZenRunContext { public string Executable { get; set; } public string CommandLineArguments { get; set; } public string DataPath { get; set; } public string HostName { get; private set; } = string.Empty; public int HostPort { get; private set; } = 0; public bool IsValid { get; private set; } = false; public static ILogger Logger => Log.Logger; public static ZenRunContext ReadFromContextFile(FileReference ContextFile) { if (!FileReference.Exists(ContextFile)) { return null; } string ContextData = FileReference.ReadAllText(ContextFile); JsonSerializerOptions JsonOptions = ZenUtils.GetDefaultJsonSerializerOptions(); ZenRunContext Ret = JsonSerializer.Deserialize(ContextData, JsonOptions); if (Ret != null) { Ret.InitializeInstanceData(); } return Ret; } public static ZenRunContext DiscoverServerContext() { FileReference ContextFile = new (ZenUtils.GetZenRunContextFile()); if (!FileReference.Exists(ContextFile)) { return null; } return ReadFromContextFile(ContextFile); } private void InitializeInstanceData() { string LockFilePath = Path.Combine(DataPath, ".lock"); FileReference LockFile = new(LockFilePath); if (!FileReference.Exists(LockFile)) { return; } byte[] CbObjectData; try { using(FileStream Stream = File.Open(LockFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) { CbObjectData = new byte[Stream.Length]; Stream.Read(CbObjectData); } } catch(IOException) { return; } if (CbObjectData == null) { return; } CbObject LockObject = new CbObject(CbObjectData); int PID = LockObject["pid"].AsInt32(); int ServerPort = LockObject["port"].AsInt32(); IsValid = PID > 0 && (ServerPort > 0 && ServerPort <= 0xffff); if (IsValid) { HostPort = ServerPort; HostName = "localhost"; // for now only support localhost } } /// /// Queries the server's health status to check if the the service is running /// /// public bool IsServiceRunning() { if (!IsValid) { return false; } string Uri = string.Format("http://{0}:{1}/health/ready", HostName, HostPort); using var Request = new HttpRequestMessage(HttpMethod.Get, Uri); HttpResponseMessage HttpGetResult = null; HttpClient Client = new HttpClient(); try { HttpGetResult = Client.Send(Request); } catch { return false; } if (HttpGetResult.IsSuccessStatusCode) { return true; } return false; } /// /// Checks if the server was run with --workspaces-enabled /// /// public bool DoesServerSupportWorkspaces() { if (CommandLineArguments.Contains("--workspaces-enabled")) { return true; } return false; } /// /// If there doesn't exists a workspace pointing to BaseDir (or above in the subtree) will create a new zen workspace pointing to BaseDir. /// If a workspace already exists it won't change its Dynamic setting. /// /// /// If the newly created workspace should allow connecting clients to create workspace shares on demand /// ZenWorkspace if the workspace was created or one already exists. null on failure public ZenWorkspace GetOrCreateWorkspace(DirectoryReference BaseDir, bool Dynamic = false) { if (!DoesServerSupportWorkspaces()) { return null; } if (!DirectoryReference.Exists(BaseDir)) { Logger.LogError("Can't create zen workspace in {0}. Directory doesn't exist", BaseDir.FullName); } ZenWorkspace Workspace = FindWorkspaceForDir(BaseDir); if (Workspace != null) { return Workspace; } string Command = String.Format("workspace create {0}", BaseDir.FullName); if (Dynamic) { Command += " --allow-share-create-from-http"; } if (!ZenUtils.RunZenExe(Command)) { return null; } Workspace = FindWorkspaceForDir(BaseDir); return Workspace; } /// /// If there doesn't exists a workspace share pointing to ShareDir, will create a new zen workspace share pointing to it. /// /// /// public ZenWorkspaceShare GetOrCreateShare(DirectoryReference ShareDir) { if (!DoesServerSupportWorkspaces()) { return null; } if (!DirectoryReference.Exists(ShareDir)) { Logger.LogError("Can't create zen share in {0}. Directory doesn't exist", ShareDir); return null; } ZenWorkspaceShare Result = FindShareForDir(ShareDir); if (Result != null) { return Result; } ZenWorkspace Workspace = FindWorkspaceForDir(ShareDir); if (Workspace == null) { Logger.LogError("Can't create zen share in {0}. No zen workspace exists for this directory", ShareDir.FullName); return null; } DirectoryReference WorkspaceRef = new(Workspace.root_path); // don't want to have a share pointing to a workspace if (WorkspaceRef == ShareDir) { Logger.LogError("Can't create zen share in {0}. There already exists a workspace pointing to this directory.", ShareDir.FullName); return null; } string ShareRelPath = ShareDir.MakeRelativeTo(WorkspaceRef); string Command = String.Format("workspace-share create {0} \"{1}\"", Workspace.id, ShareRelPath); if (!ZenUtils.RunZenExe(Command)) { return null; } Result = FindShareForDir(ShareDir); return Result; } /// /// Queries the server for a full list of existing workspaces /// /// private List GetWorkspaceList() { if (!IsServiceRunning() || !DoesServerSupportWorkspaces()) { return new List(); } HttpResponseMessage HttpGetResult = null; HttpClient Client = new HttpClient(); string Uri = string.Format("http://{0}:{1}/ws", HostName, HostPort); using var Request = new HttpRequestMessage(HttpMethod.Get, Uri); Request.Headers.Add("Accept", "application/json"); try { HttpGetResult = Client.Send(Request); } catch { return new List(); } if (!HttpGetResult.IsSuccessStatusCode) { return new List(); } Task ResultContent = HttpGetResult.Content.ReadAsStringAsync(); ResultContent.Wait(); JsonSerializerOptions JsonOptions = new(); JsonOptions.AllowTrailingCommas = true; JsonOptions.PropertyNameCaseInsensitive = true; JsonOptions.ReadCommentHandling = JsonCommentHandling.Skip; JsonNode WorkspaceJsonObject = JsonNode.Parse(ResultContent.Result); JsonArray WorkspaceArray = WorkspaceJsonObject["workspaces"]!.AsArray(); List Result = JsonSerializer.Deserialize>(WorkspaceArray, JsonOptions); return Result; } /// /// Finds a zen workspace for the specified directory. Includes workspaces that are above in the directory tree /// /// /// ZenWorkspace on success. null if there's no workspace for in this directory tree public ZenWorkspace FindWorkspaceForDir(DirectoryReference BaseDir) { var Workspaces = GetWorkspaceList(); if (Workspaces == null) { return null; } foreach (ZenWorkspace Workspace in Workspaces) { if (BaseDir.IsUnderDirectory(new DirectoryReference(Workspace.root_path))) { return Workspace; } } return null; } /// /// Finds a zen workspace share for the specified directory. /// /// /// ZenWorkspaceShare if the share exists. null either if there's no share or there exists no workspace in this dir subtree. public ZenWorkspaceShare FindShareForDir(DirectoryReference ShareDir) { ZenWorkspace Workspace = FindWorkspaceForDir(ShareDir); if (Workspace == null || Workspace.shares == null) { return null; } string RelShare = ShareDir.MakeRelativeTo(new DirectoryReference(Workspace.root_path)); foreach (ZenWorkspaceShare ZenShare in Workspace.shares) { if (ZenShare.share_path == RelShare) { return ZenShare; } } return null; } /// /// Creates a zen workspace share pointing to ShareDir. If necessary it creates a zen workspace in the parent directory. /// ShareDir must not be a root directory, e.g. d:\ /// /// /// /// ZenWorkspace share on success. /// null if the directory doesn't exist or zen server failed to create the workspace /// public ZenWorkspaceShare CreateWorkspaceAndShare(DirectoryReference ShareDir) { if (!DoesServerSupportWorkspaces()) { return null; } if (!DirectoryReference.Exists(ShareDir)) { Logger.LogError("Can't create workspace and share in {0}. Directory doesn't exist", ShareDir.FullName); return null; } if (ShareDir.IsRootDirectory()) { Logger.LogError("Can't create share in {0}. Share can't be a root directory", ShareDir); return null; } ZenWorkspaceShare Share = FindShareForDir(ShareDir); if (Share != null) { return Share; } ZenWorkspace Workspace = GetOrCreateWorkspace(ShareDir.ParentDirectory); if (Workspace == null) { Logger.LogError("Can't create workspace in share's parent directory {0}", ShareDir.ParentDirectory); return null; } Share = GetOrCreateShare(ShareDir); if (Share == null) { Logger.LogError("Create zen share in {0} failed", ShareDir); return null; } return Share; } } public static class ZenUtils { private static string GetServerExecutableName() { string ExecutableName = "zenserver"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { ExecutableName += ".exe"; } return ExecutableName; } public static string GetZenInstallPath() { const string EpicProductIdentifier = "UnrealEngine"; string LocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); return Path.Combine(LocalAppData, EpicProductIdentifier, "Common", "Zen", "Install"); } public static string GetZenRunContextFile() { return Path.Combine(GetZenInstallPath(), "zenserver.runcontext"); } public static FileReference GetZenExeLocation(string ExecutableName) { string PathString = String.Format("{0}/Binaries/{1}/{2}{3}", Unreal.EngineDirectory, HostPlatform.Current.HostEditorPlatform.ToString(), ExecutableName, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""); if (Path.IsPathRooted(PathString)) { return new FileReference(PathString); } else { return new FileReference(Path.Combine(CommandUtils.CmdEnv.LocalRoot, PathString)); } } public static bool IsLocalZenServerRunning() { FileReference ContextFileRef = new(GetZenRunContextFile()); if (!FileReference.Exists(ContextFileRef)) { return false; } ZenRunContext Context = ZenRunContext.ReadFromContextFile(ContextFileRef); if (Context == null) { return false; } return Context.IsServiceRunning(); } public static JsonSerializerOptions GetDefaultJsonSerializerOptions() { JsonSerializerOptions Options = new(); Options.AllowTrailingCommas = true; Options.PropertyNameCaseInsensitive = true; Options.ReadCommentHandling = JsonCommentHandling.Skip; return Options; } public static void ZenLaunch(FileReference SponsorFile) { FileReference ZenLaunchExe = GetZenExeLocation("ZenLaunch"); string LaunchArguments = String.Format("{0} -SponsorProcessID={1}", CommandUtils.MakePathSafeToUseWithCommandLine(SponsorFile.FullName), Environment.ProcessId); CommandUtils.RunAndLog(CommandUtils.CmdEnv, ZenLaunchExe.FullName, LaunchArguments, Options: CommandUtils.ERunOptions.Default); } public static bool RunZenExe(string Command) { FileReference ZenExe = GetZenExeLocation("zen"); try { CommandUtils.RunAndLog(ZenExe.FullName, Command); } catch { Log.Logger.LogError("{0} failed to run", ZenExe.FullName); return false; } return true; } public static bool ShouldSetupPakStreaming(ProjectParams Params, DeploymentContext SC) { if (CommandUtils.IsBuildMachine) { return false; } if (!Params.UsePak(SC.StageTargetPlatform)) { return false; } ConfigHierarchy EngineConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, DirectoryReference.FromFile(Params.RawProjectPath), BuildHostPlatform.Current.Platform, SC.CustomConfig); EngineConfig.GetBool("/Script/UnrealEd.ProjectPackagingSettings", "bUseZenStore", out bool UseZenStore); EngineConfig.GetBool("/Script/UnrealEd.ProjectPackagingSettings", "bEnablePakStreaming", out bool EnablePakStreaming); return UseZenStore && EnablePakStreaming; } } }