Skip to content

Commit 1d4ee78

Browse files
committed
Fix race condition
1 parent f842a20 commit 1d4ee78

File tree

7 files changed

+54
-9
lines changed

7 files changed

+54
-9
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ dotnet tool install --global TALXIS.CLI
4040
**Update to the latest version:**
4141
```sh
4242
dotnet tool update --global TALXIS.CLI
43+
44+
dotnet new update
4345
```
46+
> [!TIP]
47+
> Running `dotnet new update` ensures that template packages used by the CLI are updated to their latest versions.
4448
4549
After installation, use the CLI via the `txc` command in any terminal.
4650

src/TALXIS.CLI.Workspace/ComponentCreateCliCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.CommandLine.Completions;
22
using DotMake.CommandLine;
3+
using TALXIS.CLI.Workspace.TemplateEngine;
34
namespace TALXIS.CLI.Workspace;
45

56
[CliCommand(

src/TALXIS.CLI.Workspace/ComponentExplainCliCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using DotMake.CommandLine;
2+
using TALXIS.CLI.Workspace.TemplateEngine;
23

34
namespace TALXIS.CLI.Workspace;
45

src/TALXIS.CLI.Workspace/ComponentListCliCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using DotMake.CommandLine;
2+
using TALXIS.CLI.Workspace.TemplateEngine;
23

34
namespace TALXIS.CLI.Workspace;
45

src/TALXIS.CLI.Workspace/ComponentParameterListCliCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using DotMake.CommandLine;
2+
using TALXIS.CLI.Workspace.TemplateEngine;
23

34
namespace TALXIS.CLI.Workspace;
45

src/TALXIS.CLI.Workspace/TemplateEngine/TemplateInvoker.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Microsoft.Extensions.Logging;
22
using Microsoft.TemplateEngine.Abstractions;
3-
namespace TALXIS.CLI.Workspace
3+
namespace TALXIS.CLI.Workspace.TemplateEngine
44
{
55
/// <summary>
66
/// Template invoker that uses the new service-based architecture for template engine operations.
@@ -15,9 +15,9 @@ public class TemplateInvoker : IDisposable
1515
public TemplateInvoker(string? outputPath = null, LogLevel logLevel = LogLevel.Error)
1616
{
1717
// Use the factory to create the service-based architecture
18-
_templateCreationService = TemplateEngine.TemplateEngineFactory.CreateTemplateCreationService(outputPath, logLevel);
19-
_templateDiscoveryService = TemplateEngine.TemplateEngineFactory.CreateTemplateDiscoveryService(outputPath, logLevel);
20-
_templatePackageService = TemplateEngine.TemplateEngineFactory.CreateTemplatePackageService(outputPath, logLevel);
18+
_templateCreationService = TemplateEngineFactory.CreateTemplateCreationService(outputPath, logLevel);
19+
_templateDiscoveryService = TemplateEngineFactory.CreateTemplateDiscoveryService(outputPath, logLevel);
20+
_templatePackageService = TemplateEngineFactory.CreateTemplatePackageService(outputPath, logLevel);
2121
}
2222

2323
/// <summary>
@@ -68,7 +68,9 @@ public async Task<IReadOnlyList<ITemplateParameter>> ListParametersForTemplateAs
6868

6969
public void Dispose()
7070
{
71-
// Services are created via factory and don't need explicit disposal
71+
// Dispose of services that implement IDisposable
72+
_templatePackageService?.Dispose();
73+
// Note: _templateCreationService and _templateDiscoveryService don't implement IDisposable
7274
// The underlying template engine components handle their own cleanup
7375
}
7476
}

src/TALXIS.CLI.Workspace/TemplateEngine/TemplatePackageService.cs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ namespace TALXIS.CLI.Workspace.TemplateEngine
99
/// <summary>
1010
/// Service responsible for managing template packages (installation, listing, etc.)
1111
/// </summary>
12-
public class TemplatePackageService
12+
public class TemplatePackageService : IDisposable
1313
{
1414
private readonly TemplatePackageManager _templatePackageManager;
1515
private readonly IEngineEnvironmentSettings _environmentSettings;
1616
private readonly string _templatePackageName = "TALXIS.DevKit.Templates.Dataverse";
17-
private bool _isTemplateInstalled = false;
17+
private readonly SemaphoreSlim _installationSemaphore = new(1, 1);
18+
private volatile bool _isTemplateInstalled = false;
1819
private IManagedTemplatePackage? _installedTemplatePackage;
1920

2021
public string TemplatePackageName => _templatePackageName;
@@ -27,13 +28,35 @@ public TemplatePackageService(TemplatePackageManager templatePackageManager, IEn
2728

2829
public async Task EnsureTemplatePackageInstalledAsync(string? version = null)
2930
{
31+
// Double-checked locking pattern for thread safety within the same process
3032
if (_isTemplateInstalled && _installedTemplatePackage != null)
3133
{
3234
return; // Already installed and we have a reference to it
3335
}
3436

37+
await _installationSemaphore.WaitAsync();
3538
try
3639
{
40+
// Check again inside the lock (double-checked locking)
41+
if (_isTemplateInstalled && _installedTemplatePackage != null)
42+
{
43+
return; // Another thread completed the installation
44+
}
45+
46+
// First check if the package is already installed globally (cross-process safety)
47+
var existingPackages = await _templatePackageManager.GetManagedTemplatePackagesAsync(false, CancellationToken.None);
48+
var existingPackage = existingPackages.FirstOrDefault(p =>
49+
string.Equals(p.Identifier, _templatePackageName, StringComparison.OrdinalIgnoreCase));
50+
51+
if (existingPackage != null)
52+
{
53+
// Package is already installed globally, just store reference
54+
_installedTemplatePackage = existingPackage;
55+
_isTemplateInstalled = true;
56+
return;
57+
}
58+
59+
// Package not installed, proceed with installation
3760
// Following the official dotnet CLI pattern: create install request with details
3861
var installRequest = new InstallRequest(_templatePackageName, version, details: new Dictionary<string, string>(), force: false);
3962

@@ -67,6 +90,7 @@ public async Task EnsureTemplatePackageInstalledAsync(string? version = null)
6790
throw new InvalidOperationException($"Template package '{_templatePackageName}' was installed but could not be retrieved as a managed package");
6891
}
6992

93+
// Set flag last to ensure atomic operation visibility
7094
_isTemplateInstalled = true;
7195
}
7296
catch (Exception ex) when (!(ex is InvalidOperationException))
@@ -81,20 +105,31 @@ public async Task EnsureTemplatePackageInstalledAsync(string? version = null)
81105

82106
throw new InvalidOperationException(userErrorMessage, ex);
83107
}
108+
finally
109+
{
110+
_installationSemaphore.Release();
111+
}
84112
}
85113

86114
public async Task<List<ITemplateInfo>> ListTemplatesAsync(string? version = null)
87115
{
88116
await EnsureTemplatePackageInstalledAsync(version);
89117

90-
if (_installedTemplatePackage == null)
118+
// Read the installed package reference with memory barrier for thread safety
119+
var installedPackage = _installedTemplatePackage;
120+
if (installedPackage == null)
91121
{
92122
throw new InvalidOperationException("Template package was installed but reference is not available");
93123
}
94124

95125
// Use the official dotnet CLI pattern - get templates from the specific installed package
96-
var templates = await _templatePackageManager.GetTemplatesAsync(_installedTemplatePackage, CancellationToken.None);
126+
var templates = await _templatePackageManager.GetTemplatesAsync(installedPackage, CancellationToken.None);
97127
return templates.ToList();
98128
}
129+
130+
public void Dispose()
131+
{
132+
_installationSemaphore?.Dispose();
133+
}
99134
}
100135
}

0 commit comments

Comments
 (0)