in src/Agent.Worker/Build/TfsVCSourceProvider.cs [26:420]
public async Task GetSourceAsync(
IExecutionContext executionContext,
ServiceEndpoint endpoint,
CancellationToken cancellationToken)
{
Trace.Entering();
// Validate args.
ArgUtil.NotNull(executionContext, nameof(executionContext));
ArgUtil.NotNull(endpoint, nameof(endpoint));
if (executionContext == null || endpoint == null)
{
return;
}
if (PlatformUtil.RunningOnWindows)
{
// Validate .NET Framework 4.6 or higher is installed.
if (!NetFrameworkUtil.Test(new Version(4, 6), Trace))
{
throw new Exception(StringUtil.Loc("MinimumNetFramework46"));
}
}
// Create the tf command manager.
var tf = HostContext.CreateService<ITfsVCCommandManager>();
tf.CancellationToken = cancellationToken;
tf.Endpoint = endpoint;
tf.ExecutionContext = executionContext;
// Setup proxy.
var agentProxy = HostContext.GetService<IVstsAgentWebProxy>();
if (!string.IsNullOrEmpty(executionContext.Variables.Agent_ProxyUrl) && !agentProxy.WebProxy.IsBypassed(endpoint.Url))
{
executionContext.Debug($"Configure '{tf.FilePath}' to work through proxy server '{executionContext.Variables.Agent_ProxyUrl}'.");
tf.SetupProxy(executionContext.Variables.Agent_ProxyUrl, executionContext.Variables.Agent_ProxyUsername, executionContext.Variables.Agent_ProxyPassword);
}
// Setup client certificate.
var agentCertManager = HostContext.GetService<IAgentCertificateManager>();
if (agentCertManager.SkipServerCertificateValidation)
{
executionContext.Debug("TF does not support ignore SSL certificate validation error.");
}
var configUrl = new Uri(HostContext.GetService<IConfigurationStore>().GetSettings().ServerUrl);
if (!string.IsNullOrEmpty(agentCertManager.ClientCertificateFile) &&
Uri.Compare(endpoint.Url, configUrl, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
{
executionContext.Debug($"Configure '{tf.FilePath}' to work with client cert '{agentCertManager.ClientCertificateFile}'.");
tf.SetupClientCertificate(agentCertManager.ClientCertificateFile, agentCertManager.ClientCertificatePrivateKeyFile, agentCertManager.ClientCertificateArchiveFile, agentCertManager.ClientCertificatePassword);
}
// Add TF to the PATH.
string tfPath = tf.FilePath;
ArgUtil.File(tfPath, nameof(tfPath));
executionContext.Output(StringUtil.Loc("Prepending0WithDirectoryContaining1", Constants.PathVariable, Path.GetFileName(tfPath)));
PathUtil.PrependPath(Path.GetDirectoryName(tfPath));
executionContext.Debug($"{Constants.PathVariable}: '{Environment.GetEnvironmentVariable(Constants.PathVariable)}'");
if (PlatformUtil.RunningOnWindows)
{
// Set TFVC_BUILDAGENT_POLICYPATH
string policyDllPath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.ServerOM), "Microsoft.TeamFoundation.VersionControl.Controls.dll");
ArgUtil.File(policyDllPath, nameof(policyDllPath));
const string policyPathEnvKey = "TFVC_BUILDAGENT_POLICYPATH";
executionContext.Output(StringUtil.Loc("SetEnvVar", policyPathEnvKey));
Environment.SetEnvironmentVariable(policyPathEnvKey, policyDllPath);
}
// Check if the administrator accepted the license terms of the TEE EULA when configuring the agent.
AgentSettings settings = HostContext.GetService<IConfigurationStore>().GetSettings();
if (tf.Features.HasFlag(TfsVCFeatures.Eula) && settings.AcceptTeeEula)
{
// Check if the "tf eula -accept" command needs to be run for the current user.
bool skipEula = false;
try
{
skipEula = tf.TestEulaAccepted();
}
catch (Exception ex)
{
executionContext.Debug("Unexpected exception while testing whether the TEE EULA has been accepted for the current user.");
executionContext.Debug(ex.ToString());
}
if (!skipEula)
{
// Run the command "tf eula -accept".
try
{
await tf.EulaAsync();
}
catch (Exception ex)
{
executionContext.Debug(ex.ToString());
executionContext.Warning(ex.Message);
}
}
}
// Get the workspaces.
executionContext.Output(StringUtil.Loc("QueryingWorkspaceInfo"));
ITfsVCWorkspace[] tfWorkspaces = await tf.WorkspacesAsync();
// Determine the workspace name.
string buildDirectory = executionContext.Variables.Get(Constants.Variables.Agent.BuildDirectory);
ArgUtil.NotNullOrEmpty(buildDirectory, nameof(buildDirectory));
string workspaceName = $"ws_{Path.GetFileName(buildDirectory)}_{settings.AgentId}";
executionContext.Variables.Set(Constants.Variables.Build.RepoTfvcWorkspace, workspaceName);
// Get the definition mappings.
DefinitionWorkspaceMapping[] definitionMappings =
JsonConvert.DeserializeObject<DefinitionWorkspaceMappings>(endpoint.Data[EndpointData.TfvcWorkspaceMapping])?.Mappings;
// Determine the sources directory.
string sourcesDirectory = GetEndpointData(endpoint, Constants.EndpointData.SourcesDirectory);
ArgUtil.NotNullOrEmpty(sourcesDirectory, nameof(sourcesDirectory));
// Attempt to re-use an existing workspace if the command manager supports scorch
// or if clean is not specified.
ITfsVCWorkspace existingTFWorkspace = null;
bool clean = endpoint.Data.ContainsKey(EndpointData.Clean) &&
StringUtil.ConvertToBoolean(endpoint.Data[EndpointData.Clean], defaultValue: false);
if (tf.Features.HasFlag(TfsVCFeatures.Scorch) || !clean)
{
existingTFWorkspace = WorkspaceUtil.MatchExactWorkspace(
executionContext: executionContext,
tfWorkspaces: tfWorkspaces,
name: workspaceName,
definitionMappings: definitionMappings,
sourcesDirectory: sourcesDirectory);
if (existingTFWorkspace != null)
{
if (tf.Features.HasFlag(TfsVCFeatures.GetFromUnmappedRoot))
{
// Undo pending changes.
ITfsVCStatus tfStatus = await tf.StatusAsync(localPath: sourcesDirectory);
if (tfStatus?.HasPendingChanges ?? false)
{
await tf.UndoAsync(localPath: sourcesDirectory);
// Cleanup remaining files/directories from pend adds.
tfStatus.AllAdds
.OrderByDescending(x => x.LocalItem) // Sort descending so nested items are deleted before their parent is deleted.
.ToList()
.ForEach(x =>
{
executionContext.Output(StringUtil.Loc("Deleting", x.LocalItem));
IOUtil.Delete(x.LocalItem, cancellationToken);
});
}
}
else
{
// Perform "undo" for each map.
foreach (DefinitionWorkspaceMapping definitionMapping in definitionMappings ?? new DefinitionWorkspaceMapping[0])
{
if (definitionMapping.MappingType == DefinitionMappingType.Map)
{
// Check the status.
string localPath = definitionMapping.GetRootedLocalPath(sourcesDirectory);
ITfsVCStatus tfStatus = await tf.StatusAsync(localPath: localPath);
if (tfStatus?.HasPendingChanges ?? false)
{
// Undo.
await tf.UndoAsync(localPath: localPath);
// Cleanup remaining files/directories from pend adds.
tfStatus.AllAdds
.OrderByDescending(x => x.LocalItem) // Sort descending so nested items are deleted before their parent is deleted.
.ToList()
.ForEach(x =>
{
executionContext.Output(StringUtil.Loc("Deleting", x.LocalItem));
IOUtil.Delete(x.LocalItem, cancellationToken);
});
}
}
}
}
// Scorch.
if (clean)
{
// Try to scorch.
try
{
await tf.ScorchAsync();
}
catch (ProcessExitCodeException ex)
{
// Scorch failed.
// Warn, drop the folder, and re-clone.
executionContext.Warning(ex.Message);
existingTFWorkspace = null;
}
}
}
}
// Create a new workspace.
if (existingTFWorkspace == null)
{
// Remove any conflicting workspaces.
await RemoveConflictingWorkspacesAsync(
tf: tf,
tfWorkspaces: tfWorkspaces,
name: workspaceName,
directory: sourcesDirectory);
// Remove any conflicting workspace from a different computer.
// This is primarily a hosted scenario where a registered hosted
// agent can land on a different computer each time.
tfWorkspaces = await tf.WorkspacesAsync(matchWorkspaceNameOnAnyComputer: true);
foreach (ITfsVCWorkspace tfWorkspace in tfWorkspaces ?? new ITfsVCWorkspace[0])
{
await tf.WorkspaceDeleteAsync(tfWorkspace);
}
// Recreate the sources directory.
executionContext.Debug($"Deleting: '{sourcesDirectory}'.");
IOUtil.DeleteDirectory(sourcesDirectory, cancellationToken);
Directory.CreateDirectory(sourcesDirectory);
// Create the workspace.
await tf.WorkspaceNewAsync();
// Remove the default mapping.
if (tf.Features.HasFlag(TfsVCFeatures.DefaultWorkfoldMap))
{
await tf.WorkfoldUnmapAsync("$/");
}
// Sort the definition mappings.
definitionMappings =
(definitionMappings ?? new DefinitionWorkspaceMapping[0])
.OrderBy(x => x.NormalizedServerPath?.Length ?? 0) // By server path length.
.ToArray() ?? new DefinitionWorkspaceMapping[0];
// Add the definition mappings to the workspace.
foreach (DefinitionWorkspaceMapping definitionMapping in definitionMappings)
{
switch (definitionMapping.MappingType)
{
case DefinitionMappingType.Cloak:
// Add the cloak.
await tf.WorkfoldCloakAsync(serverPath: definitionMapping.ServerPath);
break;
case DefinitionMappingType.Map:
// Add the mapping.
await tf.WorkfoldMapAsync(
serverPath: definitionMapping.ServerPath,
localPath: definitionMapping.GetRootedLocalPath(sourcesDirectory));
break;
default:
throw new NotSupportedException();
}
}
}
if (tf.Features.HasFlag(TfsVCFeatures.GetFromUnmappedRoot))
{
// Get.
await tf.GetAsync(localPath: sourcesDirectory);
}
else
{
// Perform "get" for each map.
foreach (DefinitionWorkspaceMapping definitionMapping in definitionMappings ?? new DefinitionWorkspaceMapping[0])
{
if (definitionMapping.MappingType == DefinitionMappingType.Map)
{
await tf.GetAsync(localPath: definitionMapping.GetRootedLocalPath(sourcesDirectory));
}
}
}
// Steps for shelveset/gated.
string shelvesetName = GetEndpointData(endpoint, Constants.EndpointData.SourceTfvcShelveset);
if (!string.IsNullOrEmpty(shelvesetName))
{
// Steps for gated.
ITfsVCShelveset tfShelveset = null;
string gatedShelvesetName = GetEndpointData(endpoint, Constants.EndpointData.GatedShelvesetName);
if (!string.IsNullOrEmpty(gatedShelvesetName))
{
// Clean the last-saved-checkin-metadata for existing workspaces.
//
// A better long term fix is to add a switch to "tf unshelve" that completely overwrites
// the last-saved-checkin-metadata, instead of merging associated work items.
//
// The targeted workaround for now is to create a trivial change and "tf shelve /move",
// which will delete the last-saved-checkin-metadata.
if (existingTFWorkspace != null)
{
executionContext.Output("Cleaning last saved checkin metadata.");
// Find a local mapped directory.
string firstLocalDirectory =
(definitionMappings ?? new DefinitionWorkspaceMapping[0])
.Where(x => x.MappingType == DefinitionMappingType.Map)
.Select(x => x.GetRootedLocalPath(sourcesDirectory))
.FirstOrDefault(x => Directory.Exists(x));
if (firstLocalDirectory == null)
{
executionContext.Warning("No mapped folder found. Unable to clean last-saved-checkin-metadata.");
}
else
{
// Create a trival change and "tf shelve /move" to clear the
// last-saved-checkin-metadata.
string cleanName = "__tf_clean_wksp_metadata";
string tempCleanFile = Path.Combine(firstLocalDirectory, cleanName);
try
{
File.WriteAllText(path: tempCleanFile, contents: "clean last-saved-checkin-metadata", encoding: Encoding.UTF8);
await tf.AddAsync(tempCleanFile);
await tf.ShelveAsync(shelveset: cleanName, commentFile: tempCleanFile, move: true);
}
catch (Exception ex)
{
executionContext.Warning($"Unable to clean last-saved-checkin-metadata. {ex.Message}");
try
{
await tf.UndoAsync(tempCleanFile);
}
catch (Exception ex2)
{
executionContext.Warning($"Unable to undo '{tempCleanFile}'. {ex2.Message}");
}
}
finally
{
IOUtil.DeleteFile(tempCleanFile);
}
}
}
// Get the shelveset metadata.
tfShelveset = await tf.ShelvesetsAsync(shelveset: shelvesetName);
// The above command throws if the shelveset is not found,
// so the following assertion should never fail.
ArgUtil.NotNull(tfShelveset, nameof(tfShelveset));
}
// Unshelve.
await tf.UnshelveAsync(shelveset: shelvesetName);
// Ensure we undo pending changes for shelveset build at the end.
_undoShelvesetPendingChanges = true;
if (!string.IsNullOrEmpty(gatedShelvesetName))
{
// Create the comment file for reshelve.
StringBuilder comment = new StringBuilder(tfShelveset.Comment ?? string.Empty);
string runCi = GetEndpointData(endpoint, Constants.EndpointData.GatedRunCI);
bool gatedRunCi = StringUtil.ConvertToBoolean(runCi, true);
if (!gatedRunCi)
{
if (comment.Length > 0)
{
comment.AppendLine();
}
comment.Append(Constants.Build.NoCICheckInComment);
}
string commentFile = null;
try
{
commentFile = Path.GetTempFileName();
File.WriteAllText(path: commentFile, contents: comment.ToString(), encoding: Encoding.UTF8);
// Reshelve.
await tf.ShelveAsync(shelveset: gatedShelvesetName, commentFile: commentFile, move: false);
}
finally
{
// Cleanup the comment file.
if (File.Exists(commentFile))
{
File.Delete(commentFile);
}
}
}
}
// Cleanup proxy settings.
if (!string.IsNullOrEmpty(executionContext.Variables.Agent_ProxyUrl) && !agentProxy.WebProxy.IsBypassed(endpoint.Url))
{
executionContext.Debug($"Remove proxy setting for '{tf.FilePath}' to work through proxy server '{executionContext.Variables.Agent_ProxyUrl}'.");
tf.CleanupProxySetting();
}
}