Skip to content
This repository was archived by the owner on Apr 20, 2023. It is now read-only.

Commit 5fa558a

Browse files
author
William Lee
authored
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.
1 parent 38e4522 commit 5fa558a

33 files changed

+993
-205
lines changed

src/Microsoft.DotNet.InternalAbstractions/DirectoryWrapper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,10 @@ public void CreateDirectory(string path)
4747
{
4848
Directory.CreateDirectory(path);
4949
}
50+
51+
public void Delete(string path, bool recursive)
52+
{
53+
Directory.Delete(path, recursive);
54+
}
5055
}
5156
}

src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,10 @@ public void WriteAllText(string path, string content)
4545
{
4646
File.WriteAllText(path, content);
4747
}
48+
49+
public void Delete(string path)
50+
{
51+
File.Delete(path);
52+
}
4853
}
4954
}

src/Microsoft.DotNet.InternalAbstractions/IDirectory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ internal interface IDirectory
1616
string GetDirectoryFullName(string path);
1717

1818
void CreateDirectory(string path);
19+
20+
void Delete(string path, bool recursive);
1921
}
2022
}

src/Microsoft.DotNet.InternalAbstractions/IFile.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ Stream OpenFile(
2424
void CreateEmptyFile(string path);
2525

2626
void WriteAllText(string path, string content);
27+
28+
void Delete(string path);
2729
}
2830
}

src/dotnet/CommonLocalizableStrings.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,4 +588,7 @@ Output: {1}</value>
588588
<data name="ToolPackageMissingSettingsFile" xml:space="preserve">
589589
<value>Package '{0}' is missing tool settings file DotnetToolSettings.xml.</value>
590590
</data>
591-
</root>
591+
<data name="ToolPackageConflictPackageId" xml:space="preserve">
592+
<value>Tool '{0}' is already installed.</value>
593+
</data>
594+
</root>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Transactions;
7+
using Microsoft.Extensions.EnvironmentAbstractions;
8+
9+
namespace Microsoft.DotNet.ShellShim
10+
{
11+
internal class CreateShimTransaction : IEnlistmentNotification
12+
{
13+
private readonly Action<List<FilePath>> _createShim;
14+
private readonly Action<List<FilePath>> _rollback;
15+
private List<FilePath> _locationOfShimDuringTransaction = new List<FilePath>();
16+
17+
public CreateShimTransaction(
18+
Action<List<FilePath>> createShim,
19+
Action<List<FilePath>> rollback)
20+
{
21+
_createShim = createShim ?? throw new ArgumentNullException(nameof(createShim));
22+
_rollback = rollback ?? throw new ArgumentNullException(nameof(rollback));
23+
}
24+
25+
public void CreateShim()
26+
{
27+
_createShim(_locationOfShimDuringTransaction);
28+
}
29+
30+
public void Commit(Enlistment enlistment)
31+
{
32+
enlistment.Done();
33+
}
34+
35+
public void InDoubt(Enlistment enlistment)
36+
{
37+
Rollback(enlistment);
38+
}
39+
40+
public void Prepare(PreparingEnlistment preparingEnlistment)
41+
{
42+
preparingEnlistment.Done();
43+
}
44+
45+
public void Rollback(Enlistment enlistment)
46+
{
47+
_rollback(_locationOfShimDuringTransaction);
48+
49+
enlistment.Done();
50+
}
51+
}
52+
}

src/dotnet/ShellShim/ShellShimMaker.cs

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
78
using System.Runtime.InteropServices;
89
using System.Text;
10+
using System.Transactions;
911
using System.Xml.Linq;
1012
using Microsoft.DotNet.Cli.Utils;
1113
using Microsoft.DotNet.Tools;
@@ -22,11 +24,38 @@ public class ShellShimMaker : IShellShimMaker
2224

2325
public ShellShimMaker(string pathToPlaceShim)
2426
{
25-
_pathToPlaceShim =
26-
pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
27+
_pathToPlaceShim = pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
2728
}
2829

