// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; 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.Horde; using EpicGames.ProjectStore; using EpicGames.Serialization; using UnrealBuildTool; using Microsoft.Extensions.Logging; namespace AutomationTool.Tasks { /// /// Enumeration of different storage options for snapshots. /// public enum SnapshotStorageType { /// /// A reserved non-valid storage type for snapshots. /// Invalid, /// /// Snapshot stored in cloud repository such as Unreal Cloud DDC. /// Cloud, /// /// Snapshot stored in builds repository such as Unreal Cloud DDC. /// Builds, /// /// Snapshot stored in a zenserver. /// Zen, /// /// Snapshot stored as a file on disk. /// File, } /// /// Parameters for a task that exports an snapshot from ZenServer /// public class ZenExportSnapshotTaskParameters { /// /// The project from which to export the snapshot /// [TaskParameter(Optional = true)] public FileReference Project { get; set; } /// /// The target platform(s) to export the snapshot for /// [TaskParameter(Optional = true)] public string Platform { get; set; } /// /// The metadata to associate with the snapshot /// [TaskParameter(Optional = true)] public string Metadata { get; set; } /// /// A file to read with information about the snapshot that should be used as a base when exporting this new snapshot /// [TaskParameter(Optional = true)] public FileReference SnapshotBaseDescriptorFile { get; set; } /// /// A file to create with information about the snapshot that was exported /// [TaskParameter(Optional = true)] public FileReference SnapshotDescriptorFile { get; set; } /// /// The type of destination to export the snapshot to (cloud, ...) /// [TaskParameter] public string DestinationStorageType { get; set; } /// /// The identifier to use when exporting to a destination /// [TaskParameter(Optional = true)] public string DestinationIdentifier { get; set; } /// /// The host name to use when exporting to a cloud destination /// [TaskParameter(Optional = true)] public string DestinationCloudHost { get; set; } /// /// The host name to use when writing a snapshot descriptor for a cloud destination /// [TaskParameter(Optional = true)] public string SnapshotDescriptorCloudHost { get; set; } /// /// The target platform to use when writing a snapshot descriptor /// [TaskParameter(Optional = true)] public string SnapshotDescriptorPlatform { get; set; } /// /// The http version to use when exporting to a cloud destination /// [TaskParameter(Optional = true)] public string DestinationCloudHttpVersion { get; set; } /// /// The http version to use when writing a snapshot descriptor for a cloud destination /// [TaskParameter(Optional = true)] public string SnapshotDescriptorCloudHttpVersion { get; set; } /// /// The namespace to use when exporting to a cloud destination /// [TaskParameter(Optional = true)] public string DestinationCloudNamespace { get; set; } /// /// A custom bucket name to use when exporting to a cloud destination /// [TaskParameter(Optional = true)] public string DestinationCloudBucket { get; set; } /// /// The host name to use when exporting to a zen destination /// [TaskParameter(Optional = true)] public string DestinationZenHost { get; set; } /// /// The directory to use when exporting to a file destination /// [TaskParameter(Optional = true)] public DirectoryReference DestinationFileDir { get; set; } /// /// The filename to use when exporting to a file destination /// [TaskParameter(Optional = true)] public string DestinationFileName { get; set; } /// /// Optional. Where to look for the ue.projectstore /// The pattern {Platform} can be used for exporting multiple platforms at once. /// [TaskParameter(Optional = true)] public string OverridePlatformCookedDir { get; set; } /// /// Optional. Whether to force export of data even if the destination claims to have them. /// [TaskParameter(Optional = true)] public bool ForceExport { get; set; } = false; /// /// Optional. Whether to entirely bypass the exporting of data and write a snapshot descriptor as if the data had been exported. /// [TaskParameter(Optional = true)] public bool SkipExport { get; set; } = false; } /// /// Exports an snapshot from Zen to a specified destination. /// [TaskElement("ZenExportSnapshot", typeof(ZenExportSnapshotTaskParameters))] public class ZenExportSnapshotTask : BgTaskImpl { /// /// 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 snapshots, the host they are stored on. /// public string Host { 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 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; } } private class ExportSourceData { public bool _isLocalHost; public string _hostName; public int _hostPort; public string _projectId; public string _oplogId; public string _targetPlatform; public SnapshotDescriptor _snapshotBaseDescriptor; } /// /// Parameters for the task /// readonly ZenExportSnapshotTaskParameters _parameters; /// /// Constructor. /// /// Parameters for this task public ZenExportSnapshotTask(ZenExportSnapshotTaskParameters parameters) { _parameters = parameters; } /// /// Gets the assumed path to where Zen should exist /// /// public static FileReference ZenExeFileReference() { return ResolveFile(String.Format("Engine/Binaries/{0}/zen{1}", HostPlatform.Current.HostEditorPlatform.ToString(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")); } /// /// Ensures that ZenServer is running on this current machine. This is needed before running any oplog commands /// This passes the sponsor'd process Id to launch zen. /// This ensures that zen does not live longer than the lifetime of a particular a process that needs Zen to be running /// /// public static void ZenLaunch(FileReference projectFile) { // Get the ZenLaunch executable path 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); } static JsonSerializerOptions GetDefaultJsonSerializerOptions() { JsonSerializerOptions options = new JsonSerializerOptions(); options.AllowTrailingCommas = true; options.ReadCommentHandling = JsonCommentHandling.Skip; options.PropertyNameCaseInsensitive = true; options.Converters.Add(new JsonStringEnumConverter()); return options; } #nullable enable static bool TryLoadJson(FileReference file, [NotNullWhen(true)] out T? obj) where T : class { if (!FileReference.Exists(file)) { obj = null; return false; } try { obj = LoadJson(file); return true; } catch (Exception) { obj = null; return false; } } static T LoadJson(FileReference file) { byte[] data = FileReference.ReadAllBytes(file); return JsonSerializer.Deserialize(data, GetDefaultJsonSerializerOptions())!; } static string SanitizeOplogName(string name) { return name.Replace('/', '_').Replace(' ', '_').Replace('+', '_').Replace('-', '_'); } private string GetCloudBucketName() { string bucketName = _parameters.DestinationCloudBucket; string projectNameAsBucketName = _parameters.Project.GetFileNameWithoutAnyExtensions().ToLowerInvariant(); if (String.IsNullOrEmpty(bucketName)) { bucketName = projectNameAsBucketName; } bucketName = SanitizeBucketName(bucketName); return bucketName; } private void WriteExportSource(JsonWriter writer, SnapshotStorageType destinationStorageType, ExportSourceData exportSource, string name, string destinationId) { string targetPlatform = _parameters.SnapshotDescriptorPlatform; if (String.IsNullOrEmpty(targetPlatform)) { targetPlatform = exportSource._targetPlatform; } writer.WriteObjectStart(); switch (destinationStorageType) { case SnapshotStorageType.Builds: case SnapshotStorageType.Cloud: string bucketName = GetCloudBucketName(); string hostName = _parameters.SnapshotDescriptorCloudHost; if (String.IsNullOrEmpty(hostName)) { hostName = _parameters.DestinationCloudHost; } string httpVersion = _parameters.SnapshotDescriptorCloudHttpVersion; if (String.IsNullOrEmpty(httpVersion)) { httpVersion = _parameters.DestinationCloudHttpVersion; } string storageTypeName = "cloud"; string storageIdentifierName = "key"; if (destinationStorageType == SnapshotStorageType.Builds) { storageTypeName = "builds"; storageIdentifierName = "builds-id"; } writer.WriteValue("name", name); writer.WriteValue("type", storageTypeName); writer.WriteValue("targetplatform", targetPlatform); writer.WriteValue("host", hostName); if (!String.IsNullOrEmpty(httpVersion) && !httpVersion.Equals("None", StringComparison.OrdinalIgnoreCase)) { writer.WriteValue("httpversion", httpVersion); } writer.WriteValue("namespace", _parameters.DestinationCloudNamespace); writer.WriteValue("bucket", bucketName); writer.WriteValue(storageIdentifierName, destinationId); break; case SnapshotStorageType.Zen: string projectName = _parameters.Project.GetFileNameWithoutAnyExtensions().ToLowerInvariant() + ".oplog"; writer.WriteValue("name", name); writer.WriteValue("type", "zen"); writer.WriteValue("targetplatform", targetPlatform); writer.WriteValue("host", _parameters.DestinationZenHost); writer.WriteValue("projectid", projectName); writer.WriteValue("oplogid", SanitizeOplogName(name)); break; case SnapshotStorageType.File: writer.WriteValue("name", name); writer.WriteValue("type", "file"); writer.WriteValue("targetplatform", targetPlatform); writer.WriteValue("directory", _parameters.DestinationFileDir.FullName); writer.WriteValue("filename", _parameters.DestinationFileName); break; } writer.WriteObjectEnd(); } private static bool TryRunAndLogWithoutSpew(string app, string commandLine, bool ignoreFailure) { 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) { if (!ignoreFailure) { Logger.LogWarning("{Text}", e.ToString()); } return false; } return true; } private static bool TryExportOplogCommand(string app, string commandLine) { int attemptLimit = 2; int attempt = 0; while (attempt < attemptLimit) { if (TryRunAndLogWithoutSpew(app, commandLine, false)) { return true; } Logger.LogWarning("Attempt {AttemptNum} of exporting the oplog failed, {Action}...", attempt + 1, attempt < (attemptLimit - 1) ? "retrying" : "abandoning"); attempt = attempt + 1; } return false; } private static string SanitizeBucketName(string inString) { return StringId.Sanitize(inString).ToString(); } /// /// 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) { SnapshotStorageType destinationStorageType = SnapshotStorageType.Invalid; if (!String.IsNullOrEmpty(_parameters.DestinationStorageType)) { destinationStorageType = (SnapshotStorageType)Enum.Parse(typeof(SnapshotStorageType), _parameters.DestinationStorageType); } FileReference projectFile = _parameters.Project; if (!FileReference.Exists(projectFile)) { throw new AutomationException("Missing project file - {0}", projectFile.FullName); } ZenLaunch(projectFile); List exportSources = new List(); foreach (string platform in _parameters.Platform.Split('+')) { DirectoryReference platformCookedDirectory; if (String.IsNullOrEmpty(_parameters.OverridePlatformCookedDir)) { platformCookedDirectory = DirectoryReference.Combine(projectFile.Directory, "Saved", "Cooked", platform); } else { platformCookedDirectory = new DirectoryReference(_parameters.OverridePlatformCookedDir.Replace("{Platform}", platform, StringComparison.InvariantCultureIgnoreCase)); } if (!DirectoryReference.Exists(platformCookedDirectory)) { throw new AutomationException("Cook output directory not found ({0})", platformCookedDirectory.FullName); } FileReference projectStoreFile = FileReference.Combine(platformCookedDirectory, "ue.projectstore"); ProjectStoreData? parsedProjectStore = null; if (TryLoadJson(projectStoreFile, out parsedProjectStore) && (parsedProjectStore != null) && (parsedProjectStore.ZenServer != null)) { ExportSourceData newExportSource = new ExportSourceData(); newExportSource._isLocalHost = parsedProjectStore.ZenServer.IsLocalHost; newExportSource._hostName = parsedProjectStore.ZenServer.HostName; newExportSource._hostPort = parsedProjectStore.ZenServer.HostPort; newExportSource._projectId = parsedProjectStore.ZenServer.ProjectId; newExportSource._oplogId = parsedProjectStore.ZenServer.OplogId; newExportSource._targetPlatform = platform; newExportSource._snapshotBaseDescriptor = null; if (_parameters.SnapshotBaseDescriptorFile != null) { FileReference platformSnapshotBase = new FileReference(_parameters.SnapshotBaseDescriptorFile.FullName.Replace("{Platform}", platform, StringComparison.InvariantCultureIgnoreCase)); SnapshotDescriptorCollection? parsedDescriptorCollection = null; if (TryLoadJson(platformSnapshotBase, out parsedDescriptorCollection) && (parsedDescriptorCollection != null) && (parsedDescriptorCollection.Snapshots != null)) { foreach (SnapshotDescriptor parsedDescriptor in parsedDescriptorCollection.Snapshots) { if (parsedDescriptor.TargetPlatform == platform) { newExportSource._snapshotBaseDescriptor = parsedDescriptor; break; } } } } exportSources.Add(newExportSource); } } int exportIndex; string[] exportNames = new string[exportSources.Count]; string[] exportIds = new string[exportSources.Count]; List successfullyExportedSources = new List(); // Get the Zen executable path FileReference zenExe = ZenExeFileReference(); // Format the command line StringBuilder oplogExportCommandline = new StringBuilder(); oplogExportCommandline.Append("oplog-export --embedloosefiles"); if (_parameters.ForceExport) { oplogExportCommandline.Append(" --force"); } switch (destinationStorageType) { case SnapshotStorageType.Builds: case SnapshotStorageType.Cloud: ProjectProperties properties = ProjectUtils.GetProjectProperties(_parameters.Project); ConfigHierarchy config = properties.EngineConfigs[HostPlatform.Current.HostEditorPlatform]; bool foundConfig = config.TryGetValueGeneric("StorageServers", "Cloud", out CloudConfiguration cloudConfig); if (String.IsNullOrEmpty(_parameters.DestinationCloudHost)) { if (!foundConfig || String.IsNullOrEmpty(cloudConfig.Host)) { throw new AutomationException("Missing destination cloud host"); } _parameters.DestinationCloudHost = cloudConfig.Host.Split(";")[0]; } if (String.IsNullOrEmpty(_parameters.DestinationCloudNamespace)) { if (!foundConfig || String.IsNullOrEmpty(cloudConfig.BuildsNamespace)) { throw new AutomationException(String.Format("Missing destination cloud namespace {0}", cloudConfig.BuildsNamespace)); } _parameters.DestinationCloudNamespace = cloudConfig.BuildsNamespace; } if (String.IsNullOrEmpty(_parameters.DestinationIdentifier)) { throw new AutomationException("Missing destination identifier when exporting to cloud"); } string bucketName = GetCloudBucketName(); string storageTypeName = "cloud"; string storageIdentifierName = "key"; if (destinationStorageType == SnapshotStorageType.Builds) { storageTypeName = "builds"; storageIdentifierName = "builds-id"; } oplogExportCommandline.AppendFormat(" --{0} {1} --namespace {2} --bucket {3}", storageTypeName, _parameters.DestinationCloudHost, _parameters.DestinationCloudNamespace, bucketName); if (!String.IsNullOrEmpty(_parameters.DestinationCloudHttpVersion)) { if (_parameters.DestinationCloudHttpVersion.Equals("http2-only", StringComparison.OrdinalIgnoreCase)) { oplogExportCommandline.Append(" --assume-http2"); } else { throw new AutomationException("Unexpected destination cloud http version"); } } exportIndex = 0; foreach (ExportSourceData exportSource in exportSources) { string hostUrlArg = String.Format("--hosturl http://{0}:{1}", exportSource._isLocalHost ? "localhost" : exportSource._hostName, exportSource._hostPort); string baseKeyArg = String.Empty; if ((destinationStorageType == SnapshotStorageType.Cloud) && (exportSource._snapshotBaseDescriptor != null) && !String.IsNullOrEmpty(exportSource._snapshotBaseDescriptor.Key)) { if (exportSource._snapshotBaseDescriptor.Type == SnapshotStorageType.Cloud) { baseKeyArg = " --basekey " + exportSource._snapshotBaseDescriptor.Key; } else { Logger.LogWarning("Base snapshot descriptor was for a snapshot storage type {Type}, but we're producing a snapshot of type cloud. Skipping use of base snapshot.", exportSource._snapshotBaseDescriptor.Type); } } StringBuilder exportSingleSourceCommandline = new StringBuilder(oplogExportCommandline.Length); exportSingleSourceCommandline.Append(oplogExportCommandline); StringBuilder destinationNameBuilder = new StringBuilder(); destinationNameBuilder.AppendFormat("{0}.{1}.{2}", bucketName, _parameters.DestinationIdentifier, exportSource._oplogId); exportNames[exportIndex] = destinationNameBuilder.ToString().ToLowerInvariant(); string destinationId; if (destinationStorageType == SnapshotStorageType.Builds) { StringBuilder metadata = new StringBuilder(); metadata.AppendFormat("type=oplog;createdAt={0}", DateTime.UtcNow.ToString("O")); // Add keys for the job that's executing string? stepId = null; string? jobId = Environment.GetEnvironmentVariable("UE_HORDE_JOBID"); if (!String.IsNullOrEmpty(jobId)) { metadata.AppendFormat(";job={0}", jobId); stepId = Environment.GetEnvironmentVariable("UE_HORDE_STEPID"); if (!String.IsNullOrEmpty(stepId)) { metadata.AppendFormat(";step={0}", stepId); } } string? hordeUrl = Environment.GetEnvironmentVariable("UE_HORDE_URL"); // if we are running in horde and have the required environment variables we append a link back to the horde job into metadata if (!String.IsNullOrEmpty(hordeUrl) && !String.IsNullOrEmpty(jobId) && !String.IsNullOrEmpty(stepId)) { metadata.Append($";buildurl={hordeUrl}job/{jobId}?step={stepId}"); } if (!String.IsNullOrEmpty(_parameters.Metadata)) { metadata.AppendFormat(";{0}", _parameters.Metadata); } exportSingleSourceCommandline.AppendFormat(" --builds-metadata \"{0}\"", metadata.ToString()); CbObjectId objectId = CbObjectId.NewObjectId(); destinationId = objectId.ToString().ToLowerInvariant(); } else { IoHash destinationKeyHash = IoHash.Compute(Encoding.UTF8.GetBytes(exportNames[exportIndex])); destinationId = destinationKeyHash.ToString().ToLowerInvariant(); } exportIds[exportIndex] = destinationId; exportSingleSourceCommandline.AppendFormat(" {0} --{1} {2} {3} {4} {5}", hostUrlArg, storageIdentifierName, destinationId, baseKeyArg, exportSource._projectId, exportSource._oplogId); if (_parameters.SkipExport || TryExportOplogCommand(zenExe.FullName, exportSingleSourceCommandline.ToString())) { successfullyExportedSources.Add(exportSource); } exportIndex = exportIndex + 1; } break; case SnapshotStorageType.Zen: if (String.IsNullOrEmpty(_parameters.DestinationZenHost)) { throw new AutomationException("Missing destination zen host"); } if (String.IsNullOrEmpty(_parameters.DestinationIdentifier)) { throw new AutomationException("Missing destination identifier when exporting to zen"); } string projectName = projectFile.GetFileNameWithoutAnyExtensions().ToLowerInvariant() + ".oplog"; StringBuilder createProjectCommandline = new StringBuilder(); createProjectCommandline.AppendFormat("project-create --hosturl {0} {1}", _parameters.DestinationZenHost, projectName); TryRunAndLogWithoutSpew(zenExe.FullName, createProjectCommandline.ToString(), true); oplogExportCommandline.AppendFormat(" --zen {0}", _parameters.DestinationZenHost); exportIndex = 0; foreach (ExportSourceData exportSource in exportSources) { string hostUrlArg = String.Format("--hosturl http://{0}:{1}", exportSource._isLocalHost ? "localhost" : exportSource._hostName, exportSource._hostPort); StringBuilder exportSingleSourceCommandline = new StringBuilder(oplogExportCommandline.Length); exportSingleSourceCommandline.Append(oplogExportCommandline); StringBuilder destinationKeyBuilder = new StringBuilder(); destinationKeyBuilder.AppendFormat("{0}.{1}", _parameters.DestinationIdentifier, exportSource._oplogId); exportNames[exportIndex] = destinationKeyBuilder.ToString().ToLowerInvariant(); string destinationOplog = SanitizeOplogName(exportNames[exportIndex]); exportSingleSourceCommandline.AppendFormat(" {0} --target-project {1} --target-oplog {2} {3} {4}", hostUrlArg, projectName, destinationOplog, exportSource._projectId, exportSource._oplogId); if (_parameters.SkipExport || TryExportOplogCommand(zenExe.FullName, exportSingleSourceCommandline.ToString())) { successfullyExportedSources.Add(exportSource); } exportIndex = exportIndex + 1; } break; case SnapshotStorageType.File: string defaultProjectId = ProjectUtils.GetProjectPathId(projectFile); exportIndex = 0; foreach (ExportSourceData exportSource in exportSources) { StringBuilder exportNameBuilder = new StringBuilder(); exportNameBuilder.AppendFormat("{0}.{1}.{2}", projectFile.GetFileNameWithoutAnyExtensions().ToLowerInvariant(), _parameters.DestinationIdentifier, exportSource._oplogId); exportNames[exportIndex] = exportNameBuilder.ToString().ToLowerInvariant(); StringBuilder exportSingleSourceCommandline = new StringBuilder(oplogExportCommandline.Length); exportSingleSourceCommandline.Append(oplogExportCommandline); string destinationFileName = exportSource._oplogId; if (!String.IsNullOrEmpty(_parameters.DestinationFileName)) { destinationFileName = _parameters.DestinationFileName.Replace("{Platform}", exportSource._targetPlatform, StringComparison.InvariantCultureIgnoreCase); } string projectId = String.IsNullOrEmpty(exportSource._projectId) ? defaultProjectId : exportSource._projectId; string baseNameArg = String.Empty; DirectoryReference platformDestinationFileDir = new DirectoryReference(_parameters.DestinationFileDir.FullName.Replace("{Platform}", exportSource._targetPlatform, StringComparison.InvariantCultureIgnoreCase)); if ((exportSource._snapshotBaseDescriptor != null) && !String.IsNullOrEmpty(exportSource._snapshotBaseDescriptor.Directory) && !String.IsNullOrEmpty(exportSource._snapshotBaseDescriptor.Filename)) { if (exportSource._snapshotBaseDescriptor.Type == SnapshotStorageType.File) { FileReference baseSnapshotFile = new FileReference(Path.Combine(exportSource._snapshotBaseDescriptor.Directory, exportSource._snapshotBaseDescriptor.Filename)); if (FileReference.Exists(baseSnapshotFile)) { baseNameArg = " --basename " + CommandUtils.MakePathSafeToUseWithCommandLine(baseSnapshotFile.FullName); } else { Logger.LogWarning("Base snapshot descriptor missing. Skipping use of base snapshot."); } } else { Logger.LogWarning("Base snapshot descriptor was for a snapshot storage type {Type}, but we're producing a snapshot of type file. Skipping use of base snapshot.", exportSource._snapshotBaseDescriptor.Type); } } exportSingleSourceCommandline.AppendFormat(" --file {0} --name {1} {2} {3} {4}", CommandUtils.MakePathSafeToUseWithCommandLine(platformDestinationFileDir.FullName), destinationFileName, baseNameArg, projectId, exportSource._oplogId); if (_parameters.SkipExport || TryExportOplogCommand(zenExe.FullName, exportSingleSourceCommandline.ToString())) { successfullyExportedSources.Add(exportSource); } exportIndex = exportIndex + 1; } break; default: throw new AutomationException("Unknown/invalid/unimplemented destination storage type - {0}", _parameters.DestinationStorageType); } if ((_parameters.SnapshotDescriptorFile != null) && successfullyExportedSources.Any()) { if (_parameters.SnapshotDescriptorFile.FullName.Contains("{Platform}", StringComparison.OrdinalIgnoreCase)) { // Separate descriptor file per platform exportIndex = 0; foreach (ExportSourceData exportSource in successfullyExportedSources) { FileReference platformSnapshotDescriptorFile = new FileReference(_parameters.SnapshotDescriptorFile.FullName.Replace("{Platform}", exportSource._targetPlatform, StringComparison.InvariantCultureIgnoreCase)); DirectoryReference.CreateDirectory(platformSnapshotDescriptorFile.Directory); using (JsonWriter writer = new JsonWriter(platformSnapshotDescriptorFile)) { writer.WriteObjectStart(); writer.WriteArrayStart("snapshots"); WriteExportSource(writer, destinationStorageType, exportSource, exportNames[exportIndex], exportIds[exportIndex]); writer.WriteArrayEnd(); writer.WriteObjectEnd(); } exportIndex = exportIndex + 1; } } else { // Write out a single snapshot descriptor with info about all snapshots DirectoryReference.CreateDirectory(_parameters.SnapshotDescriptorFile.Directory); using (JsonWriter writer = new JsonWriter(_parameters.SnapshotDescriptorFile)) { writer.WriteObjectStart(); writer.WriteArrayStart("snapshots"); exportIndex = 0; foreach (ExportSourceData exportSource in successfullyExportedSources) { WriteExportSource(writer, destinationStorageType, exportSource, exportNames[exportIndex], exportIds[exportIndex]); exportIndex = exportIndex + 1; } writer.WriteArrayEnd(); writer.WriteObjectEnd(); } } } return Task.CompletedTask; } /// /// 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; } } struct CloudConfiguration { #pragma warning disable IDE1006 // Static analysis wants these to be named differently, but they must be named the same as the config file properties public string Host = ""; public string BuildsNamespace = ""; public string BuildsBaselineBranch = ""; #pragma warning restore IDE1006 public CloudConfiguration() {} } }