Skip to content

Commit

Permalink
Update TF.exe and binding redirects for vstshost (#4990)
Browse files Browse the repository at this point in the history
* 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 <undefined>
Co-authored-by: Kirill Ivlev <102740624+kirill-ivlev@users.noreply.github.com>

* Install legacy tf, vstsom, vstshost tools

* Update blob storage url for vstshost

---------

Co-authored-by: Kirill Ivlev <102740624+kirill-ivlev@users.noreply.github.com>
Co-authored-by: v-levockina <undefined>
  • Loading branch information
aleksandrlevochkin and kirill-ivlev authored Sep 13, 2024
1 parent 363ba84 commit 02554e4
Show file tree
Hide file tree
Showing 16 changed files with 393 additions and 19 deletions.
11 changes: 8 additions & 3 deletions src/Agent.Plugins/TFCliManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/Agent.Plugins/TfsVCSourceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
8 changes: 8 additions & 0 deletions src/Agent.Sdk/Knob/AgentKnobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -758,5 +758,13 @@ 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, the agent will install the legacy versions of TF, vstsom and vstshost",
new RuntimeKnobSource("AGENT_INSTALL_LEGACY_TF_EXE"),
new EnvironmentKnobSource("AGENT_INSTALL_LEGACY_TF_EXE"),
new PipelineFeatureSource("InstallLegacyTfExe"),
new BuiltInDefaultKnobSource("false"));
}
}
11 changes: 8 additions & 3 deletions src/Agent.Worker/Build/TFCommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion src/Agent.Worker/Build/TfsVCSourceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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));
Expand Down
34 changes: 28 additions & 6 deletions src/Agent.Worker/Handlers/LegacyPowerShellHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -81,7 +82,10 @@ private List<Tuple<String, List<Tuple<String, String>>>> GetAdditionalCommandsFo
}

// Initialize our Azure Support (imports the module, sets up the Azure subscription)
string path = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "vstshost");
string path = AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean()
? Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "vstshost-legacy")
: Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "vstshost");

string azurePSM1 = Path.Combine(path, "Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Azure\\Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Azure.psm1");

Trace.Verbose("AzurePowerShellHandler.UpdatePowerShellEnvironment - AddCommand(Import-Module)");
Expand Down Expand Up @@ -205,9 +209,18 @@ 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);

string targetDirectory = AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean()
? HostContext.GetDirectory(WellKnownDirectory.LegacyPSHostLegacy)
: HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost);

IOUtil.CopyDirectory(
source: HostContext.GetDirectory(WellKnownDirectory.ServerOM),
target: HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost),
source: sourceDirectory,
target: targetDirectory,
cancellationToken: ExecutionContext.CancellationToken);
Trace.Info("Finished copying files.");

Expand All @@ -230,7 +243,11 @@ public async Task RunAsync()

try
{
String vstsPSHostExe = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost), "LegacyVSTSPowerShellHost.exe");
String vstsPSHostExeDirectory = AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean()
? HostContext.GetDirectory(WellKnownDirectory.LegacyPSHostLegacy)
: HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost);

String vstsPSHostExe = Path.Combine(vstsPSHostExeDirectory, "LegacyVSTSPowerShellHost.exe");
Int32 exitCode = await processInvoker.ExecuteAsync(workingDirectory: workingDirectory,
fileName: vstsPSHostExe,
arguments: "",
Expand Down Expand Up @@ -432,10 +449,15 @@ protected virtual void AddLegacyHostEnvironmentVariables(string scriptFile, stri

private void AddProxySetting(IVstsAgentWebProxy agentProxy)
{
string appConfig = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost), _appConfigFileName);
string psHostDirectory = AgentKnobs.InstallLegacyTfExe.GetValue(ExecutionContext).AsBoolean()
? HostContext.GetDirectory(WellKnownDirectory.LegacyPSHostLegacy)
: HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost);

string appConfig = Path.Combine(psHostDirectory, _appConfigFileName);

ArgUtil.File(appConfig, _appConfigFileName);

string appConfigRestore = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.LegacyPSHost), _appConfigRestoreFileName);
string appConfigRestore = Path.Combine(psHostDirectory, _appConfigRestoreFileName);
if (!File.Exists(appConfigRestore))
{
Trace.Info("Take snapshot of current appconfig for restore modified appconfig.");
Expand Down
5 changes: 5 additions & 0 deletions src/Agent.Worker/JobExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ public async Task<List<IStep>> 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<IStep> postJobStepsBuilder = new Stack<IStep>();
Dictionary<Guid, Variables> taskVariablesMapping = new Dictionary<Guid, Variables>();
Expand Down
6 changes: 5 additions & 1 deletion src/Agent.Worker/JobRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ public async Task<TaskResult> 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)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Agent.Worker/Release/ReleaseJobExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
145 changes: 145 additions & 0 deletions src/Agent.Worker/TfManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Microsoft.VisualStudio.Services.Agent.Util;
using Microsoft.VisualStudio.Services.Agent.Worker;
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}.");
}

string vstsHostLegacyExternalsPath = Path.Combine(externalsPath, "vstshost-legacy");

if (!Directory.Exists(vstsHostLegacyExternalsPath))
{
const string vstsHostDownloadUrl = "https://vstsagenttools.blob.core.windows.net/tools/vstshost/m122_887c6659/vstshost.zip";
string tempVstsHostDirectory = Path.Combine(externalsPath, "vstshost_download_temp");

await DownloadAsync(executionContext, vstsHostDownloadUrl, tempVstsHostDirectory, vstsHostLegacyExternalsPath, retryOptions);
}
else
{
executionContext.Debug($"vstshost-legacy download already exists at {vstsHostLegacyExternalsPath}.");
}
}

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.");
}
}
}
}
6 changes: 6 additions & 0 deletions src/Microsoft.VisualStudio.Services.Agent/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public enum WellKnownDirectory
Tools,
Update,
Work,
TfLegacy,
ServerOMLegacy,
LegacyPSHostLegacy
}

public enum WellKnownConfigFile
Expand Down Expand Up @@ -312,10 +315,13 @@ public static class Path
public static readonly string DiagDirectory = "_diag";
public static readonly string ExternalsDirectory = "externals";
public static readonly string LegacyPSHostDirectory = "vstshost";
public static readonly string LegacyPSHostLegacyDirectory = "vstshost-legacy";
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";
Expand Down
Loading

0 comments on commit 02554e4

Please sign in to comment.