Atomic install tool (#8518)
* Make dotnet install tool atomic Apply TransactionScope to tool install. It can handle the correct timing of roll back and commit. Convert existing ToolPackageObtainer and ShellShimMaker by passing logic via lambda to an object that has IEnlistmentNotification interface. It turns out the very clean. Use .stage as staging place to verify of package content, and shim. It should roll back when something is wrong. When there is ctrl-c, there will be garbage in .stage folder but not the root of the package folder.
This commit is contained in:
parent
38e452204c
commit
5fa558a2ed
33 changed files with 995 additions and 207 deletions
|
@ -47,5 +47,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
|
|||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
public void Delete(string path, bool recursive)
|
||||
{
|
||||
Directory.Delete(path, recursive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,5 +45,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
|
|||
{
|
||||
File.WriteAllText(path, content);
|
||||
}
|
||||
|
||||
public void Delete(string path)
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,5 +16,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
|
|||
string GetDirectoryFullName(string path);
|
||||
|
||||
void CreateDirectory(string path);
|
||||
|
||||
void Delete(string path, bool recursive);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,5 +24,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
|
|||
void CreateEmptyFile(string path);
|
||||
|
||||
void WriteAllText(string path, string content);
|
||||
|
||||
void Delete(string path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -588,4 +588,7 @@ Output: {1}</value>
|
|||
<data name="ToolPackageMissingSettingsFile" xml:space="preserve">
|
||||
<value>Package '{0}' is missing tool settings file DotnetToolSettings.xml.</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ToolPackageConflictPackageId" xml:space="preserve">
|
||||
<value>Tool '{0}' is already installed.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
52
src/dotnet/ShellShim/CreateShimTransaction.cs
Normal file
52
src/dotnet/ShellShim/CreateShimTransaction.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Transactions;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
|
||||
namespace Microsoft.DotNet.ShellShim
|
||||
{
|
||||
internal class CreateShimTransaction : IEnlistmentNotification
|
||||
{
|
||||
private readonly Action<List<FilePath>> _createShim;
|
||||
private readonly Action<List<FilePath>> _rollback;
|
||||
private List<FilePath> _locationOfShimDuringTransaction = new List<FilePath>();
|
||||
|
||||
public CreateShimTransaction(
|
||||
Action<List<FilePath>> createShim,
|
||||
Action<List<FilePath>> rollback)
|
||||
{
|
||||
_createShim = createShim ?? throw new ArgumentNullException(nameof(createShim));
|
||||
_rollback = rollback ?? throw new ArgumentNullException(nameof(rollback));
|
||||
}
|
||||
|
||||
public void CreateShim()
|
||||
{
|
||||
_createShim(_locationOfShimDuringTransaction);
|
||||
}
|
||||
|
||||
public void Commit(Enlistment enlistment)
|
||||
{
|
||||
enlistment.Done();
|
||||
}
|
||||
|
||||
public void InDoubt(Enlistment enlistment)
|
||||
{
|
||||
Rollback(enlistment);
|
||||
}
|
||||
|
||||
public void Prepare(PreparingEnlistment preparingEnlistment)
|
||||
{
|
||||
preparingEnlistment.Done();
|
||||
}
|
||||
|
||||
public void Rollback(Enlistment enlistment)
|
||||
{
|
||||
_rollback(_locationOfShimDuringTransaction);
|
||||
|
||||
enlistment.Done();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,12 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Transactions;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.Tools;
|
||||
|
@ -22,11 +24,38 @@ namespace Microsoft.DotNet.ShellShim
|
|||
|
||||
public ShellShimMaker(string pathToPlaceShim)
|
||||
{
|
||||
_pathToPlaceShim =
|
||||
pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
|
||||
_pathToPlaceShim = pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
|
||||
}
|
||||
|
||||
public void CreateShim(FilePath packageExecutable, string shellCommandName)
|
||||
{
|
||||
var createShimTransaction = new CreateShimTransaction(
|
||||
createShim: locationOfShimDuringTransaction =>
|
||||
{
|
||||
EnsureCommandNameUniqueness(shellCommandName);
|
||||
PlaceShim(packageExecutable, shellCommandName, locationOfShimDuringTransaction);
|
||||
},
|
||||
rollback: locationOfShimDuringTransaction =>
|
||||
{
|
||||
foreach (FilePath f in locationOfShimDuringTransaction)
|
||||
{
|
||||
if (File.Exists(f.Value))
|
||||
{
|
||||
File.Delete(f.Value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
using (var transactionScope = new TransactionScope())
|
||||
{
|
||||
Transaction.Current.EnlistVolatile(createShimTransaction, EnlistmentOptions.None);
|
||||
createShimTransaction.CreateShim();
|
||||
|
||||
transactionScope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaceShim(FilePath packageExecutable, string shellCommandName, List<FilePath> locationOfShimDuringTransaction)
|
||||
{
|
||||
FilePath shimPath = GetShimPath(shellCommandName);
|
||||
|
||||
|
@ -37,12 +66,20 @@ namespace Microsoft.DotNet.ShellShim
|
|||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
CreateConfigFile(shimPath.Value + ".config", entryPoint: packageExecutable, runner: "dotnet");
|
||||
FilePath windowsConfig = GetWindowsConfigPath(shellCommandName);
|
||||
CreateConfigFile(
|
||||
windowsConfig,
|
||||
entryPoint: packageExecutable,
|
||||
runner: "dotnet");
|
||||
|
||||
locationOfShimDuringTransaction.Add(windowsConfig);
|
||||
|
||||
using (var shim = File.Create(shimPath.Value))
|
||||
using (var exe = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherExeResourceName))
|
||||
{
|
||||
exe.CopyTo(shim);
|
||||
}
|
||||
locationOfShimDuringTransaction.Add(shimPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -51,6 +88,7 @@ namespace Microsoft.DotNet.ShellShim
|
|||
script.AppendLine($"dotnet {packageExecutable.ToQuotedString()} \"$@\"");
|
||||
|
||||
File.WriteAllText(shimPath.Value, script.ToString());
|
||||
locationOfShimDuringTransaction.Add(shimPath);
|
||||
|
||||
SetUserExecutionPermissionToShimFile(shimPath);
|
||||
}
|
||||
|
@ -58,7 +96,7 @@ namespace Microsoft.DotNet.ShellShim
|
|||
|
||||
public void EnsureCommandNameUniqueness(string shellCommandName)
|
||||
{
|
||||
if (File.Exists(Path.Combine(_pathToPlaceShim, shellCommandName)))
|
||||
if (File.Exists(GetShimPath(shellCommandName).Value))
|
||||
{
|
||||
throw new GracefulException(
|
||||
string.Format(CommonLocalizableStrings.FailInstallToolSameName,
|
||||
|
@ -66,10 +104,10 @@ namespace Microsoft.DotNet.ShellShim
|
|||
}
|
||||
}
|
||||
|
||||
internal void CreateConfigFile(string outputPath, FilePath entryPoint, string runner)
|
||||
internal void CreateConfigFile(FilePath outputPath, FilePath entryPoint, string runner)
|
||||
{
|
||||
XDocument config;
|
||||
using (var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherConfigResourceName))
|
||||
using(var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherConfigResourceName))
|
||||
{
|
||||
config = XDocument.Load(resource);
|
||||
}
|
||||
|
@ -77,11 +115,16 @@ namespace Microsoft.DotNet.ShellShim
|
|||
var appSettings = config.Descendants("appSettings").First();
|
||||
appSettings.Add(new XElement("add", new XAttribute("key", "entryPoint"), new XAttribute("value", entryPoint.Value)));
|
||||
appSettings.Add(new XElement("add", new XAttribute("key", "runner"), new XAttribute("value", runner ?? string.Empty)));
|
||||
config.Save(outputPath);
|
||||
config.Save(outputPath.Value);
|
||||
}
|
||||
|
||||
public void Remove(string shellCommandName)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
File.Delete(GetWindowsConfigPath(shellCommandName).Value);
|
||||
}
|
||||
|
||||
File.Delete(GetShimPath(shellCommandName).Value);
|
||||
}
|
||||
|
||||
|
@ -96,6 +139,11 @@ namespace Microsoft.DotNet.ShellShim
|
|||
return new FilePath(scriptPath);
|
||||
}
|
||||
|
||||
private FilePath GetWindowsConfigPath(string shellCommandName)
|
||||
{
|
||||
return new FilePath(GetShimPath(shellCommandName).Value + ".config");
|
||||
}
|
||||
|
||||
private static void SetUserExecutionPermissionToShimFile(FilePath scriptPath)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
|
|
52
src/dotnet/ToolPackage/ToolPackageObtainTransaction.cs
Normal file
52
src/dotnet/ToolPackage/ToolPackageObtainTransaction.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Transactions;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
|
||||
namespace Microsoft.DotNet.ToolPackage
|
||||
{
|
||||
internal class ToolPackageObtainTransaction : IEnlistmentNotification
|
||||
{
|
||||
private readonly Func<List<DirectoryPath>, ToolConfigurationAndExecutablePath> _obtainAndReturnExecutablePath;
|
||||
private readonly Action<List<DirectoryPath>> _rollback;
|
||||
private List<DirectoryPath> _locationOfPackageDuringTransaction = new List<DirectoryPath>();
|
||||
|
||||
public ToolPackageObtainTransaction(
|
||||
Func<List<DirectoryPath>, ToolConfigurationAndExecutablePath> obtainAndReturnExecutablePath,
|
||||
Action<List<DirectoryPath>> rollback)
|
||||
{
|
||||
_obtainAndReturnExecutablePath = obtainAndReturnExecutablePath ?? throw new ArgumentNullException(nameof(obtainAndReturnExecutablePath));
|
||||
_rollback = rollback ?? throw new ArgumentNullException(nameof(rollback));
|
||||
}
|
||||
|
||||
public ToolConfigurationAndExecutablePath ObtainAndReturnExecutablePath()
|
||||
{
|
||||
return _obtainAndReturnExecutablePath(_locationOfPackageDuringTransaction);
|
||||
}
|
||||
|
||||
public void Commit(Enlistment enlistment)
|
||||
{
|
||||
enlistment.Done();
|
||||
}
|
||||
|
||||
public void InDoubt(Enlistment enlistment)
|
||||
{
|
||||
Rollback(enlistment);
|
||||
}
|
||||
|
||||
public void Prepare(PreparingEnlistment preparingEnlistment)
|
||||
{
|
||||
preparingEnlistment.Done();
|
||||
}
|
||||
|
||||
public void Rollback(Enlistment enlistment)
|
||||
{
|
||||
_rollback(_locationOfPackageDuringTransaction);
|
||||
|
||||
enlistment.Done();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Transactions;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.DotNet.Tools;
|
||||
using Microsoft.DotNet.Cli;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.Configurer;
|
||||
using Microsoft.DotNet.Tools;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
using NuGet.ProjectModel;
|
||||
|
||||
|
@ -42,6 +41,69 @@ namespace Microsoft.DotNet.ToolPackage
|
|||
string targetframework = null,
|
||||
string source = null,
|
||||
string verbosity = null)
|
||||
{
|
||||
var stageDirectory = _toolsPath.WithSubDirectories(".stage", Path.GetRandomFileName());
|
||||
|
||||
var toolPackageObtainTransaction = new ToolPackageObtainTransaction(
|
||||
obtainAndReturnExecutablePath: (locationOfPackageDuringTransaction) =>
|
||||
{
|
||||
if (Directory.Exists(_toolsPath.WithSubDirectories(packageId).Value))
|
||||
{
|
||||
throw new PackageObtainException(
|
||||
string.Format(CommonLocalizableStrings.ToolPackageConflictPackageId, packageId));
|
||||
}
|
||||
|
||||
locationOfPackageDuringTransaction.Add(stageDirectory);
|
||||
var toolConfigurationAndExecutablePath = ObtainAndReturnExecutablePathInStageFolder(
|
||||
packageId,
|
||||
stageDirectory,
|
||||
packageVersion,
|
||||
nugetconfig,
|
||||
targetframework,
|
||||
source,
|
||||
verbosity);
|
||||
|
||||
DirectoryPath destinationDirectory = _toolsPath.WithSubDirectories(packageId);
|
||||
|
||||
Directory.Move(
|
||||
stageDirectory.Value,
|
||||
destinationDirectory.Value);
|
||||
|
||||
locationOfPackageDuringTransaction.Clear();
|
||||
locationOfPackageDuringTransaction.Add(destinationDirectory);
|
||||
|
||||
return toolConfigurationAndExecutablePath;
|
||||
},
|
||||
rollback: (locationOfPackageDuringTransaction) =>
|
||||
{
|
||||
foreach (DirectoryPath l in locationOfPackageDuringTransaction)
|
||||
{
|
||||
if (Directory.Exists(l.Value))
|
||||
{
|
||||
Directory.Delete(l.Value, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
using (var transactionScope = new TransactionScope())
|
||||
{
|
||||
Transaction.Current.EnlistVolatile(toolPackageObtainTransaction, EnlistmentOptions.None);
|
||||
var toolConfigurationAndExecutablePath = toolPackageObtainTransaction.ObtainAndReturnExecutablePath();
|
||||
|
||||
transactionScope.Complete();
|
||||
return toolConfigurationAndExecutablePath;
|
||||
}
|
||||
}
|
||||
|
||||
private ToolConfigurationAndExecutablePath ObtainAndReturnExecutablePathInStageFolder(
|
||||
string packageId,
|
||||
DirectoryPath stageDirectory,
|
||||
string packageVersion = null,
|
||||
FilePath? nugetconfig = null,
|
||||
string targetframework = null,
|
||||
string source = null,
|
||||
string verbosity = null)
|
||||
{
|
||||
if (packageId == null)
|
||||
{
|
||||
|
@ -65,34 +127,34 @@ namespace Microsoft.DotNet.ToolPackage
|
|||
|
||||
var packageVersionOrPlaceHolder = new PackageVersion(packageVersion);
|
||||
|
||||
DirectoryPath toolDirectory =
|
||||
CreateIndividualToolVersionDirectory(packageId, packageVersionOrPlaceHolder);
|
||||
DirectoryPath nugetSandboxDirectory =
|
||||
CreateNugetSandboxDirectory(packageVersionOrPlaceHolder, stageDirectory);
|
||||
|
||||
FilePath tempProjectPath = CreateTempProject(
|
||||
packageId,
|
||||
packageVersionOrPlaceHolder,
|
||||
targetframework,
|
||||
toolDirectory);
|
||||
nugetSandboxDirectory);
|
||||
|
||||
_projectRestorer.Restore(tempProjectPath, toolDirectory, nugetconfig, source, verbosity);
|
||||
_projectRestorer.Restore(tempProjectPath, nugetSandboxDirectory, nugetconfig, source, verbosity);
|
||||
|
||||
if (packageVersionOrPlaceHolder.IsPlaceholder)
|
||||
{
|
||||
var concreteVersion =
|
||||
new DirectoryInfo(
|
||||
Directory.GetDirectories(
|
||||
toolDirectory.WithSubDirectories(packageId).Value).Single()).Name;
|
||||
nugetSandboxDirectory.WithSubDirectories(packageId).Value).Single()).Name;
|
||||
DirectoryPath versioned =
|
||||
toolDirectory.GetParentPath().WithSubDirectories(concreteVersion);
|
||||
nugetSandboxDirectory.GetParentPath().WithSubDirectories(concreteVersion);
|
||||
|
||||
MoveToVersionedDirectory(versioned, toolDirectory);
|
||||
MoveToVersionedDirectory(versioned, nugetSandboxDirectory);
|
||||
|
||||
toolDirectory = versioned;
|
||||
nugetSandboxDirectory = versioned;
|
||||
packageVersion = concreteVersion;
|
||||
}
|
||||
|
||||
LockFile lockFile = new LockFileFormat()
|
||||
.ReadWithLock(toolDirectory.WithFile("project.assets.json").Value)
|
||||
.ReadWithLock(nugetSandboxDirectory.WithFile("project.assets.json").Value)
|
||||
.Result;
|
||||
|
||||
LockFileItem dotnetToolSettings = FindAssetInLockFile(lockFile, "DotnetToolSettings.xml", packageId);
|
||||
|
@ -104,7 +166,7 @@ namespace Microsoft.DotNet.ToolPackage
|
|||
}
|
||||
|
||||
FilePath toolConfigurationPath =
|
||||
toolDirectory
|
||||
nugetSandboxDirectory
|
||||
.WithSubDirectories(packageId, packageVersion)
|
||||
.WithFile(dotnetToolSettings.Path);
|
||||
|
||||
|
@ -122,7 +184,9 @@ namespace Microsoft.DotNet.ToolPackage
|
|||
|
||||
return new ToolConfigurationAndExecutablePath(
|
||||
toolConfiguration,
|
||||
toolDirectory.WithSubDirectories(
|
||||
_toolsPath.WithSubDirectories(
|
||||
packageId,
|
||||
packageVersion,
|
||||
packageId,
|
||||
packageVersion)
|
||||
.WithFile(entryPointFromLockFile.Path));
|
||||
|
@ -179,12 +243,13 @@ namespace Microsoft.DotNet.ToolPackage
|
|||
new XElement("RestoreAdditionalProjectFallbackFolders", string.Empty), // block other
|
||||
new XElement("RestoreAdditionalProjectFallbackFoldersExcludes", string.Empty), // block other
|
||||
new XElement("DisableImplicitNuGetFallbackFolder", "true")), // disable SDK side implicit NuGetFallbackFolder
|
||||
new XElement("ItemGroup",
|
||||
new XElement("ItemGroup",
|
||||
new XElement("PackageReference",
|
||||
new XAttribute("Include", packageId),
|
||||
new XAttribute("Version", packageVersion.IsConcreteValue ? packageVersion.Value : "*") // nuget will restore * for latest
|
||||
))
|
||||
));
|
||||
))
|
||||
));
|
||||
|
||||
|
||||
File.WriteAllText(tempProjectPath.Value,
|
||||
tempProjectContent.ToString());
|
||||
|
@ -192,12 +257,12 @@ namespace Microsoft.DotNet.ToolPackage
|
|||
return tempProjectPath;
|
||||
}
|
||||
|
||||
private DirectoryPath CreateIndividualToolVersionDirectory(
|
||||
string packageId,
|
||||
PackageVersion packageVersion)
|
||||
private DirectoryPath CreateNugetSandboxDirectory(
|
||||
PackageVersion packageVersion,
|
||||
DirectoryPath stageDirectory
|
||||
)
|
||||
{
|
||||
DirectoryPath individualTool = _toolsPath.WithSubDirectories(packageId);
|
||||
DirectoryPath individualToolVersion = individualTool.WithSubDirectories(packageVersion.Value);
|
||||
DirectoryPath individualToolVersion = stageDirectory.WithSubDirectories(packageVersion.Value);
|
||||
EnsureDirectoryExists(individualToolVersion);
|
||||
return individualToolVersion;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Transactions;
|
||||
using Microsoft.DotNet.Cli;
|
||||
using Microsoft.DotNet.Cli.CommandLine;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
|
@ -21,6 +22,7 @@ namespace Microsoft.DotNet.Tools.Install.Tool
|
|||
private readonly IEnvironmentPathInstruction _environmentPathInstruction;
|
||||
private readonly IShellShimMaker _shellShimMaker;
|
||||
private readonly IReporter _reporter;
|
||||
private readonly IReporter _errorReporter;
|
||||
|
||||
private readonly string _packageId;
|
||||
private readonly string _packageVersion;
|
||||
|
@ -69,13 +71,12 @@ namespace Microsoft.DotNet.Tools.Install.Tool
|
|||
|
||||
_shellShimMaker = shellShimMaker ?? new ShellShimMaker(cliFolderPathCalculator.ToolsShimPath);
|
||||
|
||||
_reporter = reporter;
|
||||
_reporter = (reporter ?? Reporter.Output);
|
||||
_errorReporter = (reporter ?? Reporter.Error);
|
||||
}
|
||||
|
||||
public override int Execute()
|
||||
{
|
||||
var reporter = (_reporter ?? Reporter.Output);
|
||||
var errorReporter = (_reporter ?? Reporter.Error);
|
||||
if (!_global)
|
||||
{
|
||||
throw new GracefulException(LocalizableStrings.InstallToolCommandOnlySupportGlobal);
|
||||
|
@ -83,54 +84,53 @@ namespace Microsoft.DotNet.Tools.Install.Tool
|
|||
|
||||
try
|
||||
{
|
||||
var toolConfigurationAndExecutablePath = ObtainPackage();
|
||||
FilePath? configFile = null;
|
||||
if (_configFilePath != null)
|
||||
{
|
||||
configFile = new FilePath(_configFilePath);
|
||||
}
|
||||
|
||||
var commandName = toolConfigurationAndExecutablePath.Configuration.CommandName;
|
||||
_shellShimMaker.EnsureCommandNameUniqueness(commandName);
|
||||
using (var transactionScope = new TransactionScope())
|
||||
{
|
||||
var toolConfigurationAndExecutablePath = _toolPackageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: _packageId,
|
||||
packageVersion: _packageVersion,
|
||||
nugetconfig: configFile,
|
||||
targetframework: _framework,
|
||||
source: _source,
|
||||
verbosity: _verbosity);
|
||||
|
||||
_shellShimMaker.CreateShim(
|
||||
toolConfigurationAndExecutablePath.Executable,
|
||||
commandName);
|
||||
var commandName = toolConfigurationAndExecutablePath.Configuration.CommandName;
|
||||
|
||||
_environmentPathInstruction
|
||||
.PrintAddPathInstructionIfPathDoesNotExist();
|
||||
_shellShimMaker.CreateShim(
|
||||
toolConfigurationAndExecutablePath.Executable,
|
||||
commandName);
|
||||
|
||||
reporter.WriteLine(
|
||||
string.Format(LocalizableStrings.InstallationSucceeded, commandName));
|
||||
_environmentPathInstruction
|
||||
.PrintAddPathInstructionIfPathDoesNotExist();
|
||||
|
||||
_reporter.WriteLine(
|
||||
string.Format(LocalizableStrings.InstallationSucceeded, commandName));
|
||||
transactionScope.Complete();
|
||||
}
|
||||
}
|
||||
catch (PackageObtainException ex)
|
||||
{
|
||||
errorReporter.WriteLine(ex.Message.Red());
|
||||
errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailed, _packageId).Red());
|
||||
_errorReporter.WriteLine(ex.Message.Red());
|
||||
_errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailed, _packageId).Red());
|
||||
return 1;
|
||||
}
|
||||
catch (ToolConfigurationException ex)
|
||||
{
|
||||
errorReporter.WriteLine(
|
||||
_errorReporter.WriteLine(
|
||||
string.Format(
|
||||
LocalizableStrings.InvalidToolConfiguration,
|
||||
ex.Message).Red());
|
||||
errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailedContactAuthor, _packageId).Red());
|
||||
_errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailedContactAuthor, _packageId).Red());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private ToolConfigurationAndExecutablePath ObtainPackage()
|
||||
{
|
||||
FilePath? configFile = null;
|
||||
if (_configFilePath != null)
|
||||
{
|
||||
configFile = new FilePath(_configFilePath);
|
||||
}
|
||||
|
||||
return _toolPackageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: _packageId,
|
||||
packageVersion: _packageVersion,
|
||||
nugetconfig: configFile,
|
||||
targetframework: _framework,
|
||||
source: _source,
|
||||
verbosity: _verbosity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
|
|||
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
<trans-unit id="ToolPackageConflictPackageId">
|
||||
<source>Tool '{0}' is already installed.</source>
|
||||
<target state="new">Tool '{0}' is already installed.</target>
|
||||
<note />
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
|
@ -170,6 +170,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
|
|||
_directories.Add(path);
|
||||
CreateDirectoryInvoked = true;
|
||||
}
|
||||
|
||||
public void Delete(string path, bool recursive)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,6 +124,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
|
|||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
public void Delete(string path)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
}
|
||||
|
||||
private class NoPermissionDirectoryFake : IDirectory
|
||||
|
@ -153,6 +158,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
|
|||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
public void Delete(string path, bool recursive)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class Counter
|
||||
|
|
|
@ -197,6 +197,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
|
|||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
public void Delete(string path, bool recursive)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class FileMock : IFile
|
||||
|
@ -256,6 +261,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
|
|||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Delete(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class MockStream : MemoryStream
|
||||
|
|
|
@ -50,6 +50,11 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
_files[path] = content;
|
||||
}
|
||||
|
||||
public void Delete(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static FakeFile Empty => new FakeFile(new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Transactions;
|
||||
using System.Xml.Linq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.TestFramework;
|
||||
using Microsoft.DotNet.ToolPackage;
|
||||
using Microsoft.DotNet.Tools.Test.Utilities;
|
||||
using Microsoft.DotNet.Tools.Test.Utilities.Mock;
|
||||
using Microsoft.DotNet.Tools.Tests.ComponentMocks;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
using Xunit;
|
||||
|
@ -38,15 +39,16 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return;
|
||||
|
||||
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
|
||||
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
|
||||
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
|
||||
|
||||
var tmpFile = Path.Combine(TempRoot.Root, Path.GetRandomFileName());
|
||||
var tmpFile = new FilePath(Path.Combine(cleanFolderUnderTempRoot, Path.GetRandomFileName()));
|
||||
|
||||
shellShimMaker.CreateConfigFile(tmpFile, entryPoint, runner);
|
||||
|
||||
new FileInfo(tmpFile).Should().Exist();
|
||||
new FileInfo(tmpFile.Value).Should().Exist();
|
||||
|
||||
var generated = XDocument.Load(tmpFile);
|
||||
var generated = XDocument.Load(tmpFile.Value);
|
||||
|
||||
generated.Descendants("appSettings")
|
||||
.Descendants("add")
|
||||
|
@ -61,13 +63,33 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
{
|
||||
var outputDll = MakeHelloWorldExecutableDll();
|
||||
|
||||
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
|
||||
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
|
||||
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
shellShimMaker.CreateShim(
|
||||
new FilePath(outputDll.FullName),
|
||||
shellCommandName);
|
||||
var stdOut = ExecuteInShell(shellCommandName);
|
||||
shellShimMaker.CreateShim(outputDll, shellCommandName);
|
||||
|
||||
var stdOut = ExecuteInShell(shellCommandName, cleanFolderUnderTempRoot);
|
||||
|
||||
stdOut.Should().Contain("Hello World");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenAnExecutablePathItCanGenerateShimFileInTransaction()
|
||||
{
|
||||
var outputDll = MakeHelloWorldExecutableDll();
|
||||
|
||||
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
|
||||
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
using (var transactionScope = new TransactionScope())
|
||||
{
|
||||
shellShimMaker.CreateShim(outputDll, shellCommandName);
|
||||
transactionScope.Complete();
|
||||
}
|
||||
|
||||
var stdOut = ExecuteInShell(shellCommandName, cleanFolderUnderTempRoot);
|
||||
|
||||
stdOut.Should().Contain("Hello World");
|
||||
}
|
||||
|
@ -80,9 +102,8 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
var shellShimMaker = new ShellShimMaker(Path.Combine(TempRoot.Root, extraNonExistDirectory));
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
Action a = () => shellShimMaker.CreateShim(
|
||||
new FilePath(outputDll.FullName),
|
||||
shellCommandName);
|
||||
Action a = () => shellShimMaker.CreateShim(outputDll, shellCommandName);
|
||||
|
||||
a.ShouldNotThrow<DirectoryNotFoundException>();
|
||||
}
|
||||
|
||||
|
@ -94,14 +115,13 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
{
|
||||
var outputDll = MakeHelloWorldExecutableDll();
|
||||
|
||||
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
|
||||
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
|
||||
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
shellShimMaker.CreateShim(
|
||||
new FilePath(outputDll.FullName),
|
||||
shellCommandName);
|
||||
shellShimMaker.CreateShim(outputDll, shellCommandName);
|
||||
|
||||
var stdOut = ExecuteInShell(shellCommandName, arguments);
|
||||
var stdOut = ExecuteInShell(shellCommandName, cleanFolderUnderTempRoot, arguments);
|
||||
|
||||
for (int i = 0; i < expectedPassThru.Length; i++)
|
||||
{
|
||||
|
@ -115,17 +135,17 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
public void GivenAnExecutablePathWithExistingSameNameShimItThrows(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
MakeNameConflictingCommand(TempRoot.Root, shellCommandName);
|
||||
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
|
||||
MakeNameConflictingCommand(cleanFolderUnderTempRoot, shellCommandName);
|
||||
|
||||
IShellShimMaker shellShimMaker;
|
||||
if (testMockBehaviorIsInSync)
|
||||
{
|
||||
shellShimMaker = new ShellShimMakerMock(TempRoot.Root);
|
||||
shellShimMaker = new ShellShimMakerMock(cleanFolderUnderTempRoot);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellShimMaker = new ShellShimMaker(TempRoot.Root);
|
||||
shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
|
||||
}
|
||||
|
||||
Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName);
|
||||
|
@ -138,18 +158,89 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenAnExecutablePathWithoutExistingSameNameShimItShouldNotThrow(bool testMockBehaviorIsInSync)
|
||||
public void GivenAnExecutablePathWithExistingSameNameShimItRollsBack(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
var pathToShim = GetNewCleanFolderUnderTempRoot();
|
||||
MakeNameConflictingCommand(pathToShim, shellCommandName);
|
||||
|
||||
IShellShimMaker shellShimMaker;
|
||||
if (testMockBehaviorIsInSync)
|
||||
{
|
||||
shellShimMaker = new ShellShimMakerMock(TempRoot.Root);
|
||||
shellShimMaker = new ShellShimMakerMock(pathToShim);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellShimMaker = new ShellShimMaker(TempRoot.Root);
|
||||
shellShimMaker = new ShellShimMaker(pathToShim);
|
||||
}
|
||||
|
||||
Action a = () =>
|
||||
{
|
||||
using (var t = new TransactionScope())
|
||||
{
|
||||
shellShimMaker.CreateShim(new FilePath("dummy.dll"), shellCommandName);
|
||||
|
||||
t.Complete();
|
||||
}
|
||||
};
|
||||
a.ShouldThrow<GracefulException>();
|
||||
|
||||
Directory.GetFiles(pathToShim).Should().HaveCount(1, "there is only intent conflicted command");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenAnExecutablePathErrorHappensItRollsBack(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
|
||||
var pathToShim = GetNewCleanFolderUnderTempRoot();
|
||||
|
||||
IShellShimMaker shellShimMaker;
|
||||
if (testMockBehaviorIsInSync)
|
||||
{
|
||||
shellShimMaker = new ShellShimMakerMock(pathToShim);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellShimMaker = new ShellShimMaker(pathToShim);
|
||||
}
|
||||
|
||||
Action intendedError = () => throw new PackageObtainException();
|
||||
|
||||
Action a = () =>
|
||||
{
|
||||
using (var t = new TransactionScope())
|
||||
{
|
||||
shellShimMaker.CreateShim(new FilePath("dummy.dll"), shellCommandName);
|
||||
|
||||
intendedError();
|
||||
t.Complete();
|
||||
}
|
||||
};
|
||||
a.ShouldThrow<PackageObtainException>();
|
||||
|
||||
Directory.GetFiles(pathToShim).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenAnExecutablePathWithoutExistingSameNameShimItShouldNotThrow(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
|
||||
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
|
||||
|
||||
IShellShimMaker shellShimMaker;
|
||||
if (testMockBehaviorIsInSync)
|
||||
{
|
||||
shellShimMaker = new ShellShimMakerMock(cleanFolderUnderTempRoot);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
|
||||
}
|
||||
|
||||
Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName);
|
||||
|
@ -158,16 +249,21 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
|
||||
private static void MakeNameConflictingCommand(string pathToPlaceShim, string shellCommandName)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
shellCommandName = shellCommandName + ".exe";
|
||||
}
|
||||
|
||||
File.WriteAllText(Path.Combine(pathToPlaceShim, shellCommandName), string.Empty);
|
||||
}
|
||||
|
||||
private string ExecuteInShell(string shellCommandName, string arguments = "")
|
||||
private string ExecuteInShell(string shellCommandName, string cleanFolderUnderTempRoot, string arguments = "")
|
||||
{
|
||||
ProcessStartInfo processStartInfo;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var file = Path.Combine(TempRoot.Root, shellCommandName + ".exe");
|
||||
var file = Path.Combine(cleanFolderUnderTempRoot, shellCommandName + ".exe");
|
||||
processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = file,
|
||||
|
@ -186,7 +282,7 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
}
|
||||
|
||||
_output.WriteLine($"Launching '{processStartInfo.FileName} {processStartInfo.Arguments}'");
|
||||
processStartInfo.WorkingDirectory = TempRoot.Root;
|
||||
processStartInfo.WorkingDirectory = cleanFolderUnderTempRoot;
|
||||
processStartInfo.EnvironmentVariables["PATH"] = Path.GetDirectoryName(new Muxer().MuxerPath);
|
||||
|
||||
processStartInfo.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);
|
||||
|
@ -196,7 +292,7 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
return stdOut ?? "";
|
||||
}
|
||||
|
||||
private static FileInfo MakeHelloWorldExecutableDll()
|
||||
private static FilePath MakeHelloWorldExecutableDll()
|
||||
{
|
||||
const string testAppName = "TestAppSimple";
|
||||
const string emptySpaceToTestSpaceInPath = " ";
|
||||
|
@ -212,7 +308,15 @@ namespace Microsoft.DotNet.ShellShim.Tests
|
|||
.GetDirectories().Single()
|
||||
.GetFile($"{testAppName}.dll");
|
||||
|
||||
return outputDll;
|
||||
return new FilePath(outputDll.FullName);
|
||||
}
|
||||
|
||||
private static string GetNewCleanFolderUnderTempRoot()
|
||||
{
|
||||
DirectoryInfo CleanFolderUnderTempRoot = new DirectoryInfo(Path.Combine(TempRoot.Root, "cleanfolder" + Path.GetRandomFileName()));
|
||||
CleanFolderUnderTempRoot.Create();
|
||||
|
||||
return CleanFolderUnderTempRoot.FullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Transactions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.DotNet.Tools.Test.Utilities;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
|
@ -58,8 +59,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
bundledTargetFrameworkMoniker: new Lazy<string>(),
|
||||
projectRestorer: new ProjectRestorer(reporter));
|
||||
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath = packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework);
|
||||
|
@ -109,6 +109,43 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
File.Delete(executable.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenNugetConfigAndPackageNameAndVersionAndTargetFrameworkWhenCallItCanDownloadThePackageInTransaction(
|
||||
bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var reporter = new BufferedReporter();
|
||||
FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var packageObtainer =
|
||||
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
|
||||
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath;
|
||||
|
||||
using (var transactionScope = new TransactionScope())
|
||||
{
|
||||
toolConfigurationAndExecutablePath
|
||||
= packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
nugetconfig: nugetConfigPath,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
transactionScope.Complete();
|
||||
}
|
||||
|
||||
reporter.Lines.Should().BeEmpty();
|
||||
|
||||
FilePath executable = toolConfigurationAndExecutablePath.Executable;
|
||||
File.Exists(executable.Value)
|
||||
.Should()
|
||||
.BeTrue(executable + " should have the executable");
|
||||
|
||||
File.Delete(executable.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
|
@ -122,7 +159,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
var packageObtainer =
|
||||
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
|
||||
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutableDirectory =
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
|
@ -138,7 +175,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
/dependency2 package id/
|
||||
/project.assets.json
|
||||
*/
|
||||
var assetJsonPath = toolConfigurationAndExecutableDirectory
|
||||
var assetJsonPath = toolConfigurationAndExecutablePath
|
||||
.Executable
|
||||
.GetDirectoryPath()
|
||||
.GetParentPath()
|
||||
|
@ -177,7 +214,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
IToolPackageObtainer packageObtainer;
|
||||
if (testMockBehaviorIsInSync)
|
||||
{
|
||||
packageObtainer = new ToolPackageObtainerMock();
|
||||
packageObtainer = new ToolPackageObtainerMock(toolsPath: toolsPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -212,7 +249,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
public void GivenAllButNoPackageVersionItCanDownloadThePackage(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var reporter = new BufferedReporter();
|
||||
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var packageObtainer =
|
||||
|
@ -235,35 +272,6 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
File.Delete(executable.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenAllButNoPackageVersionAndInvokeTwiceItShouldNotThrow(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var reporter = new BufferedReporter();
|
||||
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var packageObtainer =
|
||||
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
|
||||
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
nugetconfig: nugetConfigPath,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
reporter.Lines.Should().BeEmpty();
|
||||
|
||||
Action secondCall = () => packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
nugetconfig: nugetConfigPath,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
reporter.Lines.Should().BeEmpty();
|
||||
|
||||
secondCall.ShouldNotThrow();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
|
@ -292,7 +300,8 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
toolsPath: toolsPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -334,6 +343,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
var nonExistNugetConfigFile = new FilePath("NonExistent.file");
|
||||
Action a = () =>
|
||||
{
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
|
@ -358,16 +368,21 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
var reporter = new BufferedReporter();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var packageObtainer = ConstructDefaultPackageObtainer(toolsPath, reporter);
|
||||
var toolConfigurationAndExecutableDirectory = packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework,
|
||||
source:GetTestLocalFeedPath());
|
||||
var packageObtainer = ConstructDefaultPackageObtainer(
|
||||
toolsPath,
|
||||
reporter,
|
||||
testMockBehaviorIsInSync: testMockBehaviorIsInSync,
|
||||
addSourceFeedWithFilePath: GetTestLocalFeedPath());
|
||||
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework,
|
||||
source: GetTestLocalFeedPath());
|
||||
|
||||
reporter.Lines.Should().BeEmpty();
|
||||
|
||||
var executable = toolConfigurationAndExecutableDirectory.Executable;
|
||||
var executable = toolConfigurationAndExecutablePath.Executable;
|
||||
|
||||
File.Exists(executable.Value)
|
||||
.Should()
|
||||
|
@ -376,6 +391,158 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
File.Delete(executable.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenFailedRestoreItCanRollBack(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var reporter = new BufferedReporter();
|
||||
var packageObtainer = ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync);
|
||||
|
||||
try
|
||||
{
|
||||
using (var t = new TransactionScope())
|
||||
{
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: "non exist package id",
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
t.Complete();
|
||||
}
|
||||
}
|
||||
catch (PackageObtainException)
|
||||
{
|
||||
// catch the intent error
|
||||
}
|
||||
|
||||
AssertRollBack(toolsPath);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GiveSucessRestoreButFailedOnNextStepItCanRollBack(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
var reporter = new BufferedReporter();
|
||||
var packageObtainer = ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync);
|
||||
|
||||
void FailedStepAfterSuccessRestore() => throw new GracefulException("simulated error");
|
||||
|
||||
try
|
||||
{
|
||||
using (var t = new TransactionScope())
|
||||
{
|
||||
ToolConfigurationAndExecutablePath obtainAndReturnExecutablePathtransactional
|
||||
= packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
FailedStepAfterSuccessRestore();
|
||||
t.Complete();
|
||||
}
|
||||
}
|
||||
catch (GracefulException)
|
||||
{
|
||||
// catch the simulated error
|
||||
}
|
||||
|
||||
AssertRollBack(toolsPath);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenAllButNoPackageVersionAndInvokeTwiceItShouldNotThrow(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var reporter = new BufferedReporter();
|
||||
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var packageObtainer =
|
||||
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
|
||||
|
||||
try
|
||||
{
|
||||
using (var t = new TransactionScope())
|
||||
{
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
packageVersion: TestPackageVersion,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
t.Complete();
|
||||
}
|
||||
}
|
||||
catch (PackageObtainException)
|
||||
{
|
||||
// catch the simulated error
|
||||
}
|
||||
|
||||
AssertRollBack(toolsPath);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GivenAllButNoPackageVersionAndInvokeTwiceInTransactionItShouldRollback(bool testMockBehaviorIsInSync)
|
||||
{
|
||||
var reporter = new BufferedReporter();
|
||||
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
|
||||
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
|
||||
|
||||
var packageObtainer =
|
||||
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
|
||||
|
||||
packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
nugetconfig: nugetConfigPath,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
reporter.Lines.Should().BeEmpty();
|
||||
|
||||
Action secondCall = () => packageObtainer.ObtainAndReturnExecutablePath(
|
||||
packageId: TestPackageId,
|
||||
nugetconfig: nugetConfigPath,
|
||||
targetframework: _testTargetframework);
|
||||
|
||||
reporter.Lines.Should().BeEmpty();
|
||||
|
||||
secondCall.ShouldThrow<PackageObtainException>();
|
||||
|
||||
Directory.Exists(Path.Combine(toolsPath, TestPackageId))
|
||||
.Should().BeTrue("The result of first one is still here");
|
||||
|
||||
Directory.GetDirectories(Path.Combine(toolsPath, ".stage"))
|
||||
.Should().BeEmpty("nothing in stage folder, already rolled back");
|
||||
}
|
||||
|
||||
private static void AssertRollBack(string toolsPath)
|
||||
{
|
||||
if (!Directory.Exists(toolsPath))
|
||||
{
|
||||
return; // nothing at all
|
||||
}
|
||||
|
||||
Directory.GetFiles(toolsPath).Should().BeEmpty();
|
||||
Directory.GetDirectories(toolsPath)
|
||||
.Should().NotContain(d => !new DirectoryInfo(d).Name.Equals(".stage"),
|
||||
"no broken folder, exclude stage folder");
|
||||
|
||||
Directory.GetDirectories(Path.Combine(toolsPath, ".stage"))
|
||||
.Should().BeEmpty("nothing in stage folder");
|
||||
}
|
||||
|
||||
private static readonly Func<FilePath> GetUniqueTempProjectPathEachTest = () =>
|
||||
{
|
||||
var tempProjectDirectory =
|
||||
|
@ -412,7 +579,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, toolsPath: toolsPath);
|
||||
}
|
||||
|
||||
if (addSourceFeedWithFilePath != null)
|
||||
|
@ -422,7 +589,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
{
|
||||
new MockFeed
|
||||
{
|
||||
Type = MockFeedType.ExplicitNugetConfig,
|
||||
Type = MockFeedType.Source,
|
||||
Uri = addSourceFeedWithFilePath,
|
||||
Packages = new List<MockFeedPackage>
|
||||
{
|
||||
|
@ -433,10 +600,11 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
toolsPath: toolsPath);
|
||||
}
|
||||
|
||||
return new ToolPackageObtainerMock();
|
||||
return new ToolPackageObtainerMock(toolsPath: toolsPath);
|
||||
}
|
||||
|
||||
return new ToolPackageObtainer(
|
||||
|
@ -465,7 +633,6 @@ namespace Microsoft.DotNet.ToolPackage.Tests
|
|||
}
|
||||
|
||||
private static string GetTestLocalFeedPath() => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed");
|
||||
|
||||
private readonly string _testTargetframework = BundledTargetFramework.GetTargetFrameworkMoniker();
|
||||
private const string TestPackageVersion = "1.0.4";
|
||||
private const string TestPackageId = "global.tool.console.demo";
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Transactions;
|
||||
using Microsoft.DotNet.Cli;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.ShellShim;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
|
@ -18,12 +22,56 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
|
|||
public ShellShimMakerMock(string pathToPlaceShim, IFileSystem fileSystem = null)
|
||||
{
|
||||
_pathToPlaceShim =
|
||||
pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
|
||||
pathToPlaceShim ??
|
||||
throw new ArgumentNullException(nameof(pathToPlaceShim));
|
||||
|
||||
_fileSystem = fileSystem ?? new FileSystemWrapper();
|
||||
}
|
||||
|
||||
public void EnsureCommandNameUniqueness(string shellCommandName)
|
||||
{
|
||||
if (_fileSystem.File.Exists(GetShimPath(shellCommandName).Value))
|
||||
{
|
||||
throw new GracefulException(
|
||||
string.Format(CommonLocalizableStrings.FailInstallToolSameName,
|
||||
shellCommandName));
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateShim(FilePath packageExecutable, string shellCommandName)
|
||||
{
|
||||
var createShimTransaction = new CreateShimTransaction(
|
||||
createShim: locationOfShimDuringTransaction =>
|
||||
{
|
||||
EnsureCommandNameUniqueness(shellCommandName);
|
||||
PlaceShim(packageExecutable, shellCommandName, locationOfShimDuringTransaction);
|
||||
},
|
||||
rollback: locationOfShimDuringTransaction =>
|
||||
{
|
||||
foreach (FilePath f in locationOfShimDuringTransaction)
|
||||
{
|
||||
if (File.Exists(f.Value))
|
||||
{
|
||||
File.Delete(f.Value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
using (var transactionScope = new TransactionScope())
|
||||
{
|
||||
Transaction.Current.EnlistVolatile(createShimTransaction, EnlistmentOptions.None);
|
||||
createShimTransaction.CreateShim();
|
||||
|
||||
transactionScope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string shellCommandName)
|
||||
{
|
||||
File.Delete(GetShimPath(shellCommandName).Value);
|
||||
}
|
||||
|
||||
private void PlaceShim(FilePath packageExecutable, string shellCommandName, List<FilePath> locationOfShimDuringTransaction)
|
||||
{
|
||||
var fakeshim = new FakeShim
|
||||
{
|
||||
|
@ -32,18 +80,20 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
|
|||
};
|
||||
var script = JsonConvert.SerializeObject(fakeshim);
|
||||
|
||||
FilePath scriptPath = new FilePath(Path.Combine(_pathToPlaceShim, shellCommandName));
|
||||
FilePath scriptPath = GetShimPath(shellCommandName);
|
||||
_fileSystem.File.WriteAllText(scriptPath.Value, script);
|
||||
locationOfShimDuringTransaction.Add(scriptPath);
|
||||
}
|
||||
|
||||
public void EnsureCommandNameUniqueness(string shellCommandName)
|
||||
private FilePath GetShimPath(string shellCommandName)
|
||||
{
|
||||
if (_fileSystem.File.Exists(Path.Combine(_pathToPlaceShim, shellCommandName)))
|
||||
var scriptPath = Path.Combine(_pathToPlaceShim, shellCommandName);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
throw new GracefulException(
|
||||
string.Format(CommonLocalizableStrings.FailInstallToolSameName,
|
||||
shellCommandName));
|
||||
scriptPath += ".exe";
|
||||
}
|
||||
|
||||
return new FilePath(scriptPath);
|
||||
}
|
||||
|
||||
public class FakeShim
|
||||
|
|
|
@ -5,6 +5,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Transactions;
|
||||
using Microsoft.DotNet.Cli;
|
||||
using Microsoft.DotNet.ToolPackage;
|
||||
using Microsoft.Extensions.EnvironmentAbstractions;
|
||||
|
||||
|
@ -12,20 +14,27 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
|
|||
{
|
||||
internal class ToolPackageObtainerMock : IToolPackageObtainer
|
||||
{
|
||||
private readonly Action _beforeRunObtain;
|
||||
private readonly string _toolsPath;
|
||||
public const string FakeEntrypointName = "SimulatorEntryPoint.dll";
|
||||
public const string FakeCommandName = "SimulatorCommand";
|
||||
private readonly Action _beforeRunObtain;
|
||||
private readonly Action _duringObtain;
|
||||
private static IFileSystem _fileSystem;
|
||||
private string _fakeExecutableDirectory;
|
||||
private List<MockFeed> _mockFeeds;
|
||||
private string _packageIdVersionDirectory;
|
||||
|
||||
public ToolPackageObtainerMock(
|
||||
IFileSystem fileSystemWrapper = null,
|
||||
bool useDefaultFeed = true,
|
||||
IEnumerable<MockFeed> additionalFeeds = null,
|
||||
Action beforeRunObtain = null)
|
||||
Action beforeRunObtain = null,
|
||||
Action duringObtain = null,
|
||||
string toolsPath = null)
|
||||
{
|
||||
_beforeRunObtain = beforeRunObtain ?? (() => { });
|
||||
_toolsPath = toolsPath ?? "toolsPath";
|
||||
_beforeRunObtain = beforeRunObtain ?? (() => {});
|
||||
_duringObtain = duringObtain ?? (() => {});
|
||||
_fileSystem = fileSystemWrapper ?? new FileSystemWrapper();
|
||||
_mockFeeds = new List<MockFeed>();
|
||||
|
||||
|
@ -34,14 +43,14 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
|
|||
_mockFeeds.Add(new MockFeed
|
||||
{
|
||||
Type = MockFeedType.FeedFromLookUpNugetConfig,
|
||||
Packages = new List<MockFeedPackage>
|
||||
{
|
||||
new MockFeedPackage
|
||||
Packages = new List<MockFeedPackage>
|
||||
{
|
||||
PackageId = "global.tool.console.demo",
|
||||
Version = "1.0.4"
|
||||
new MockFeedPackage
|
||||
{
|
||||
PackageId = "global.tool.console.demo",
|
||||
Version = "1.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -59,42 +68,97 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
|
|||
string source = null,
|
||||
string verbosity = null)
|
||||
{
|
||||
_beforeRunObtain();
|
||||
var stagedFile = Path.Combine(_toolsPath, ".stage", Path.GetRandomFileName());
|
||||
bool afterStage = false;
|
||||
|
||||
PickFeedByNugetConfig(nugetconfig);
|
||||
PickFeedBySource(source);
|
||||
var toolPackageObtainTransaction = new ToolPackageObtainTransaction(
|
||||
obtainAndReturnExecutablePath: (_) =>
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(_toolsPath, packageId)))
|
||||
{
|
||||
throw new PackageObtainException(
|
||||
string.Format(CommonLocalizableStrings.ToolPackageConflictPackageId, packageId));
|
||||
}
|
||||
|
||||
MockFeedPackage package = _mockFeeds
|
||||
.SelectMany(f => f.Packages)
|
||||
.Where(p => MatchPackageVersion(p, packageId, packageVersion)).OrderByDescending(p => p.Version)
|
||||
.FirstOrDefault();
|
||||
_beforeRunObtain();
|
||||
|
||||
if (package == null)
|
||||
PickFeedByNugetConfig(nugetconfig);
|
||||
PickFeedBySource(source);
|
||||
|
||||
MockFeedPackage package = _mockFeeds
|
||||
.SelectMany(f => f.Packages)
|
||||
.Where(p => MatchPackageVersion(p, packageId, packageVersion)).OrderByDescending(p => p.Version)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (package == null)
|
||||
{
|
||||
throw new PackageObtainException("simulated cannot find package");
|
||||
}
|
||||
|
||||
packageVersion = package.Version;
|
||||
targetframework = targetframework ?? "targetframework";
|
||||
|
||||
_packageIdVersionDirectory = Path.Combine(_toolsPath, packageId, packageVersion);
|
||||
|
||||
_fakeExecutableDirectory = Path.Combine(_packageIdVersionDirectory,
|
||||
packageId, packageVersion, "morefolders", "tools",
|
||||
targetframework);
|
||||
|
||||
SimulateStageFile();
|
||||
_duringObtain();
|
||||
|
||||
_fileSystem.File.Delete(stagedFile);
|
||||
afterStage = true;
|
||||
|
||||
_fileSystem.Directory.CreateDirectory(_packageIdVersionDirectory);
|
||||
_fileSystem.File.CreateEmptyFile(Path.Combine(_packageIdVersionDirectory, "project.assets.json"));
|
||||
_fileSystem.Directory.CreateDirectory(_fakeExecutableDirectory);
|
||||
var fakeExecutable = Path.Combine(_fakeExecutableDirectory, FakeEntrypointName);
|
||||
_fileSystem.File.CreateEmptyFile(fakeExecutable);
|
||||
|
||||
return new ToolConfigurationAndExecutablePath(
|
||||
toolConfiguration: new ToolConfiguration(FakeCommandName, FakeEntrypointName),
|
||||
executable : new FilePath(fakeExecutable));;
|
||||
},
|
||||
|
||||
rollback: (_) =>
|
||||
{
|
||||
if (afterStage == false)
|
||||
{
|
||||
if (_fileSystem.File.Exists(stagedFile))
|
||||
{
|
||||
_fileSystem.File.Delete(stagedFile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_fileSystem.Directory.Exists(Path.Combine(_toolsPath, packageId)))
|
||||
{
|
||||
_fileSystem.Directory.Delete(Path.Combine(_toolsPath, packageId), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
using(var transactionScope = new TransactionScope())
|
||||
{
|
||||
throw new PackageObtainException("simulated cannot find package");
|
||||
Transaction.Current.EnlistVolatile(toolPackageObtainTransaction, EnlistmentOptions.None);
|
||||
var toolConfigurationAndExecutablePath = toolPackageObtainTransaction.ObtainAndReturnExecutablePath();
|
||||
|
||||
transactionScope.Complete();
|
||||
return toolConfigurationAndExecutablePath;
|
||||
}
|
||||
}
|
||||
|
||||
private void SimulateStageFile()
|
||||
{
|
||||
var stageDirectory = Path.Combine(_toolsPath, ".stage");
|
||||
if (!_fileSystem.Directory.Exists(stageDirectory))
|
||||
{
|
||||
_fileSystem.Directory.CreateDirectory(stageDirectory);
|
||||
}
|
||||
|
||||
packageVersion = package.Version;
|
||||
targetframework = targetframework ?? "targetframework";
|
||||
|
||||
var packageIdVersionDirectory = Path.Combine("toolPath", packageId, packageVersion);
|
||||
|
||||
_fakeExecutableDirectory = Path.Combine(packageIdVersionDirectory,
|
||||
packageId, packageVersion, "morefolders", "tools",
|
||||
targetframework);
|
||||
var fakeExecutable = Path.Combine(_fakeExecutableDirectory, FakeEntrypointName);
|
||||
|
||||
if (!_fileSystem.Directory.Exists(_fakeExecutableDirectory))
|
||||
{
|
||||
_fileSystem.Directory.CreateDirectory(_fakeExecutableDirectory);
|
||||
}
|
||||
|
||||
_fileSystem.File.CreateEmptyFile(Path.Combine(packageIdVersionDirectory, "project.assets.json"));
|
||||
_fileSystem.File.CreateEmptyFile(fakeExecutable);
|
||||
|
||||
return new ToolConfigurationAndExecutablePath(
|
||||
toolConfiguration: new ToolConfiguration(FakeCommandName, FakeEntrypointName),
|
||||
executable: new FilePath(fakeExecutable));
|
||||
_fileSystem.File.CreateEmptyFile(Path.Combine(stageDirectory, "stagedfile"));
|
||||
}
|
||||
|
||||
private void PickFeedBySource(string source)
|
||||
|
|
|
@ -106,6 +106,16 @@ namespace Microsoft.Extensions.DependencyModel.Tests
|
|||
{
|
||||
_files[path] = content;
|
||||
}
|
||||
|
||||
public void Delete(string path)
|
||||
{
|
||||
if (!Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_files.Remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
private class DirectoryMock : IDirectory
|
||||
|
@ -143,6 +153,22 @@ namespace Microsoft.Extensions.DependencyModel.Tests
|
|||
{
|
||||
_files.Add(path, path);
|
||||
}
|
||||
|
||||
public void Delete(string path, bool recursive)
|
||||
{
|
||||
if (!recursive && Exists(path) == true)
|
||||
{
|
||||
if (_files.Keys.Where(k => k.StartsWith(path)).Count() > 1)
|
||||
{
|
||||
throw new IOException("The directory is not empty");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var k in _files.Keys.Where(k => k.StartsWith(path)).ToList())
|
||||
{
|
||||
_files.Remove(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TemporaryDirectoryMock : ITemporaryDirectoryMock
|
||||
|
|
|
@ -19,6 +19,7 @@ using Newtonsoft.Json;
|
|||
using Xunit;
|
||||
using Parser = Microsoft.DotNet.Cli.Parser;
|
||||
using LocalizableStrings = Microsoft.DotNet.Tools.Install.Tool.LocalizableStrings;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
||||
{
|
||||
|
@ -32,20 +33,22 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
private readonly ParseResult _parseResult;
|
||||
private readonly BufferedReporter _reporter;
|
||||
private const string PathToPlaceShim = "pathToPlace";
|
||||
private const string PathToPlacePackages = PathToPlaceShim + "pkg";
|
||||
private const string PackageId = "global.tool.console.demo";
|
||||
|
||||
public InstallToolCommandTests()
|
||||
{
|
||||
_fileSystemWrapper = new FileSystemMockBuilder().Build();
|
||||
_toolPackageObtainerMock = new ToolPackageObtainerMock(_fileSystemWrapper);
|
||||
_toolPackageObtainerMock = new ToolPackageObtainerMock(_fileSystemWrapper, toolsPath: PathToPlacePackages);
|
||||
_shellShimMakerMock = new ShellShimMakerMock(PathToPlaceShim, _fileSystemWrapper);
|
||||
_reporter = new BufferedReporter();
|
||||
_environmentPathInstructionMock =
|
||||
new EnvironmentPathInstructionMock(_reporter, PathToPlaceShim);
|
||||
|
||||
ParseResult result = Parser.Instance.Parse("dotnet install tool -g global.tool.console.demo");
|
||||
ParseResult result = Parser.Instance.Parse($"dotnet install tool -g {PackageId}");
|
||||
_appliedCommand = result["dotnet"]["install"]["tool"];
|
||||
var parser = Parser.Instance;
|
||||
_parseResult = parser.ParseFrom("dotnet install", new[] {"tool", "global.tool.console.demo"});
|
||||
_parseResult = parser.ParseFrom("dotnet install", new[] {"tool", PackageId});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -60,12 +63,9 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
installToolCommand.Execute().Should().Be(0);
|
||||
|
||||
// It is hard to simulate shell behavior. Only Assert shim can point to executable dll
|
||||
_fileSystemWrapper.File.Exists(Path.Combine("pathToPlace", ToolPackageObtainerMock.FakeCommandName))
|
||||
.Should().BeTrue();
|
||||
_fileSystemWrapper.File.Exists(ExpectedCommandPath()).Should().BeTrue();
|
||||
var deserializedFakeShim = JsonConvert.DeserializeObject<ShellShimMakerMock.FakeShim>(
|
||||
_fileSystemWrapper.File.ReadAllText(
|
||||
Path.Combine("pathToPlace",
|
||||
ToolPackageObtainerMock.FakeCommandName)));
|
||||
_fileSystemWrapper.File.ReadAllText(ExpectedCommandPath()));
|
||||
_fileSystemWrapper.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue();
|
||||
}
|
||||
|
||||
|
@ -73,11 +73,10 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
public void WhenRunWithPackageIdWithSourceItShouldCreateValidShim()
|
||||
{
|
||||
const string sourcePath = "http://mysouce.com";
|
||||
ParseResult result = Parser.Instance.Parse($"dotnet install tool -g global.tool.console.demo --source {sourcePath}");
|
||||
ParseResult result = Parser.Instance.Parse($"dotnet install tool -g {PackageId} --source {sourcePath}");
|
||||
AppliedOption appliedCommand = result["dotnet"]["install"]["tool"];
|
||||
const string packageId = "global.tool.console.demo";
|
||||
ParseResult parseResult =
|
||||
Parser.Instance.ParseFrom("dotnet install", new[] {"tool", packageId, "--source", sourcePath});
|
||||
Parser.Instance.ParseFrom("dotnet install", new[] { "tool", PackageId, "--source", sourcePath });
|
||||
|
||||
var installToolCommand = new InstallToolCommand(appliedCommand,
|
||||
parseResult,
|
||||
|
@ -91,7 +90,7 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
{
|
||||
new MockFeedPackage
|
||||
{
|
||||
PackageId = packageId,
|
||||
PackageId = PackageId,
|
||||
Version = "1.0.4"
|
||||
}
|
||||
}
|
||||
|
@ -103,13 +102,11 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
installToolCommand.Execute().Should().Be(0);
|
||||
|
||||
// It is hard to simulate shell behavior. Only Assert shim can point to executable dll
|
||||
_fileSystemWrapper.File.Exists(Path.Combine("pathToPlace", ToolPackageObtainerMock.FakeCommandName))
|
||||
.Should().BeTrue();
|
||||
_fileSystemWrapper.File.Exists(ExpectedCommandPath())
|
||||
.Should().BeTrue();
|
||||
ShellShimMakerMock.FakeShim deserializedFakeShim =
|
||||
JsonConvert.DeserializeObject<ShellShimMakerMock.FakeShim>(
|
||||
_fileSystemWrapper.File.ReadAllText(
|
||||
Path.Combine("pathToPlace",
|
||||
ToolPackageObtainerMock.FakeCommandName)));
|
||||
_fileSystemWrapper.File.ReadAllText(ExpectedCommandPath()));
|
||||
_fileSystemWrapper.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue();
|
||||
}
|
||||
|
||||
|
@ -157,6 +154,49 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
.Contain(string.Format(LocalizableStrings.ToolInstallationFailed, "global.tool.console.demo"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenFailedPackageObtainWhenRunWithPackageIdItShouldHaveNoBrokenFolderOnDisk()
|
||||
{
|
||||
var toolPackageObtainerSimulatorThatThrows
|
||||
= new ToolPackageObtainerMock(
|
||||
_fileSystemWrapper, true, null,
|
||||
duringObtain: () => throw new PackageObtainException("Simulated error"),
|
||||
toolsPath: PathToPlacePackages);
|
||||
var installToolCommand = new InstallToolCommand(
|
||||
_appliedCommand,
|
||||
_parseResult,
|
||||
toolPackageObtainerSimulatorThatThrows,
|
||||
_shellShimMakerMock,
|
||||
_environmentPathInstructionMock,
|
||||
_reporter);
|
||||
|
||||
installToolCommand.Execute();
|
||||
|
||||
_fileSystemWrapper.Directory.Exists(Path.Combine(PathToPlacePackages, PackageId)).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenCreateShimItShouldHaveNoBrokenFolderOnDisk()
|
||||
{
|
||||
_fileSystemWrapper.File.CreateEmptyFile(ExpectedCommandPath()); // Create conflict shim
|
||||
var toolPackageObtainerSimulatorThatThrows
|
||||
= new ToolPackageObtainerMock(
|
||||
_fileSystemWrapper, true, null,
|
||||
toolsPath: PathToPlacePackages);
|
||||
var installToolCommand = new InstallToolCommand(
|
||||
_appliedCommand,
|
||||
_parseResult,
|
||||
toolPackageObtainerSimulatorThatThrows,
|
||||
_shellShimMakerMock,
|
||||
_environmentPathInstructionMock,
|
||||
_reporter);
|
||||
|
||||
Action a = () => installToolCommand.Execute();
|
||||
a.ShouldThrow<GracefulException>();
|
||||
|
||||
_fileSystemWrapper.Directory.Exists(Path.Combine(PathToPlacePackages, PackageId)).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenInCorrectToolConfigurationWhenRunWithPackageIdItShouldFail()
|
||||
{
|
||||
|
@ -208,5 +248,13 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
|
|||
.Single().Should()
|
||||
.Contain(string.Format(LocalizableStrings.InstallationSucceeded, "SimulatorCommand"));
|
||||
}
|
||||
|
||||
private static string ExpectedCommandPath()
|
||||
{
|
||||
var extension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
|
||||
return Path.Combine(
|
||||
"pathToPlace",
|
||||
ToolPackageObtainerMock.FakeCommandName + extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue