Skip to content

Commit

Permalink
Follow-up improvements to Aspire 9 debugging and configuration (#628)
Browse files Browse the repository at this point in the history
### Summary & Motivation

Aspire is now configured to keep SQL Server alive between debug sessions
using the new Aspire 9 `.WithLifetime(ContainerLifetime.Persistent)`,
significantly improving secondary startup.

Unfortunately, Aspire does not seem to release ports immediately upon
shutdown, requiring 10 seconds to a minute before the AppHost can
restart. To mitigate this issue temporarily, logic has been added to
kill any Developer Control Pane processes (`dcpctrl`) from previous
sessions when starting the AppHost, preventing conflicts and strange
errors. For more details, see this issue:
dotnet/aspire#6704.

The SPA generation and `index.html` caching strategy have been updated
to align with the new Aspire startup behavior during debugging.

The AppGateway no longer launches automatically when starting Aspire
AppHost, as it needs a moment to be ready.

Additionally, a bug has been resolved where the frontend incorrectly
used the old `platformplatform.pfx` certificate instead of
`localhost.pfx`.

Lastly, a fix for a duplicated `dotnet dotnet` issue in the README has
been applied.

### Checklist

- [x] I have added a Label to the pull-request
- [x] I have added tests, and done manual regression tests
- [x] I have updated the documentation, if necessary
  • Loading branch information
tjementum authored Nov 19, 2024
2 parents 76b98f5 + 6500a8b commit b74c354
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 34 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Using .NET Aspire, docker images with SQL Server, Blob Storage, and mail server

```bash
cd application/AppHost
dotnet dotnet run # First run will be slow as Docker images are downloaded
dotnet run # First run will be slow as Docker images are downloaded
```

Alternatively, open the [PlatformPlatform](/application/PlatformPlatform.sln) solution in Rider or Visual Studio and run the [Aspire AppHost](/application/AppHost/AppHost.csproj) project.
Expand Down
1 change: 0 additions & 1 deletion application/AppGateway/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"profiles": {
"Api": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:9000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "development"
Expand Down
34 changes: 32 additions & 2 deletions application/AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System.Diagnostics;
using AppHost;
using Azure.Storage.Blobs;
using Microsoft.Extensions.Configuration;
using Projects;

// Detect if Aspire ports from the previous run are released. See https://github.com/dotnet/aspire/issues/6704
EnsureDeveloperControlPaneIsNotRunning();

var builder = DistributedApplication.CreateBuilder(args);

var certificatePassword = builder.CreateSslCertificateIfNotExists();
Expand All @@ -11,7 +15,8 @@

var sqlPassword = builder.CreateStablePassword("sql-server-password");
var sqlServer = builder.AddSqlServer("sql-server", sqlPassword, 9002)
.WithDataVolume("platform-platform-sql-server-data");
.WithDataVolume("platform-platform-sql-server-data")
.WithLifetime(ContainerLifetime.Persistent);

var azureStorage = builder
.AddAzureStorage("azure-storage")
Expand Down Expand Up @@ -76,12 +81,37 @@
.WithReference(frontendBuild)
.WithReference(accountManagementApi)
.WithReference(backOfficeApi)
.WaitFor(accountManagementApi);
.WaitFor(accountManagementApi)
.WaitFor(frontendBuild);

await builder.Build().RunAsync();

return;

void EnsureDeveloperControlPaneIsNotRunning()
{
const string processName = "dcpctrl"; // The Aspire Developer Control Pane process name

var process = Process.GetProcesses()
.SingleOrDefault(p => p.ProcessName.Contains(processName, StringComparison.OrdinalIgnoreCase));

if (process == null) return;

Console.WriteLine($"Shutting down developer control pane from previous run. Process: {process.ProcessName} (ID: {process.Id})");

Thread.Sleep(TimeSpan.FromSeconds(5)); // Allow Docker containers to shut down to avoid orphaned containers

try
{
process.Kill();
Console.WriteLine($"Process {process.Id} killed successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to kill process {process.Id}: {ex.Message}");
}
}

void CreateBlobContainer(string containerName)
{
var connectionString = builder.Configuration.GetConnectionString("blob-storage");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public class SinglePageAppConfiguration
public static readonly string[] SupportedLocalizations = ["en-US", "da-DK", "nl-NL"];

public static readonly string BuildRootPath = GetWebAppDistRoot("WebApp", "dist");
private static readonly DateTime StartupTime = DateTime.UtcNow;

public static readonly JsonSerializerOptions JsonHtmlEncodingOptions =
new(SharedDependencyConfiguration.DefaultJsonSerializerOptions)
Expand Down Expand Up @@ -78,45 +77,34 @@ public SinglePageAppConfiguration(bool isDevelopment, params (string Key, string

public string GetHtmlTemplate()
{
if (_htmlTemplate is not null && !_isDevelopment)
{
return _htmlTemplate;
}

AwaitSinglePageAppGeneration();

if (!File.Exists(_htmlTemplatePath))
{
throw new FileNotFoundException("index.html does not exist.", _htmlTemplatePath);
}

return _htmlTemplate ??= File.ReadAllText(_htmlTemplatePath, new UTF8Encoding());
}

public string GetRemoteEntryJs()
{
return _remoteEntryJsContent ??= File.ReadAllText(_remoteEntryJsPath, new UTF8Encoding());
}

/// <summary>
/// This only runs locally, where the frontend is generating the index.html at startup, and it might not exist.
/// This method is called every time, so any changes to the index.html will be updated while debugging.
/// In rare cases, the index.html contains RsBuild info when generating it. A simple reload will fix this.
/// </summary>
[Conditional("DEBUG")]
private void AwaitSinglePageAppGeneration()
{
if (_htmlTemplate is not null) return;

var stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < TimeSpan.FromSeconds(25))
var tryUntil = DateTime.Now.AddSeconds(30);
while (DateTime.Now < tryUntil)
{
// A new index.html is created when starting, so we ensure the index.html is not from an old build
if (new FileInfo(_htmlTemplatePath).LastWriteTimeUtc > StartupTime.AddSeconds(-15)) break;
if (File.Exists(_htmlTemplatePath))
{
_htmlTemplate = File.ReadAllText(_htmlTemplatePath, new UTF8Encoding());
return;
}

Thread.Sleep(TimeSpan.FromMilliseconds(100));
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}

// If the index.html was just created, the Web App Dev server needs a few moments to warm up
if (new FileInfo(_htmlTemplatePath).LastWriteTimeUtc > DateTime.UtcNow.AddSeconds(-1))
{
Thread.Sleep(TimeSpan.FromMilliseconds(500));
}
public string GetRemoteEntryJs()
{
return _remoteEntryJsContent ??= File.ReadAllText(_remoteEntryJsPath, new UTF8Encoding());
}

private static string GetWebAppDistRoot(string webAppProjectName, string webAppDistRootName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function DevelopmentServerPlugin(options: DevelopmentServerPluginOptions)
}

// Path to the platformplatform.pfx certificate generated as part of the Aspire setup
const pfxPath = path.join(os.homedir(), ".aspnet", "dev-certs", "https", "platformplatform.pfx");
const pfxPath = path.join(os.homedir(), ".aspnet", "dev-certs", "https", "localhost.pfx");
const passphrase = process.env.CERTIFICATE_PASSWORD ?? "";

if (!fs.existsSync(pfxPath)) {
Expand Down

0 comments on commit b74c354

Please sign in to comment.