// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Xml;
using EpicGames.Core;
using EpicGames.ProjectStore;
using UnrealBuildBase;
namespace AutomationTool.Tasks
{
///
/// Parameters for a task that imports snapshots into ZenServer
///
public class ZenImportSnapshotTaskParameters
{
///
/// The project into which snapshots should be imported
///
[TaskParameter]
public FileReference Project { get; set; }
///
/// A file to read with information about the snapshots to be imported
///
[TaskParameter(Optional = true)]
public FileReference SnapshotDescriptorFile { get; set; }
///
/// A JSON blob with information about the snapshots to be imported
///
[TaskParameter(Optional = true)]
public string SnapshotDescriptorJSON { get; set; }
///
/// Optional. Where to look for the ue.projectstore file.
/// The pattern {Platform} can be used for importing multiple platforms at once.
///
[TaskParameter(Optional = true)]
public string OverridePlatformCookedDir { get; set; }
///
/// Optional. If true, force import of the oplog (corresponds to --force on the Zen command line).
///
[TaskParameter(Optional = true)]
public bool ForceImport { get; set; } = false;
///
/// Optional. If true, import oplogs asynchronously (corresponds to --async on the Zen command line).
///
[TaskParameter(Optional = true)]
public bool AsyncImport { get; set; } = false;
///
/// Optional. Remote zenserver host to import snapshots into.
///
[TaskParameter(Optional = true)]
public string RemoteZenHost { get; set; }
///
/// Optional. Port on remote host which zenserver is listening on.
///
[TaskParameter(Optional = true)]
public int RemoteZenPort { get; set; } = 8558;
///
/// Optional. Destination project ID to import snapshots into.
///
[TaskParameter(Optional = true)]
public string DestinationProjectId { get; set; }
}
///
/// Imports a snapshot from a specified destination to Zen.
///
[TaskElement("ZenImportSnapshot", typeof(ZenImportSnapshotTaskParameters))]
public class ZenImportSnapshotTask : BgTaskImpl
{
///
/// Enumeration of different storage options for snapshots.
///
public enum SnapshotStorageType
{
///
/// A reserved non-valid storage type for snapshots.
///
Invalid,
///
/// Snapshot stored in cloud repositories such as Unreal Cloud DDC.
///
Cloud,
///
/// Snapshot stored in a zenserver.
///
Zen,
///
/// Snapshot stored as a file on disk.
///
File,
///
/// Snapshot stored in Unreal Cloud builds API.
///
Builds,
}
///
/// Metadata about a snapshot
///
class SnapshotDescriptor
{
///
/// Name of the snapshot
///
public string Name { get; set; }
///
/// Storage type used for the snapshot
///
public SnapshotStorageType Type { get; set; }
///
/// Target platform for this snapshot
///
public string TargetPlatform { get; set; }
///
/// For cloud or Zen snapshots, the host they are stored on.
///
public string Host { get; set; }
///
/// For Zen snapshots, the project ID to import from.
///
public string ProjectId { get; set; }
///
/// For Zen snapshots, the oplog ID to import from.
///
public string OplogId { get; set; }
///
/// For cloud snapshots, the namespace they are stored in.
///
public string Namespace { get; set; }
///
/// For cloud snapshots, the bucket they are stored in.
///
public string Bucket { get; set; }
///
/// For cloud snapshots, the key they are stored in.
///
public string Key { get; set; }
///
/// For builds snapshots, the builds ID that identifies them.
///
[JsonPropertyName("builds-id")]
public string BuildsId { get; set; }
///
/// For file snapshots, the directory it is stored in.
///
public string Directory { get; set; }
///
/// For file snapshots, the filename (not including path) that they are stored in.
///
public string Filename { get; set; }
}
///
/// A collection of one or more snapshot descriptors
///
class SnapshotDescriptorCollection
{
///
/// The list of snapshots contained within this collection.
///
public List Snapshots { get; set; }
}
///
/// Parameters for the task
///
readonly ZenImportSnapshotTaskParameters _parameters;
///
/// Constructor.
///
/// Parameters for this task.
public ZenImportSnapshotTask(ZenImportSnapshotTaskParameters parameters)
{
_parameters = parameters;
}
///
/// Output this task out to an XML writer.
///
public override void Write(XmlWriter writer)
{
Write(writer, _parameters);
}
///
/// Find all the tags which are used as inputs to this task
///
/// The tag names which are read by this task
public override IEnumerable FindConsumedTagNames()
{
yield break;
}
///
/// Find all the tags which are modified by this task
///
/// The tag names which are modified by this task
public override IEnumerable FindProducedTagNames()
{
yield break;
}
private static void RunAndLogWithoutSpew(string app, string commandLine)
{
ProcessResult.SpewFilterCallbackType silentOutputFilter = new ProcessResult.SpewFilterCallbackType(line =>
{
return null;
});
try
{
CommandUtils.RunAndLog(CommandUtils.CmdEnv, app, commandLine, MaxSuccessCode: 0, Options: CommandUtils.ERunOptions.Default, SpewFilterCallback: silentOutputFilter);
}
catch (CommandUtils.CommandFailedException e)
{
throw new AutomationException("Zen command failed: {0}", e.ToString());
}
}
private static FileReference ZenExeFileReference()
{
return ResolveFile(String.Format("Engine/Binaries/{0}/zen{1}", HostPlatform.Current.HostEditorPlatform.ToString(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""));
}
private static void ZenLaunch(FileReference projectFile)
{
FileReference zenLaunchExe = ResolveFile(String.Format("Engine/Binaries/{0}/ZenLaunch{1}", HostPlatform.Current.HostEditorPlatform.ToString(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""));
StringBuilder zenLaunchCommandline = new StringBuilder();
zenLaunchCommandline.AppendFormat("{0} -SponsorProcessID={1}", CommandUtils.MakePathSafeToUseWithCommandLine(projectFile.FullName), Environment.ProcessId);
CommandUtils.RunAndLog(CommandUtils.CmdEnv, zenLaunchExe.FullName, zenLaunchCommandline.ToString(), Options: CommandUtils.ERunOptions.Default);
}
private class LowerCaseNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name) => name.ToLower();
}
private static JsonSerializerOptions GetDefaultJsonSerializerOptions()
{
JsonSerializerOptions options = new JsonSerializerOptions();
options.AllowTrailingCommas = true;
options.ReadCommentHandling = JsonCommentHandling.Skip;
options.PropertyNameCaseInsensitive = true;
options.PropertyNamingPolicy = new LowerCaseNamingPolicy();
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
private static SnapshotDescriptorCollection GetDescriptors(ZenImportSnapshotTaskParameters parameters)
{
SnapshotDescriptorCollection descriptors = new SnapshotDescriptorCollection();
descriptors.Snapshots = new List();
if (parameters.SnapshotDescriptorFile == null && String.IsNullOrEmpty(parameters.SnapshotDescriptorJSON))
{
throw new AutomationException("Must provide one of either SnapshotDescriptorFile or SnapshotDescriptorJSON");
}
if (parameters.SnapshotDescriptorFile != null)
{
if (!FileReference.Exists(parameters.SnapshotDescriptorFile))
{
throw new AutomationException("Snapshot descriptor file {0} does not exist", parameters.SnapshotDescriptorFile.FullName);
}
try
{
byte[] data = FileReference.ReadAllBytes(parameters.SnapshotDescriptorFile);
SnapshotDescriptorCollection fileDescriptors = JsonSerializer.Deserialize(data, GetDefaultJsonSerializerOptions());
descriptors.Snapshots.AddRange(fileDescriptors.Snapshots);
}
catch (Exception)
{
throw new AutomationException("Failed to deserialize snapshot descriptors from file {0}", parameters.SnapshotDescriptorFile.FullName);
}
}
if (!String.IsNullOrEmpty(parameters.SnapshotDescriptorJSON))
{
try
{
SnapshotDescriptorCollection jsonDescriptors = JsonSerializer.Deserialize(parameters.SnapshotDescriptorJSON, GetDefaultJsonSerializerOptions());
descriptors.Snapshots.AddRange(jsonDescriptors.Snapshots);
}
catch (Exception)
{
throw new AutomationException("Failed to deserialize snapshot descriptors from property SnapshotDescriptorJSON");
}
}
return descriptors;
}
private FileReference WriteProjectStoreFile(string projectId, string platform, string host, int port)
{
DirectoryReference platformCookedDirectory;
if (String.IsNullOrEmpty(_parameters.OverridePlatformCookedDir))
{
platformCookedDirectory = DirectoryReference.Combine(_parameters.Project.Directory, "Saved", "Cooked", platform);
}
else
{
platformCookedDirectory = new DirectoryReference(_parameters.OverridePlatformCookedDir.Replace("{Platform}", platform, StringComparison.InvariantCultureIgnoreCase));
}
if (!DirectoryReference.Exists(platformCookedDirectory))
{
DirectoryReference.CreateDirectory(platformCookedDirectory);
}
FileReference projectStoreFile = new FileReference(Path.Combine(platformCookedDirectory.FullName, "ue.projectstore"));
ProjectStoreData data = new ProjectStoreData();
data.ZenServer = new ZenServerStoreData();
data.ZenServer.IsLocalHost = String.IsNullOrEmpty(host);
data.ZenServer.HostName = "[::1]";
data.ZenServer.HostPort = port;
data.ZenServer.ProjectId = projectId;
data.ZenServer.OplogId = platform;
if (!String.IsNullOrEmpty(host))
{
data.ZenServer.RemoteHostNames.Add(host);
}
else
{
data.ZenServer.RemoteHostNames.AddRange(GetHostAddresses());
}
string json = JsonSerializer.Serialize(data, GetDefaultJsonSerializerOptions());
FileReference.WriteAllText(projectStoreFile, json);
return projectStoreFile;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA5351", Justification = "Not using MD5 for cryptographically secure use case")]
private string GetProjectID()
{
using (System.Security.Cryptography.MD5 hasher = System.Security.Cryptography.MD5.Create())
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(_parameters.Project.FullName.Replace("\\", "/", StringComparison.InvariantCulture));
byte[] hashed = hasher.ComputeHash(bytes);
string hexString = Convert.ToHexString(hashed).ToLowerInvariant();
return _parameters.Project.GetFileNameWithoutAnyExtensions() + "." + hexString.Substring(0, 8);
}
}
private static string GetHostNameFQDN()
{
string hostName = Dns.GetHostName();
// take first hostname that contains '.'
if (!hostName.Contains('.', StringComparison.InvariantCulture))
{
IPAddress[] ipAddresses = Dns.GetHostAddresses("");
foreach (IPAddress ipAddress in ipAddresses)
{
string hostNameTemp = Dns.GetHostEntry(ipAddress).HostName;
if (hostNameTemp.Contains('.', StringComparison.InvariantCulture))
{
hostName = hostNameTemp;
break;
}
}
}
return hostName;
}
private static string[] GetHostAddresses()
{
return Dns.GetHostAddresses("", System.Net.Sockets.AddressFamily.InterNetwork)
.Select(addr => addr.ToString())
.Append($"hostname://{GetHostNameFQDN()}")
.ToArray();
}
///
/// ExecuteAsync the task.
///
/// Information about the current job
/// Set of build products produced by this node.
/// Mapping from tag names to the set of files they include
public override Task ExecuteAsync(JobContext job, HashSet buildProducts, Dictionary> tagNameToFileSet)
{
// Correct the casing of the case-insensitive path passed in to match the actual casing of the project file on disk.
_parameters.Project = FileReference.FindCorrectCase(_parameters.Project);
SnapshotDescriptorCollection descriptors = GetDescriptors(_parameters);
string host;
if (String.IsNullOrEmpty(_parameters.RemoteZenHost))
{
ZenLaunch(_parameters.Project);
host = "localhost:8558";
}
else
{
host = _parameters.RemoteZenHost + ":" + _parameters.RemoteZenPort.ToString();
}
string projectId = String.IsNullOrEmpty(_parameters.DestinationProjectId) ? GetProjectID() : _parameters.DestinationProjectId;
DirectoryReference rootDir = Unreal.RootDirectory;
DirectoryReference engineDir = new DirectoryReference(Path.Combine(Unreal.RootDirectory.FullName, "Engine"));
DirectoryReference projectDir = _parameters.Project.Directory;
StringBuilder projectCreateCommandLine = new StringBuilder();
projectCreateCommandLine.AppendFormat("project-create --force-update {0} {1} {2} {3} {4}", projectId, rootDir.FullName, engineDir.FullName, projectDir.FullName, _parameters.Project.FullName);
projectCreateCommandLine.AppendFormat(" --hosturl {0}", host);
FileReference zenExe = ZenExeFileReference();
RunAndLogWithoutSpew(zenExe.FullName, projectCreateCommandLine.ToString());
foreach (SnapshotDescriptor descriptor in descriptors.Snapshots)
{
FileReference projectStoreFile = WriteProjectStoreFile(projectId, descriptor.TargetPlatform, _parameters.RemoteZenHost, _parameters.RemoteZenPort);
string oplogName = descriptor.TargetPlatform;
StringBuilder oplogCreateCommandLine = new StringBuilder();
oplogCreateCommandLine.AppendFormat("oplog-create --force-update {0} {1}", projectId, oplogName);
oplogCreateCommandLine.AppendFormat(" --hosturl {0}", host);
StringBuilder oplogImportCommandLine = new StringBuilder();
oplogImportCommandLine.AppendFormat("oplog-import {0} {1} {2} --ignore-missing-attachments --clean", projectId, oplogName, projectStoreFile.FullName);
oplogImportCommandLine.AppendFormat(" --hosturl {0}", host);
if (_parameters.ForceImport)
{
oplogImportCommandLine.AppendFormat(" --force");
}
if (_parameters.AsyncImport)
{
oplogImportCommandLine.AppendFormat(" --async");
}
switch (descriptor.Type)
{
case SnapshotStorageType.Cloud:
oplogImportCommandLine.AppendFormat(" --cloud {0}", descriptor.Host);
oplogImportCommandLine.AppendFormat(" --namespace {0}", descriptor.Namespace);
oplogImportCommandLine.AppendFormat(" --bucket {0}", descriptor.Bucket);
oplogImportCommandLine.AppendFormat(" --key {0}", descriptor.Key);
oplogImportCommandLine.AppendFormat(" --assume-http2");
break;
case SnapshotStorageType.Zen:
oplogImportCommandLine.AppendFormat(" --zen {0}", descriptor.Host);
oplogImportCommandLine.AppendFormat(" --source-project {0}", descriptor.ProjectId);
oplogImportCommandLine.AppendFormat(" --source-oplog {0}", descriptor.OplogId);
break;
case SnapshotStorageType.File:
oplogImportCommandLine.AppendFormat(" --file {0}", descriptor.Directory);
oplogImportCommandLine.AppendFormat(" --name {0}", descriptor.Filename);
break;
case SnapshotStorageType.Builds:
oplogImportCommandLine.AppendFormat(" --builds {0}", descriptor.Host);
oplogImportCommandLine.AppendFormat(" --builds-id {0}", descriptor.BuildsId);
oplogImportCommandLine.AppendFormat(" --namespace {0}", descriptor.Namespace);
oplogImportCommandLine.AppendFormat(" --bucket {0}", descriptor.Bucket);
break;
default:
break;
}
RunAndLogWithoutSpew(zenExe.FullName, oplogCreateCommandLine.ToString());
RunAndLogWithoutSpew(zenExe.FullName, oplogImportCommandLine.ToString());
}
return Task.CompletedTask;
}
}
}