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:
William Lee 2018-02-06 13:38:06 -08:00 committed by GitHub
parent 38e452204c
commit 5fa558a2ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 995 additions and 207 deletions

View file

@ -47,5 +47,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
{
Directory.CreateDirectory(path);
}
public void Delete(string path, bool recursive)
{
Directory.Delete(path, recursive);
}
}
}

View file

@ -45,5 +45,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
{
File.WriteAllText(path, content);
}
public void Delete(string path)
{
File.Delete(path);
}
}
}

View file

@ -16,5 +16,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
string GetDirectoryFullName(string path);
void CreateDirectory(string path);
void Delete(string path, bool recursive);
}
}

View file

@ -24,5 +24,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
void CreateEmptyFile(string path);
void WriteAllText(string path, string content);
void Delete(string path);
}
}

View file

@ -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>

View 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();
}
}
}

View file

@ -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))

View 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();
}
}
}

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -170,6 +170,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
_directories.Add(path);
CreateDirectoryInvoked = true;
}
public void Delete(string path, bool recursive)
{
throw new NotImplementedException();
}
}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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>());
}
}

View file

@ -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;
}
}
}

View file

@ -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";

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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);
}
}
}