2930
public void CreateShim(FilePath packageExecutable, string shellCommandName)
31+
{
32+
var createShimTransaction = new CreateShimTransaction(
33+
createShim: locationOfShimDuringTransaction =>
34+
{
35+
EnsureCommandNameUniqueness(shellCommandName);
36+
PlaceShim(packageExecutable, shellCommandName, locationOfShimDuringTransaction);
37+
},
38+
rollback: locationOfShimDuringTransaction =>
39+
{
40+
foreach (FilePath f in locationOfShimDuringTransaction)
41+
{
42+
if (File.Exists(f.Value))
43+
{
44+
File.Delete(f.Value);
45+
}
46+
}
47+
});
48+
49+
using (var transactionScope = new TransactionScope())
50+
{
51+
Transaction.Current.EnlistVolatile(createShimTransaction, EnlistmentOptions.None);
52+
createShimTransaction.CreateShim();
53+
54+
transactionScope.Complete();
55+
}
56+
}
57+
58+
private void PlaceShim(FilePath packageExecutable, string shellCommandName, List<FilePath> locationOfShimDuringTransaction)
3059
{
3160
FilePath shimPath = GetShimPath(shellCommandName);
3261

@@ -37,12 +66,20 @@ public void CreateShim(FilePath packageExecutable, string shellCommandName)
3766

3867
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
3968
{
40-
CreateConfigFile(shimPath.Value + ".config", entryPoint: packageExecutable, runner: "dotnet");
69+
FilePath windowsConfig = GetWindowsConfigPath(shellCommandName);
70+
CreateConfigFile(
71+
windowsConfig,
72+
entryPoint: packageExecutable,
73+
runner: "dotnet");
74+
75+
locationOfShimDuringTransaction.Add(windowsConfig);
76+
4177
using (var shim = File.Create(shimPath.Value))
4278
using (var exe = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherExeResourceName))
4379
{
4480
exe.CopyTo(shim);
4581
}
82+
locationOfShimDuringTransaction.Add(shimPath);
4683
}
4784
else
4885
{
@@ -51,37 +88,43 @@ public void CreateShim(FilePath packageExecutable, string shellCommandName)
5188
script.AppendLine($"dotnet {packageExecutable.ToQuotedString()} \"$@\"");
5289

5390
File.WriteAllText(shimPath.Value, script.ToString());
91+
locationOfShimDuringTransaction.Add(shimPath);
5492

5593
SetUserExecutionPermissionToShimFile(shimPath);
5694
}
5795
}
5896

5997
public void EnsureCommandNameUniqueness(string shellCommandName)
6098
{
61-
if (File.Exists(Path.Combine(_pathToPlaceShim, shellCommandName)))
99+
if (File.Exists(GetShimPath(shellCommandName).Value))
62100
{
63101
throw new GracefulException(
64102
string.Format(CommonLocalizableStrings.FailInstallToolSameName,
65103
shellCommandName));
66104
}
67105
}
68106

69-
internal void CreateConfigFile(string outputPath, FilePath entryPoint, string runner)
107+
internal void CreateConfigFile(FilePath outputPath, FilePath entryPoint, string runner)
70108
{
71109
XDocument config;
72-
using (var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherConfigResourceName))
110+
using(var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherConfigResourceName))
73111
{
74112
config = XDocument.Load(resource);
75113
}
76114

77115
var appSettings = config.Descendants("appSettings").First();
78116
appSettings.Add(new XElement("add", new XAttribute("key", "entryPoint"), new XAttribute("value", entryPoint.Value)));
79117
appSettings.Add(new XElement("add", new XAttribute("key", "runner"), new XAttribute("value", runner ?? string.Empty)));
80-
config.Save(outputPath);
118+
config.Save(outputPath.Value);
81119
}
82120

83121
public void Remove(string shellCommandName)
84122
{
123+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
124+
{
125+
File.Delete(GetWindowsConfigPath(shellCommandName).Value);
126+
}
127+
85128
File.Delete(GetShimPath(shellCommandName).Value);
86129
}
87130

@@ -96,6 +139,11 @@ private FilePath GetShimPath(string shellCommandName)
96139
return new FilePath(scriptPath);
97140
}
98141

142+
private FilePath GetWindowsConfigPath(string shellCommandName)
143+
{
144+
return new FilePath(GetShimPath(shellCommandName).Value + ".config");
145+
}
146+
99147
private static void SetUserExecutionPermissionToShimFile(FilePath scriptPath)
100148
{
101149
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Transactions;
7+
using Microsoft.Extensions.EnvironmentAbstractions;
8+
9+
namespace Microsoft.DotNet.ToolPackage
10+
{
11+
internal class ToolPackageObtainTransaction : IEnlistmentNotification
12+
{
13+
private readonly Func<List<DirectoryPath>, ToolConfigurationAndExecutablePath> _obtainAndReturnExecutablePath;
14+
private readonly Action<List<DirectoryPath>> _rollback;
15+
private List<DirectoryPath> _locationOfPackageDuringTransaction = new List<DirectoryPath>();
16+
17+
public ToolPackageObtainTransaction(
18+
Func<List<DirectoryPath>, ToolConfigurationAndExecutablePath> obtainAndReturnExecutablePath,
19+
Action<List<DirectoryPath>> rollback)
20+
{
21+
_obtainAndReturnExecutablePath = obtainAndReturnExecutablePath ?? throw new ArgumentNullException(nameof(obtainAndReturnExecutablePath));
22+
_rollback = rollback ?? throw new ArgumentNullException(nameof(rollback));
23+
}
24+
25+
public ToolConfigurationAndExecutablePath ObtainAndReturnExecutablePath()
26+
{
27+
return _obtainAndReturnExecutablePath(_locationOfPackageDuringTransaction);
28+
}
29+
30+
public void Commit(Enlistment enlistment)
31+
{
32+
enlistment.Done();
33+
}
34+
35+
public void InDoubt(Enlistment enlistment)
36+
{
37+
Rollback(enlistment);
38+
}
39+
40+
public void Prepare(PreparingEnlistment preparingEnlistment)
41+
{
42+
preparingEnlistment.Done();
43+
}
44+
45+
public void Rollback(Enlistment enlistment)
46+
{
47+
_rollback(_locationOfPackageDuringTransaction);
48+
49+
enlistment.Done();
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)