From 39c297fbee31b01d368d27589b72a7b2ed267a66 Mon Sep 17 00:00:00 2001 From: Aleksandr Levochkin <107044793+aleksandrlevochkin@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:57:22 +0200 Subject: [PATCH] Update tf.exe (#4955) * WIP * Refactoring * Refactor tool download code * Refactor + Add tests * Fix test * Change RetryOptions to record to be able to use default ToString override for logging --------- Co-authored-by: v-levockina Co-authored-by: Kirill Ivlev <102740624+kirill-ivlev@users.noreply.github.com> --- src/Agent.Plugins/TFCliManager.cs | 11 +- src/Agent.Plugins/TfsVCSourceProvider.cs | 3 +- src/Agent.Sdk/Knob/AgentKnobs.cs | 7 + src/Agent.Worker/Build/TFCommandManager.cs | 11 +- src/Agent.Worker/Build/TfsVCSourceProvider.cs | 7 +- .../Handlers/LegacyPowerShellHandler.cs | 8 +- src/Agent.Worker/JobExtension.cs | 5 + src/Agent.Worker/JobRunner.cs | 6 +- .../Release/ReleaseJobExtension.cs | 5 + src/Agent.Worker/TfManager.cs | 130 ++++++++++++++++++ .../Constants.cs | 4 + .../HostContext.cs | 12 ++ src/Misc/externals.sh | 4 +- src/Test/L0/ServiceInterfacesL0.cs | 3 +- src/Test/L0/TestHostContext.cs | 12 ++ src/Test/L0/Worker/TfManagerL0.cs | 119 ++++++++++++++++ 16 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 src/Agent.Worker/TfManager.cs create mode 100644 src/Test/L0/Worker/TfManagerL0.cs diff --git a/src/Agent.Plugins/TFCliManager.cs b/src/Agent.Plugins/TFCliManager.cs index 16e3475c22..cd9032967f 100644 --- a/src/Agent.Plugins/TFCliManager.cs +++ b/src/Agent.Plugins/TFCliManager.cs @@ -12,6 +12,7 @@ using System.Xml; using System.Security.Cryptography.X509Certificates; using Microsoft.VisualStudio.Services.Agent.Util; +using Agent.Sdk.Knob; namespace Agent.Plugins.Repository { @@ -37,11 +38,15 @@ public override TfsVCFeatures Features public static readonly int RetriesOnFailure = 3; - public string FilePath => Path.Combine(ExecutionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", "tf", "tf.exe"); + private string TfPath => AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean() + ? Path.Combine(ExecutionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", "tf-legacy") + : Path.Combine(ExecutionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", "tf"); - private string AppConfigFile => Path.Combine(ExecutionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", "tf", "tf.exe.config"); + public string FilePath => Path.Combine(TfPath, "tf.exe"); - private string AppConfigRestoreFile => Path.Combine(ExecutionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", "tf", "tf.exe.config.restore"); + private string AppConfigFile => Path.Combine(TfPath, "tf.exe.config"); + + private string AppConfigRestoreFile => Path.Combine(TfPath, "tf.exe.config.restore"); // TODO: Remove AddAsync after last-saved-checkin-metadata problem is fixed properly. public async Task AddAsync(string localPath) diff --git a/src/Agent.Plugins/TfsVCSourceProvider.cs b/src/Agent.Plugins/TfsVCSourceProvider.cs index aee7963fbe..f9ad5fc122 100644 --- a/src/Agent.Plugins/TfsVCSourceProvider.cs +++ b/src/Agent.Plugins/TfsVCSourceProvider.cs @@ -101,7 +101,8 @@ public async Task GetSourceAsync( if (PlatformUtil.RunningOnWindows) { // Set TFVC_BUILDAGENT_POLICYPATH - string policyDllPath = Path.Combine(executionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", "tf", "Microsoft.TeamFoundation.VersionControl.Controls.dll"); + string tfDirectoryName = AgentKnobs.InstallLegacyTfExe.GetValue(executionContext).AsBoolean() ? "tf-legacy" : "tf"; + string policyDllPath = Path.Combine(executionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value, "externals", tfDirectoryName, "Microsoft.TeamFoundation.VersionControl.Controls.dll"); ArgUtil.File(policyDllPath, nameof(policyDllPath)); const string policyPathEnvKey = "TFVC_BUILDAGENT_POLICYPATH"; executionContext.Output(StringUtil.Loc("SetEnvVar", policyPathEnvKey)); diff --git a/src/Agent.Sdk/Knob/AgentKnobs.cs b/src/Agent.Sdk/Knob/AgentKnobs.cs index 3ba045513a..a0c9a0bec5 100644 --- a/src/Agent.Sdk/Knob/AgentKnobs.cs +++ b/src/Agent.Sdk/Knob/AgentKnobs.cs @@ -758,5 +758,12 @@ public class AgentKnobs "Use PowerShell script wrapper to handle PowerShell ConstrainedLanguage mode.", new PipelineFeatureSource("UsePSScriptWrapper"), new BuiltInDefaultKnobSource("false")); + + public static readonly Knob InstallLegacyTfExe = new Knob( + nameof(InstallLegacyTfExe), + "If true, agent will install the previous version of TF.exe in the tf-legacy and vstsom-legacy directories", + new RuntimeKnobSource("AGENT_INSTALL_LEGACY_TF_EXE"), + new EnvironmentKnobSource("AGENT_INSTALL_LEGACY_TF_EXE"), + new BuiltInDefaultKnobSource("false")); } } diff --git a/src/Agent.Worker/Build/TFCommandManager.cs b/src/Agent.Worker/Build/TFCommandManager.cs index d606ee16c4..f44f9fdbbe 100644 --- a/src/Agent.Worker/Build/TFCommandManager.cs +++ b/src/Agent.Worker/Build/TFCommandManager.cs @@ -11,6 +11,7 @@ using System.Text; using System.Xml; using System.Security.Cryptography.X509Certificates; +using Agent.Sdk.Knob; namespace Microsoft.VisualStudio.Services.Agent.Worker.Build { @@ -34,11 +35,15 @@ public override TfsVCFeatures Features protected override string Switch => "/"; - public override string FilePath => Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Tf), "tf.exe"); + private string TfPath => AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean() + ? HostContext.GetDirectory(WellKnownDirectory.TfLegacy) + : HostContext.GetDirectory(WellKnownDirectory.Tf); - private string AppConfigFile => Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Tf), "tf.exe.config"); + public override string FilePath => Path.Combine(TfPath, "tf.exe"); - private string AppConfigRestoreFile => Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Tf), "tf.exe.config.restore"); + private string AppConfigFile => Path.Combine(TfPath, "tf.exe.config"); + + private string AppConfigRestoreFile => Path.Combine(TfPath, "tf.exe.config.restore"); // TODO: Remove AddAsync after last-saved-checkin-metadata problem is fixed properly. public async Task AddAsync(string localPath) diff --git a/src/Agent.Worker/Build/TfsVCSourceProvider.cs b/src/Agent.Worker/Build/TfsVCSourceProvider.cs index 33309a2546..d295decb87 100644 --- a/src/Agent.Worker/Build/TfsVCSourceProvider.cs +++ b/src/Agent.Worker/Build/TfsVCSourceProvider.cs @@ -17,6 +17,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Agent.Sdk.Knob; namespace Microsoft.VisualStudio.Services.Agent.Worker.Build { @@ -88,7 +89,11 @@ public async Task GetSourceAsync( if (PlatformUtil.RunningOnWindows) { // Set TFVC_BUILDAGENT_POLICYPATH - string policyDllPath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.ServerOM), "Microsoft.TeamFoundation.VersionControl.Controls.dll"); + string vstsomPath = AgentKnobs.InstallLegacyTfExe.GetValue(executionContext).AsBoolean() + ? HostContext.GetDirectory(WellKnownDirectory.ServerOMLegacy) + : HostContext.GetDirectory(WellKnownDirectory.ServerOM); + + string policyDllPath = Path.Combine(vstsomPath, "Microsoft.TeamFoundation.VersionControl.Controls.dll"); ArgUtil.File(policyDllPath, nameof(policyDllPath)); const string policyPathEnvKey = "TFVC_BUILDAGENT_POLICYPATH"; executionContext.Output(StringUtil.Loc("SetEnvVar", policyPathEnvKey)); diff --git a/src/Agent.Worker/Handlers/LegacyPowerShellHandler.cs b/src/Agent.Worker/Handlers/LegacyPowerShellHandler.cs index 4ffb86d2ad..92bc313d06 100644 --- a/src/Agent.Worker/Handlers/LegacyPowerShellHandler.cs +++ b/src/Agent.Worker/Handlers/LegacyPowerShellHandler.cs @@ -12,6 +12,7 @@ using Microsoft.VisualStudio.Services.WebApi; using System.Xml; using Microsoft.TeamFoundation.DistributedTask.Pipelines; +using Agent.Sdk.Knob; namespace Microsoft.VisualStudio.Services.Agent.Worker.Handlers { @@ -205,8 +206,13 @@ public async Task RunAsync() // Copy the OM binaries into the legacy host folder. ExecutionContext.Output(StringUtil.Loc("PrepareTaskExecutionHandler")); + + string sourceDirectory = AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean() + ? HostContext.GetDirectory(WellKnownDirectory.ServerOMLegacy) + : HostContext.GetDirectory(WellKnownDirectory.ServerOM); + IOUtil.CopyDirectory( - source: HostContext.GetDirectory(WellKnownDirectory.ServerOM), + source: sourceDirectory, target: HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost), cancellationToken: ExecutionContext.CancellationToken); Trace.Info("Finished copying files."); diff --git a/src/Agent.Worker/JobExtension.cs b/src/Agent.Worker/JobExtension.cs index c59f7f5572..4ae96685d9 100644 --- a/src/Agent.Worker/JobExtension.cs +++ b/src/Agent.Worker/JobExtension.cs @@ -268,6 +268,11 @@ public async Task> InitializeJob(IExecutionContext jobContext, Pipel } } + if (AgentKnobs.InstallLegacyTfExe.GetValue(jobContext).AsBoolean()) + { + await TfManager.DownloadLegacyTfToolsAsync(context); + } + // build up 3 lists of steps, pre-job, job, post-job Stack postJobStepsBuilder = new Stack(); Dictionary taskVariablesMapping = new Dictionary(); diff --git a/src/Agent.Worker/JobRunner.cs b/src/Agent.Worker/JobRunner.cs index 0a291a146a..959834bff3 100644 --- a/src/Agent.Worker/JobRunner.cs +++ b/src/Agent.Worker/JobRunner.cs @@ -177,7 +177,11 @@ public async Task RunAsync(Pipelines.AgentJobRequestMessage message, jobContext.SetVariable(Constants.Variables.Agent.RootDirectory, HostContext.GetDirectory(WellKnownDirectory.Work), isFilePath: true); if (PlatformUtil.RunningOnWindows) { - jobContext.SetVariable(Constants.Variables.Agent.ServerOMDirectory, HostContext.GetDirectory(WellKnownDirectory.ServerOM), isFilePath: true); + string serverOMDirectoryVariable = AgentKnobs.InstallLegacyTfExe.GetValue(jobContext).AsBoolean() + ? HostContext.GetDirectory(WellKnownDirectory.ServerOMLegacy) + : HostContext.GetDirectory(WellKnownDirectory.ServerOM); + + jobContext.SetVariable(Constants.Variables.Agent.ServerOMDirectory, serverOMDirectoryVariable, isFilePath: true); } if (!PlatformUtil.RunningOnWindows) { diff --git a/src/Agent.Worker/Release/ReleaseJobExtension.cs b/src/Agent.Worker/Release/ReleaseJobExtension.cs index 9bc98baef7..ca09854472 100644 --- a/src/Agent.Worker/Release/ReleaseJobExtension.cs +++ b/src/Agent.Worker/Release/ReleaseJobExtension.cs @@ -229,6 +229,11 @@ private async Task DownloadArtifacts(IExecutionContext executionContext, await teeUtil.DownloadTeeIfAbsent(); } + if (AgentKnobs.InstallLegacyTfExe.GetValue(executionContext).AsBoolean()) + { + await TfManager.DownloadLegacyTfToolsAsync(executionContext); + } + try { foreach (AgentArtifactDefinition agentArtifactDefinition in agentArtifactDefinitions) diff --git a/src/Agent.Worker/TfManager.cs b/src/Agent.Worker/TfManager.cs new file mode 100644 index 0000000000..fdbbffa582 --- /dev/null +++ b/src/Agent.Worker/TfManager.cs @@ -0,0 +1,130 @@ +using Microsoft.VisualStudio.Services.Agent.Util; +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Services.Agent.Worker +{ + public interface IRetryOptions + { + int CurrentCount { get; set; } + int Limit { get; init; } + } + + public record RetryOptions : IRetryOptions + { + public int CurrentCount { get; set; } + public int Limit { get; init; } + } + + public static class TfManager + { + public static async Task DownloadLegacyTfToolsAsync(IExecutionContext executionContext) + { + ArgUtil.NotNull(executionContext, nameof(executionContext)); + string externalsPath = Path.Combine(executionContext.GetVariableValueOrDefault("Agent.HomeDirectory"), Constants.Path.ExternalsDirectory); + ArgUtil.NotNull(externalsPath, nameof(externalsPath)); + + string tfLegacyExternalsPath = Path.Combine(externalsPath, "tf-legacy"); + var retryOptions = new RetryOptions() { CurrentCount = 0, Limit = 3 }; + + if (!Directory.Exists(tfLegacyExternalsPath)) + { + const string tfDownloadUrl = "https://vstsagenttools.blob.core.windows.net/tools/vstsom/m153_47c0856d/vstsom.zip"; + string tempTfDirectory = Path.Combine(externalsPath, "tf_download_temp"); + + await DownloadAsync(executionContext, tfDownloadUrl, tempTfDirectory, tfLegacyExternalsPath, retryOptions); + } + else + { + executionContext.Debug($"tf-legacy download already exists at {tfLegacyExternalsPath}."); + } + + string vstsomLegacyExternalsPath = Path.Combine(externalsPath, "vstsom-legacy"); + + if (!Directory.Exists(vstsomLegacyExternalsPath)) + { + const string vstsomDownloadUrl = "https://vstsagenttools.blob.core.windows.net/tools/vstsom/m122_887c6659/vstsom.zip"; + string tempVstsomDirectory = Path.Combine(externalsPath, "vstsom_download_temp"); + + await DownloadAsync(executionContext, vstsomDownloadUrl, tempVstsomDirectory, vstsomLegacyExternalsPath, retryOptions); + } + else + { + executionContext.Debug($"vstsom-legacy download already exists at {vstsomLegacyExternalsPath}."); + } + } + + public static async Task DownloadAsync(IExecutionContext executionContext, string blobUrl, string tempDirectory, string extractPath, IRetryOptions retryOptions) + { + Directory.CreateDirectory(tempDirectory); + string downloadPath = Path.ChangeExtension(Path.Combine(tempDirectory, "download"), ".completed"); + string toolName = new DirectoryInfo(extractPath).Name; + + const int timeout = 180; + const int defaultFileStreamBufferSize = 4096; + const int retryDelay = 10000; + + try + { + using CancellationTokenSource downloadCts = new(TimeSpan.FromSeconds(timeout)); + using CancellationTokenSource linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(downloadCts.Token, executionContext.CancellationToken); + CancellationToken cancellationToken = linkedTokenSource.Token; + + using HttpClient httpClient = new(); + using Stream stream = await httpClient.GetStreamAsync(blobUrl, cancellationToken); + using FileStream fs = new(downloadPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: defaultFileStreamBufferSize, useAsync: true); + + while (retryOptions.CurrentCount < retryOptions.Limit) + { + try + { + executionContext.Debug($"Retry options: {retryOptions.ToString()}."); + await stream.CopyToAsync(fs, cancellationToken); + executionContext.Debug($"Finished downloading {toolName}."); + await fs.FlushAsync(cancellationToken); + fs.Close(); + break; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + executionContext.Debug($"{toolName} download has been cancelled."); + throw; + } + catch (Exception) + { + retryOptions.CurrentCount++; + + if (retryOptions.CurrentCount == retryOptions.Limit) + { + IOUtil.DeleteDirectory(tempDirectory, CancellationToken.None); + executionContext.Error($"Retry limit for {toolName} download has been exceeded."); + return; + } + + executionContext.Debug($"Failed to download {toolName}"); + executionContext.Debug($"Retry {toolName} download in 10 seconds."); + await Task.Delay(retryDelay, cancellationToken); + } + } + + executionContext.Debug($"Extracting {toolName}..."); + ZipFile.ExtractToDirectory(downloadPath, extractPath); + File.WriteAllText(downloadPath, DateTime.UtcNow.ToString()); + executionContext.Debug($"{toolName} has been extracted and cleaned up"); + } + catch (Exception ex) + { + executionContext.Error(ex); + } + finally + { + IOUtil.DeleteDirectory(tempDirectory, CancellationToken.None); + executionContext.Debug($"{toolName} download directory has been cleaned up."); + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Services.Agent/Constants.cs b/src/Microsoft.VisualStudio.Services.Agent/Constants.cs index cca2a2a2a0..cb9fe38891 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/Constants.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/Constants.cs @@ -22,6 +22,8 @@ public enum WellKnownDirectory Tools, Update, Work, + TfLegacy, + ServerOMLegacy } public enum WellKnownConfigFile @@ -313,9 +315,11 @@ public static class Path public static readonly string ExternalsDirectory = "externals"; public static readonly string LegacyPSHostDirectory = "vstshost"; public static readonly string ServerOMDirectory = "vstsom"; + public static readonly string ServerOMLegacyDirectory = "vstsom-legacy"; public static readonly string TempDirectory = "_temp"; public static readonly string TeeDirectory = "tee"; public static readonly string TfDirectory = "tf"; + public static readonly string TfLegacyDirectory = "tf-legacy"; public static readonly string ToolDirectory = "_tool"; public static readonly string TaskJsonFile = "task.json"; public static readonly string TasksDirectory = "_tasks"; diff --git a/src/Microsoft.VisualStudio.Services.Agent/HostContext.cs b/src/Microsoft.VisualStudio.Services.Agent/HostContext.cs index 2cb0cacde0..d9766822f3 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/HostContext.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/HostContext.cs @@ -206,12 +206,24 @@ public virtual string GetDirectory(WellKnownDirectory directory) Constants.Path.ServerOMDirectory); break; + case WellKnownDirectory.ServerOMLegacy: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Externals), + Constants.Path.ServerOMLegacyDirectory); + break; + case WellKnownDirectory.Tf: path = Path.Combine( GetDirectory(WellKnownDirectory.Externals), Constants.Path.TfDirectory); break; + case WellKnownDirectory.TfLegacy: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Externals), + Constants.Path.TfLegacyDirectory); + break; + case WellKnownDirectory.Tee: path = Path.Combine( GetDirectory(WellKnownDirectory.Externals), diff --git a/src/Misc/externals.sh b/src/Misc/externals.sh index 26339cb73a..802997681f 100644 --- a/src/Misc/externals.sh +++ b/src/Misc/externals.sh @@ -164,14 +164,14 @@ if [[ "$PACKAGERUNTIME" == "win-x"* ]]; then acquireExternalTool "$CONTAINER_URL/azcopy/1/azcopy.zip" azcopy acquireExternalTool "$CONTAINER_URL/vstshost/m122_887c6659/vstshost.zip" vstshost - acquireExternalTool "$CONTAINER_URL/vstsom/m122_887c6659/vstsom.zip" vstsom + acquireExternalTool "$CONTAINER_URL/vstsom/m153_47c0856d_adhoc/vstsom.zip" vstsom fi acquireExternalTool "$CONTAINER_URL/mingit/${MINGIT_VERSION}/MinGit-${MINGIT_VERSION}-${BIT}-bit.zip" git acquireExternalTool "$CONTAINER_URL/git-lfs/${LFS_VERSION}/x${BIT}/git-lfs.exe" "git/mingw${BIT}/bin" acquireExternalTool "$CONTAINER_URL/pdbstr/1/pdbstr.zip" pdbstr acquireExternalTool "$CONTAINER_URL/symstore/1/symstore.zip" symstore - acquireExternalTool "$CONTAINER_URL/vstsom/m153_47c0856d/vstsom.zip" tf + acquireExternalTool "$CONTAINER_URL/vstsom/m153_47c0856d_adhoc/vstsom.zip" tf acquireExternalTool "$CONTAINER_URL/vswhere/2_8_4/vswhere.zip" vswhere acquireExternalTool "https://dist.nuget.org/win-x86-commandline/v3.4.4/nuget.exe" nuget diff --git a/src/Test/L0/ServiceInterfacesL0.cs b/src/Test/L0/ServiceInterfacesL0.cs index f7c8162132..ba4b4f057e 100644 --- a/src/Test/L0/ServiceInterfacesL0.cs +++ b/src/Test/L0/ServiceInterfacesL0.cs @@ -99,7 +99,8 @@ public void WorkerInterfacesSpecifyDefaultImplementation() typeof(IResultReader), typeof(INUnitResultsXmlReader), typeof(IWorkerCommand), - typeof(ITaskRestrictionsChecker) + typeof(ITaskRestrictionsChecker), + typeof(IRetryOptions) }; Validate( assembly: typeof(IStepsRunner).GetTypeInfo().Assembly, diff --git a/src/Test/L0/TestHostContext.cs b/src/Test/L0/TestHostContext.cs index 37c25cf393..d677add5f6 100644 --- a/src/Test/L0/TestHostContext.cs +++ b/src/Test/L0/TestHostContext.cs @@ -211,12 +211,24 @@ public string GetDirectory(WellKnownDirectory directory) Constants.Path.ServerOMDirectory); break; + case WellKnownDirectory.ServerOMLegacy: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Externals), + Constants.Path.ServerOMLegacyDirectory); + break; + case WellKnownDirectory.Tf: path = Path.Combine( GetDirectory(WellKnownDirectory.Externals), Constants.Path.TfDirectory); break; + case WellKnownDirectory.TfLegacy: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Externals), + Constants.Path.TfLegacyDirectory); + break; + case WellKnownDirectory.Tee: path = Path.Combine( GetDirectory(WellKnownDirectory.Externals), diff --git a/src/Test/L0/Worker/TfManagerL0.cs b/src/Test/L0/Worker/TfManagerL0.cs new file mode 100644 index 0000000000..5cb4ace3a6 --- /dev/null +++ b/src/Test/L0/Worker/TfManagerL0.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.Services.Agent.Tests.Worker +{ + public sealed class TfManagerL0 + { + private const string VstsomLegacy = "vstsom-legacy"; + private const string TfLegacy = "tf-legacy"; + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task DownloadTfLegacyToolsAsync() + { + // Arrange + using var tokenSource = new CancellationTokenSource(); + using var hostContext = new TestHostContext(this); + var executionContext = new Mock(); + + executionContext.Setup(x => x.CancellationToken).Returns(tokenSource.Token); + executionContext.Setup(x => x.GetVariableValueOrDefault(It.Is(s => s == "Agent.HomeDirectory"))) + .Returns(hostContext.GetDirectory(WellKnownDirectory.Root)); + + string externalsPath = hostContext.GetDirectory(WellKnownDirectory.Externals); + string tfPath = Path.Combine(externalsPath, TfLegacy); + string vstsomPath = Path.Combine(externalsPath, VstsomLegacy); + + // Act + await TfManager.DownloadLegacyTfToolsAsync(executionContext.Object); + + // Assert + Assert.True(Directory.Exists(tfPath)); + Assert.True(File.Exists(Path.Combine(tfPath, "TF.exe"))); + Assert.False(Directory.Exists(Path.Combine(externalsPath, "tf_download_temp"))); + + Assert.True(Directory.Exists(vstsomPath)); + Assert.True(File.Exists(Path.Combine(vstsomPath, "TF.exe"))); + Assert.False(Directory.Exists(Path.Combine(externalsPath, "vstsom_download_temp"))); + + // Cleanup + IOUtil.DeleteDirectory(tfPath, CancellationToken.None); + IOUtil.DeleteDirectory(vstsomPath, CancellationToken.None); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task DownloadAsync_Retries() + { + // Arrange + using var tokenSource = new CancellationTokenSource(); + using var hostContext = new TestHostContext(this); + var executionContext = new Mock(); + + executionContext.Setup(x => x.CancellationToken).Returns(tokenSource.Token); + executionContext.Setup(x => x.GetVariableValueOrDefault(It.Is(s => s == "Agent.HomeDirectory"))) + .Returns(hostContext.GetDirectory(WellKnownDirectory.Root)); + + var retryOptions = new Mock(); + retryOptions.SetupProperty(opt => opt.CurrentCount); + retryOptions.Setup(opt => opt.ToString()).Throws(); + retryOptions.Setup(opt => opt.Limit).Returns(3); + + const string downloadUrl = "https://vstsagenttools.blob.core.windows.net/tools/vstsom/m122_887c6659/vstsom.zip"; + string tempDirectory = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "temp-test"); + string extractDirectory = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "test"); + + // Act + await TfManager.DownloadAsync(executionContext.Object, downloadUrl, tempDirectory, extractDirectory, retryOptions.Object); + + // Assert + Assert.False(Directory.Exists(tempDirectory)); + Assert.False(Directory.Exists(extractDirectory)); + retryOptions.VerifySet(opt => opt.CurrentCount = It.IsAny(), Times.Exactly(3)); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task DownloadAsync_Cancellation() + { + // Arrange + using var tokenSource = new CancellationTokenSource(); + using var hostContext = new TestHostContext(this); + var executionContext = new Mock(); + + executionContext.Setup(x => x.CancellationToken).Returns(tokenSource.Token); + executionContext.Setup(x => x.GetVariableValueOrDefault(It.Is(s => s == "Agent.HomeDirectory"))) + .Returns(hostContext.GetDirectory(WellKnownDirectory.Root)); + + var retryOptions = new Mock(); + retryOptions.SetupProperty(opt => opt.CurrentCount); + retryOptions.Setup(opt => opt.ToString()).Callback(() => tokenSource.Cancel()); + retryOptions.Setup(opt => opt.Limit).Returns(3); + + const string downloadUrl = "https://vstsagenttools.blob.core.windows.net/tools/vstsom/m122_887c6659/vstsom.zip"; + string tempDirectory = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "temp-test"); + string extractDirectory = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "test"); + + // Act + await TfManager.DownloadAsync(executionContext.Object, downloadUrl, tempDirectory, extractDirectory, retryOptions.Object); + + // Assert + Assert.False(Directory.Exists(tempDirectory)); + Assert.False(Directory.Exists(extractDirectory)); + retryOptions.VerifySet(opt => opt.CurrentCount = It.IsAny(), Times.Never()); + } + } +}