44using Microsoft . TemplateEngine . Edge . Settings ;
55using Microsoft . TemplateEngine . Edge ;
66using System . Security . Cryptography ;
7+ using System . Diagnostics ;
78
89namespace TALXIS . CLI . Workspace . TemplateEngine
910{
1011 /// <summary>
11- /// Service responsible for managing template packages ( installation, listing, etc.)
12+ /// Manages the TALXIS template package ensuring a single installation across processes.
1213 /// </summary>
1314 public class TemplatePackageService : IDisposable
1415 {
1516 private readonly TemplatePackageManager _templatePackageManager ;
1617 private readonly IEngineEnvironmentSettings _environmentSettings ;
1718 private readonly string _templatePackageName = "TALXIS.DevKit.Templates.Dataverse" ;
1819 private readonly SemaphoreSlim _installationSemaphore = new ( 1 , 1 ) ;
19- private volatile bool _isTemplateInstalled = false ;
20+ private volatile bool _isTemplateInstalled ;
2021 private IManagedTemplatePackage ? _installedTemplatePackage ;
2122
23+ // Tunables
24+ private const int MutexPollDelayMs = 300 ; // Small delay between attempts
25+ private static readonly TimeSpan MutexMaxWait = TimeSpan . FromSeconds ( 30 ) ; // Fail fast threshold
26+
2227 public string TemplatePackageName => _templatePackageName ;
2328
2429 public TemplatePackageService ( TemplatePackageManager templatePackageManager , IEngineEnvironmentSettings environmentSettings )
@@ -27,188 +32,138 @@ public TemplatePackageService(TemplatePackageManager templatePackageManager, IEn
2732 _environmentSettings = environmentSettings ?? throw new ArgumentNullException ( nameof ( environmentSettings ) ) ;
2833 }
2934
35+ /// <summary>
36+ /// Ensures the template package is installed (idempotent, thread + process safe).
37+ /// </summary>
3038 public async Task EnsureTemplatePackageInstalledAsync ( string ? version = null )
3139 {
32- // Double-checked locking pattern for thread safety within the same process
33- if ( _isTemplateInstalled && _installedTemplatePackage != null )
34- {
35- return ; // Already installed and we have a reference to it
36- }
40+ // Fast in-memory short‑circuit
41+ if ( _isTemplateInstalled && _installedTemplatePackage != null ) return ;
3742
3843 await _installationSemaphore . WaitAsync ( ) ;
3944 try
4045 {
41- // Check again inside the lock (double-checked locking)
42- if ( _isTemplateInstalled && _installedTemplatePackage != null )
43- {
44- return ; // Another thread completed the installation
45- }
46-
47- // Use cross-process synchronization to prevent race conditions between multiple CLI/MCP instances
48- await EnsureTemplatePackageInstalledWithCrossProcessLockAsync ( version ) ;
46+ if ( _isTemplateInstalled && _installedTemplatePackage != null ) return ;
47+ await EnsureInstalledCrossProcessAsync ( version ) ;
4948 }
5049 finally
5150 {
5251 _installationSemaphore . Release ( ) ;
5352 }
5453 }
5554
56- /// <summary>
57- /// Ensures template package installation with cross-process synchronization to prevent race conditions
58- /// during parallel test execution or multiple CLI instances.
59- /// </summary>
60- private async Task EnsureTemplatePackageInstalledWithCrossProcessLockAsync ( string ? version )
55+ // ---------------------------- Internal helpers ----------------------------
56+
57+ private async Task EnsureInstalledCrossProcessAsync ( string ? version )
6158 {
62- // Create a cross-process mutex name based on the package name
59+ // Pre-check without lock (cheap) – if another process already completed install.
60+ if ( await TryLoadExistingInstalledPackageAsync ( ) ) return ;
61+
6362 var mutexName = CreateCrossProcessMutexName ( _templatePackageName ) ;
64-
65- using var mutex = new Mutex ( false , mutexName , out var createdNew ) ;
66- var mutexAcquired = false ;
67-
63+ using var mutex = new Mutex ( false , mutexName , out _ ) ;
64+ var acquired = await AcquireMutexWithPollingAsync ( mutex , version ) ;
6865 try
6966 {
70- // Wait for the mutex with a reasonable timeout to prevent hanging tests
71- mutexAcquired = mutex . WaitOne ( TimeSpan . FromMinutes ( 5 ) ) ;
72- if ( ! mutexAcquired )
67+ if ( ! acquired )
7368 {
74- throw new TimeoutException ( $ "Timeout waiting for cross-process lock to install template package '{ _templatePackageName } '") ;
69+ throw new TimeoutException ( $ "Timeout ( { MutexMaxWait . TotalSeconds : F0 } s) waiting to install '{ _templatePackageName } '. Another process may be stalled. ") ;
7570 }
7671
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- }
72+ // Final check inside critical section (double-checked cross-process)
73+ if ( await TryLoadExistingInstalledPackageAsync ( ) ) return ;
8974
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- }
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 )
120- {
121- throw new InvalidOperationException ( $ "Template package '{ _templatePackageName } ' was installed but could not be retrieved as a managed package") ;
122- }
123-
124- // Set flag last to ensure atomic operation visibility
125- _isTemplateInstalled = true ;
75+ await InstallTemplatePackageAsync ( version ) ;
12676 }
127- catch ( AbandonedMutexException )
77+ finally
12878 {
129- // Previous process crashed while holding the mutex, but we can continue
130- // The mutex is now owned by this thread
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 )
79+ if ( acquired )
14080 {
141- _installedTemplatePackage = existingPackage ;
142- _isTemplateInstalled = true ;
143- return ;
81+ try { mutex . ReleaseMutex ( ) ; } catch { /* ignore */ }
14482 }
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.") ;
149- }
150- catch ( Exception ex ) when ( ! ( ex is InvalidOperationException ) && ! ( ex is TimeoutException ) )
151- {
152- // Wrap unexpected exceptions with user-friendly message
153- var userErrorMessage = $ "Unexpected error while installing template package '{ _templatePackageName } '{ ( version != null ? $ " version { version } " : "" ) } .\n " +
154- $ "Technical details: { ex . Message } \n \n " +
155- $ "💡 Corrective actions:\n " +
156- $ " • Check your internet connection\n " +
157- $ " • Ensure you have permission to install global packages\n " +
158- $ " • Check if the package source is accessible";
159-
160- throw new InvalidOperationException ( userErrorMessage , ex ) ;
16183 }
162- finally
84+ }
85+
86+ /// <summary>
87+ /// Polls for mutex ownership while periodically re-checking whether installation completed elsewhere.
88+ /// </summary>
89+ private async Task < bool > AcquireMutexWithPollingAsync ( Mutex mutex , string ? version )
90+ {
91+ var sw = Stopwatch . StartNew ( ) ;
92+ while ( sw . Elapsed < MutexMaxWait )
16393 {
164- // Only release the mutex if we successfully acquired it
165- if ( mutexAcquired )
94+ try
16695 {
167- try
168- {
169- mutex . ReleaseMutex ( ) ;
170- }
171- catch ( Exception )
172- {
173- // Ignore release errors - mutex will be released when the process exits
174- }
96+ if ( mutex . WaitOne ( TimeSpan . Zero ) ) return true ; // Acquired immediately
17597 }
98+ catch ( AbandonedMutexException )
99+ {
100+ return true ; // Treat abandoned as success (we now own it)
101+ }
102+
103+ // Re-check installation status – if installed we do not need the lock anymore.
104+ if ( await TryLoadExistingInstalledPackageAsync ( ) ) return false ; // False = we did not own the mutex but work is done
105+
106+ await Task . Delay ( MutexPollDelayMs ) ;
176107 }
108+ return false ; // Timed out
177109 }
178110
179111 /// <summary>
180- /// Creates a deterministic mutex name for cross-process synchronization based on the package name .
112+ /// Attempts to locate an already installed package; updates internal state if found .
181113 /// </summary>
114+ private async Task < bool > TryLoadExistingInstalledPackageAsync ( )
115+ {
116+ var existingPackages = await _templatePackageManager . GetManagedTemplatePackagesAsync ( false , CancellationToken . None ) ;
117+ var existing = existingPackages . FirstOrDefault ( p => string . Equals ( p . Identifier , _templatePackageName , StringComparison . OrdinalIgnoreCase ) ) ;
118+ if ( existing == null ) return false ;
119+ _installedTemplatePackage = existing ;
120+ _isTemplateInstalled = true ;
121+ return true ;
122+ }
123+
124+ private async Task InstallTemplatePackageAsync ( string ? version )
125+ {
126+ var request = new InstallRequest ( _templatePackageName , version , details : new Dictionary < string , string > ( ) , force : false ) ;
127+ var provider = _templatePackageManager . GetBuiltInManagedProvider ( InstallationScope . Global ) ;
128+ var results = await provider . InstallAsync ( new [ ] { request } , CancellationToken . None ) ;
129+ var result = results . FirstOrDefault ( ) ;
130+
131+ if ( result == null || ! result . Success )
132+ {
133+ var details = result ? . ErrorMessage ?? "Unknown installation error" ;
134+ throw new InvalidOperationException ( $ "Failed to install template package '{ _templatePackageName } '.\n Details:\n { details } \n " +
135+ "💡 Corrective actions:\n " +
136+ " • Check internet connectivity\n " +
137+ " • Verify package name/version\n " +
138+ " • Ensure global install permissions\n " +
139+ " • Validate private feeds (if used)" ) ;
140+ }
141+
142+ _installedTemplatePackage = result . TemplatePackage as IManagedTemplatePackage
143+ ?? throw new InvalidOperationException ( $ "Template package '{ _templatePackageName } ' installed but not retrievable as managed package") ;
144+ _isTemplateInstalled = true ; // Publish state last
145+ }
146+
182147 private static string CreateCrossProcessMutexName ( string packageName )
183148 {
184- // Create a hash of the package name to ensure the mutex name is valid and deterministic
185149 using var sha256 = SHA256 . Create ( ) ;
186- var hashBytes = sha256 . ComputeHash ( System . Text . Encoding . UTF8 . GetBytes ( packageName ) ) ;
187- var hashString = Convert . ToBase64String ( hashBytes ) . Replace ( '+' , '-' ) . Replace ( '/' , '_' ) . TrimEnd ( '=' ) ;
188-
189- // Prefix with a namespace to avoid conflicts with other applications
190- return $ "Global\\ TALXIS_CLI_TemplatePackage_{ hashString } ";
150+ var hash = sha256 . ComputeHash ( System . Text . Encoding . UTF8 . GetBytes ( packageName ) ) ;
151+ var token = Convert . ToBase64String ( hash ) . Replace ( '+' , '-' ) . Replace ( '/' , '_' ) . TrimEnd ( '=' ) ;
152+ // Omit Windows-specific Global\ prefix for cross-platform consistency.
153+ return $ "TALXIS_CLI_TemplatePackage_{ token } ";
191154 }
192155
193156 public async Task < List < ITemplateInfo > > ListTemplatesAsync ( string ? version = null )
194157 {
195158 await EnsureTemplatePackageInstalledAsync ( version ) ;
196-
197- // Read the installed package reference with memory barrier for thread safety
198- var installedPackage = _installedTemplatePackage ;
199- if ( installedPackage == null )
200- {
201- throw new InvalidOperationException ( "Template package was installed but reference is not available" ) ;
202- }
203-
204- // Use the official dotnet CLI pattern - get templates from the specific installed package
205- var templates = await _templatePackageManager . GetTemplatesAsync ( installedPackage , CancellationToken . None ) ;
159+ var pkg = _installedTemplatePackage ?? throw new InvalidOperationException ( "Template package reference missing after install." ) ;
160+ var templates = await _templatePackageManager . GetTemplatesAsync ( pkg , CancellationToken . None ) ;
206161 return templates . ToList ( ) ;
207162 }
208163
209164 public void Dispose ( )
210165 {
211- _installationSemaphore ? . Dispose ( ) ;
166+ _installationSemaphore . Dispose ( ) ;
212167 }
213168 }
214169}
0 commit comments