@@ -63,85 +63,89 @@ private async Task EnsureTemplatePackageInstalledWithCrossProcessLockAsync(strin
6363 var mutexName = CreateCrossProcessMutexName ( _templatePackageName ) ;
6464
6565 using var mutex = new Mutex ( false , mutexName , out var createdNew ) ;
66+ var mutexAcquired = false ;
67+
6668 try
6769 {
6870 // Wait for the mutex with a reasonable timeout to prevent hanging tests
69- var acquired = mutex . WaitOne ( TimeSpan . FromMinutes ( 5 ) ) ;
70- if ( ! acquired )
71+ mutexAcquired = mutex . WaitOne ( TimeSpan . FromMinutes ( 5 ) ) ;
72+ if ( ! mutexAcquired )
7173 {
7274 throw new TimeoutException ( $ "Timeout waiting for cross-process lock to install template package '{ _templatePackageName } '") ;
7375 }
7476
75- try
77+ // First check if the package is already installed globally (cross-process safety)
78+ var existingPackages = await _templatePackageManager . GetManagedTemplatePackagesAsync ( false , CancellationToken . None ) ;
79+ var existingPackage = existingPackages . FirstOrDefault ( p =>
80+ string . Equals ( p . Identifier , _templatePackageName , StringComparison . OrdinalIgnoreCase ) ) ;
81+
82+ if ( existingPackage != null )
7683 {
77- // First check if the package is already installed globally (cross-process safety)
78- var existingPackages = await _templatePackageManager . GetManagedTemplatePackagesAsync ( false , CancellationToken . None ) ;
79- var existingPackage = existingPackages . FirstOrDefault ( p =>
80- string . Equals ( p . Identifier , _templatePackageName , StringComparison . OrdinalIgnoreCase ) ) ;
81-
82- if ( existingPackage != null )
83- {
84- // Package is already installed globally, just store reference
85- _installedTemplatePackage = existingPackage ;
86- _isTemplateInstalled = true ;
87- return ;
88- }
84+ // Package is already installed globally, just store reference
85+ _installedTemplatePackage = existingPackage ;
86+ _isTemplateInstalled = true ;
87+ return ;
88+ }
8989
90- // Package not installed, proceed with installation
91- // Following the official dotnet CLI pattern: create install request with details
92- var installRequest = new InstallRequest ( _templatePackageName , version , details : new Dictionary < string , string > ( ) , force : false ) ;
93-
94- // Get the managed provider for global scope (matches official CLI approach)
95- var provider = _templatePackageManager . GetBuiltInManagedProvider ( InstallationScope . Global ) ;
96- var installResults = await provider . InstallAsync ( new [ ] { installRequest } , CancellationToken . None ) ;
97-
98- // Check if installation was successful
99- var installResult = installResults . FirstOrDefault ( ) ;
100- if ( installResult == null || ! installResult . Success )
101- {
102- var packageId = _templatePackageName ;
103- var detailedErrors = installResult ? . ErrorMessage ?? "Unknown installation error" ;
104-
105- var userErrorMessage = $ "Failed to install template package '{ packageId } '.\n " +
106- $ "Details:\n { detailedErrors } \n \n " +
107- $ "💡 Corrective actions:\n " +
108- $ " • Check your internet connection\n " +
109- $ " • Verify the package name and version are correct\n " +
110- $ " • Ensure you have sufficient permissions for global package installation\n " +
111- $ " • If using a private package source, ensure it's properly configured";
112-
113- throw new InvalidOperationException ( userErrorMessage ) ;
114- }
90+ // Package not installed, proceed with installation
91+ // Following the official dotnet CLI pattern: create install request with details
92+ var installRequest = new InstallRequest ( _templatePackageName , version , details : new Dictionary < string , string > ( ) , force : false ) ;
93+
94+ // Get the managed provider for global scope (matches official CLI approach)
95+ var provider = _templatePackageManager . GetBuiltInManagedProvider ( InstallationScope . Global ) ;
96+ var installResults = await provider . InstallAsync ( new [ ] { installRequest } , CancellationToken . None ) ;
97+
98+ // Check if installation was successful
99+ var installResult = installResults . FirstOrDefault ( ) ;
100+ if ( installResult == null || ! installResult . Success )
101+ {
102+ var packageId = _templatePackageName ;
103+ var detailedErrors = installResult ? . ErrorMessage ?? "Unknown installation error" ;
115104
116- // Following the official dotnet CLI pattern: store reference to the installed package
117- // This is crucial for later template discovery
118- _installedTemplatePackage = installResult . TemplatePackage as IManagedTemplatePackage ;
119- if ( _installedTemplatePackage == null )
120- {
121- throw new InvalidOperationException ( $ "Template package ' { _templatePackageName } ' was installed but could not be retrieved as a managed package" ) ;
122- }
105+ var userErrorMessage = $ "Failed to install template package ' { packageId } '. \n " +
106+ $ "Details: \n { detailedErrors } \n \n " +
107+ $ "💡 Corrective actions: \n " +
108+ $ " • Check your internet connection \n " +
109+ $ " • Verify the package name and version are correct \n " +
110+ $ " • Ensure you have sufficient permissions for global package installation \n " +
111+ $ " • If using a private package source, ensure it's properly configured" ;
123112
124- // Set flag last to ensure atomic operation visibility
125- _isTemplateInstalled = true ;
113+ throw new InvalidOperationException ( userErrorMessage ) ;
126114 }
127- finally
115+
116+ // Following the official dotnet CLI pattern: store reference to the installed package
117+ // This is crucial for later template discovery
118+ _installedTemplatePackage = installResult . TemplatePackage as IManagedTemplatePackage ;
119+ if ( _installedTemplatePackage == null )
128120 {
129- mutex . ReleaseMutex ( ) ;
121+ throw new InvalidOperationException ( $ "Template package ' { _templatePackageName } ' was installed but could not be retrieved as a managed package" ) ;
130122 }
123+
124+ // Set flag last to ensure atomic operation visibility
125+ _isTemplateInstalled = true ;
131126 }
132127 catch ( AbandonedMutexException )
133128 {
134129 // Previous process crashed while holding the mutex, but we can continue
135130 // The mutex is now owned by this thread
136- try
137- {
138- // Retry the installation operation
139- await EnsureTemplatePackageInstalledWithCrossProcessLockAsync ( version ) ;
140- }
141- finally
131+ mutexAcquired = true ; // Mark as acquired since we now own it
132+
133+ // Retry the installation operation (but avoid infinite recursion)
134+ // Just perform the installation logic directly here
135+ var existingPackages = await _templatePackageManager . GetManagedTemplatePackagesAsync ( false , CancellationToken . None ) ;
136+ var existingPackage = existingPackages . FirstOrDefault ( p =>
137+ string . Equals ( p . Identifier , _templatePackageName , StringComparison . OrdinalIgnoreCase ) ) ;
138+
139+ if ( existingPackage != null )
142140 {
143- mutex . ReleaseMutex ( ) ;
141+ _installedTemplatePackage = existingPackage ;
142+ _isTemplateInstalled = true ;
143+ return ;
144144 }
145+
146+ // If package still needs installation, let the exception propagate
147+ // to avoid complex retry logic
148+ throw new InvalidOperationException ( $ "Template package installation was interrupted by another process crash. Please retry the operation.") ;
145149 }
146150 catch ( Exception ex ) when ( ! ( ex is InvalidOperationException ) && ! ( ex is TimeoutException ) )
147151 {
@@ -155,6 +159,21 @@ private async Task EnsureTemplatePackageInstalledWithCrossProcessLockAsync(strin
155159
156160 throw new InvalidOperationException ( userErrorMessage , ex ) ;
157161 }
162+ finally
163+ {
164+ // Only release the mutex if we successfully acquired it
165+ if ( mutexAcquired )
166+ {
167+ try
168+ {
169+ mutex . ReleaseMutex ( ) ;
170+ }
171+ catch ( Exception )
172+ {
173+ // Ignore release errors - mutex will be released when the process exits
174+ }
175+ }
176+ }
158177 }
159178
160179 /// <summary>
0 commit comments