|
7 | 7 | using System.Collections.Immutable; |
8 | 8 | using System.IO; |
9 | 9 | using System.Text.Json; |
| 10 | +using System.Threading; |
10 | 11 |
|
11 | 12 | namespace PostSharp.Engineering.BuildTools.Utilities |
12 | 13 | { |
@@ -42,89 +43,127 @@ public bool Install( BuildContext context ) |
42 | 43 | var configFilePath = Path.Combine( baseDirectory, ".config", "dotnet-tools.json" ); |
43 | 44 | var resourceDirectory = Path.Combine( baseDirectory, ".tools" ); |
44 | 45 |
|
45 | | - // 1. Create the dotnet tool manifest. |
46 | | - if ( !File.Exists( configFilePath ) ) |
| 46 | + // Use a named mutex to prevent race conditions when multiple parallel builds |
| 47 | + // try to install the dotnet tool at the same time. |
| 48 | + var mutexName = "Global\\DotNetToolInstall_" + baseDirectory.Replace( '\\', '_' ).Replace( '/', '_' ).Replace( ':', '_' ); |
| 49 | + |
| 50 | + using var mutex = new Mutex( false, mutexName ); |
| 51 | + |
| 52 | + try |
47 | 53 | { |
48 | | - if ( !ToolInvocationHelper.InvokeTool( |
49 | | - context.Console, |
50 | | - "dotnet", |
51 | | - $"new tool-manifest", |
52 | | - baseDirectory ) ) |
| 54 | + // Wait up to 5 minutes for the mutex. |
| 55 | + if ( !mutex.WaitOne( TimeSpan.FromMinutes( 5 ) ) ) |
53 | 56 | { |
| 57 | + context.Console.WriteError( "Timeout waiting for dotnet tool installation lock." ); |
| 58 | + |
54 | 59 | return false; |
55 | 60 | } |
56 | 61 | } |
57 | | - |
58 | | - // Open the config file and see if we have to install or update. |
59 | | - string? installVerb = null; |
60 | | - var configDocument = JsonDocument.Parse( File.ReadAllText( configFilePath ) ); |
61 | | - |
62 | | - var installedVersionString = configDocument.RootElement.GetPropertyOrNull( "tools" ) |
63 | | - .GetPropertyOrNull( this.PackageId.ToLowerInvariant() ) |
64 | | - .GetPropertyOrNull( "version" ) |
65 | | - ?.GetString(); |
66 | | - |
67 | | - if ( installedVersionString == null ) |
| 62 | + catch ( AbandonedMutexException ) |
68 | 63 | { |
69 | | - installVerb = "install"; |
| 64 | + // Another process crashed while holding the mutex. We now own it. |
70 | 65 | } |
71 | | - else |
| 66 | + |
| 67 | + try |
72 | 68 | { |
73 | | - var installedVersion = NuGetVersion.Parse( installedVersionString ); |
| 69 | + // 1. Create the dotnet tool manifest. |
| 70 | + if ( !File.Exists( configFilePath ) ) |
| 71 | + { |
| 72 | + if ( !ToolInvocationHelper.InvokeTool( |
| 73 | + context.Console, |
| 74 | + "dotnet", |
| 75 | + $"new tool-manifest", |
| 76 | + baseDirectory ) ) |
| 77 | + { |
| 78 | + return false; |
| 79 | + } |
| 80 | + |
| 81 | + // Verify the manifest was created where expected. |
| 82 | + if ( !File.Exists( configFilePath ) ) |
| 83 | + { |
| 84 | + context.Console.WriteError( |
| 85 | + $"The 'dotnet new tool-manifest' command succeeded but the manifest was not created at the expected location: '{configFilePath}'. " + |
| 86 | + $"Working directory was: '{baseDirectory}'." ); |
| 87 | + |
| 88 | + return false; |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + // Open the config file and see if we have to install or update. |
| 93 | + string? installVerb = null; |
| 94 | + var configDocument = JsonDocument.Parse( File.ReadAllText( configFilePath ) ); |
| 95 | + |
| 96 | + var installedVersionString = configDocument.RootElement.GetPropertyOrNull( "tools" ) |
| 97 | + .GetPropertyOrNull( this.PackageId.ToLowerInvariant() ) |
| 98 | + .GetPropertyOrNull( "version" ) |
| 99 | + ?.GetString(); |
74 | 100 |
|
75 | | - if ( installedVersion < NuGetVersion.Parse( this.Version ) ) |
| 101 | + if ( installedVersionString == null ) |
76 | 102 | { |
77 | | - installVerb = "update"; |
| 103 | + installVerb = "install"; |
78 | 104 | } |
79 | | - } |
| 105 | + else |
| 106 | + { |
| 107 | + var installedVersion = NuGetVersion.Parse( installedVersionString ); |
80 | 108 |
|
81 | | - // 2. Restore the tool. |
82 | | - if ( installVerb != null ) |
83 | | - { |
| 109 | + if ( installedVersion < NuGetVersion.Parse( this.Version ) ) |
| 110 | + { |
| 111 | + installVerb = "update"; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + // 2. Restore the tool. |
| 116 | + if ( installVerb != null ) |
| 117 | + { |
| 118 | + if ( !ToolInvocationHelper.InvokeTool( |
| 119 | + context.Console, |
| 120 | + "dotnet", |
| 121 | + $"tool {installVerb} {this.PackageId} --version {this.Version} --local --add-source \"https://api.nuget.org/v3/index.json\"", |
| 122 | + baseDirectory ) ) |
| 123 | + { |
| 124 | + return false; |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + // 3. Restore the tools from the manifest |
| 129 | + // The manifest might contain tools, that have been removed from the machine, or not yet installed. |
| 130 | + // The tools are stored in NuGet package cache, that can be cleaned. |
84 | 131 | if ( !ToolInvocationHelper.InvokeTool( |
85 | 132 | context.Console, |
86 | 133 | "dotnet", |
87 | | - $"tool {installVerb} {this.PackageId} --version {this.Version} --local --add-source \"https://api.nuget.org/v3/index.json\"", |
| 134 | + $"tool restore --add-source \"https://api.nuget.org/v3/index.json\"", |
88 | 135 | baseDirectory ) ) |
89 | 136 | { |
90 | 137 | return false; |
91 | 138 | } |
92 | | - } |
93 | 139 |
|
94 | | - // 3. Restore the tools from the manifest |
95 | | - // The manifest might contain tools, that have been removed from the machine, or not yet installed. |
96 | | - // The tools are stored in NuGet package cache, that can be cleaned. |
97 | | - if ( !ToolInvocationHelper.InvokeTool( |
98 | | - context.Console, |
99 | | - "dotnet", |
100 | | - $"tool restore --add-source \"https://api.nuget.org/v3/index.json\"", |
101 | | - baseDirectory ) ) |
102 | | - { |
103 | | - return false; |
104 | | - } |
105 | | - |
106 | | - // 4. Restore resource tools. |
107 | | - Directory.CreateDirectory( resourceDirectory ); |
108 | | - var assembly = this.GetType().Assembly; |
109 | | - |
110 | | - foreach ( var resourceName in assembly.GetManifestResourceNames() ) |
111 | | - { |
112 | | - const string prefix = "PostSharp.Engineering.BuildTools.Resources.Tools."; |
| 140 | + // 4. Restore resource tools. |
| 141 | + Directory.CreateDirectory( resourceDirectory ); |
| 142 | + var assembly = this.GetType().Assembly; |
113 | 143 |
|
114 | | - if ( resourceName.StartsWith( prefix, StringComparison.Ordinal ) ) |
| 144 | + foreach ( var resourceName in assembly.GetManifestResourceNames() ) |
115 | 145 | { |
116 | | - using var resource = assembly.GetManifestResourceStream( resourceName ); |
117 | | - |
118 | | - var file = Path.Combine( resourceDirectory, resourceName.Substring( prefix.Length ) ); |
| 146 | + const string prefix = "PostSharp.Engineering.BuildTools.Resources.Tools."; |
119 | 147 |
|
120 | | - using ( var outputStream = File.Create( file ) ) |
| 148 | + if ( resourceName.StartsWith( prefix, StringComparison.Ordinal ) ) |
121 | 149 | { |
122 | | - resource!.CopyTo( outputStream ); |
| 150 | + using var resource = assembly.GetManifestResourceStream( resourceName ); |
| 151 | + |
| 152 | + var file = Path.Combine( resourceDirectory, resourceName.Substring( prefix.Length ) ); |
| 153 | + |
| 154 | + using ( var outputStream = File.Create( file ) ) |
| 155 | + { |
| 156 | + resource!.CopyTo( outputStream ); |
| 157 | + } |
123 | 158 | } |
124 | 159 | } |
125 | | - } |
126 | 160 |
|
127 | | - return true; |
| 161 | + return true; |
| 162 | + } |
| 163 | + finally |
| 164 | + { |
| 165 | + mutex.ReleaseMutex(); |
| 166 | + } |
128 | 167 | } |
129 | 168 |
|
130 | 169 | public virtual bool Invoke( BuildContext context, string command, ToolInvocationOptions? options = null ) |
|
0 commit comments