diff --git a/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Program.cs b/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Program.cs index 4814b6059..dc33823bf 100644 --- a/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Program.cs +++ b/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Program.cs @@ -2,7 +2,13 @@ var builder = DistributedApplication.CreateBuilder(args); -var storage = builder.AddAzureStorage("storage").RunAsEmulator(); +var storage = builder.AddAzureStorage("storage") + // see: https://github.com/dotnet/aspire/issues/13811 + .RunAsEmulator(azurite => + { + azurite.WithArgs("--disableProductStyleUrl"); + }); + var blob = storage.AddBlobs("myblob"); var ps = builder.AddPowerShell("ps") @@ -57,5 +63,6 @@ az storage blob upload --connection-string $myblob -c demo --file ./Scripts/scri .WithArgs(2, 3) .WaitForCompletion(script1); + builder.Build().Run(); diff --git a/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Properties/launchSettings.json b/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Properties/launchSettings.json index 80e6457ea..64093b07a 100644 --- a/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Properties/launchSettings.json +++ b/examples/powershell/CommunityToolkit.Aspire.PowerShell.AppHost/Properties/launchSettings.json @@ -7,6 +7,7 @@ "launchBrowser": true, "applicationUrl": "https://localhost:17118;http://localhost:15215", "environmentVariables": { + "DCP_DIAGNOSTICS_LOG_LEVEL": "debug", "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21165", diff --git a/src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs index baf5ce2b1..f2a3970a2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using System.Diagnostics; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -34,23 +35,34 @@ public static IResourceBuilder AddPowerShell( var pool = new PowerShellRunspacePoolResource(name, languageMode, minRunspaces, maxRunspaces); + var poolBuilder = builder.AddResource(pool) + .WithInitialState(new() + { + ResourceType = "PowerShellRunspacePool", + State = KnownResourceStates.NotStarted, + Properties = [ - builder.Eventing.Subscribe(pool, async (e, ct) => - { - var poolResource = e.Resource as PowerShellRunspacePoolResource; - - Debug.Assert(poolResource is not null); + new ("LanguageMode", pool.LanguageMode.ToString()), + new ("MinRunspaces", pool.MinRunspaces.ToString()), + new ("MaxRunspaces", pool.MaxRunspaces.ToString()) + ] + }) + .ExcludeFromManifest(); + poolBuilder.OnInitializeResource(async (res, e, ct) => + { var loggerService = e.Services.GetRequiredService(); var notificationService = e.Services.GetRequiredService(); + var hostLifetime = e.Services.GetRequiredService(); var sessionState = InitialSessionState.CreateDefault(); + sessionState.UseFullLanguageModeInDebugger = true; // This will block until explicit and implied WaitFor calls are completed await builder.Eventing.PublishAsync( - new BeforeResourceStartedEvent(poolResource, e.Services), ct); + new BeforeResourceStartedEvent(res, e.Services), ct); - foreach (var annotation in poolResource.Annotations.OfType>()) + foreach (var annotation in res.Annotations.OfType>()) { if (annotation is { } reference) { @@ -62,24 +74,12 @@ await builder.Eventing.PublishAsync( } } - var poolName = poolResource.Name; + var poolName = res.Name; var poolLogger = loggerService.GetLogger(poolName); - _ = poolResource.StartAsync(sessionState, notificationService, poolLogger, ct); + _ = res.StartAsync(sessionState, notificationService, poolLogger, hostLifetime, ct); }); - return builder.AddResource(pool) - .WithInitialState(new() - { - ResourceType = "PowerShellRunspacePool", - State = KnownResourceStates.NotStarted, - Properties = [ - - new ("LanguageMode", pool.LanguageMode.ToString()), - new ("MinRunspaces", pool.MinRunspaces.ToString()), - new ("MaxRunspaces", pool.MaxRunspaces.ToString()) - ] - }) - .ExcludeFromManifest(); + return poolBuilder; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResource.cs b/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResource.cs index ea8ed8b6f..96831fe61 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResource.cs @@ -1,4 +1,5 @@ using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Globalization; using System.Management.Automation; @@ -37,12 +38,7 @@ public class PowerShellRunspacePoolResource( /// public RunspacePool? Pool { get; private set; } - private Dictionary scriptResourceCompletion = []; - internal void AddScriptResource(PowerShellScriptResource scriptResource) => scriptResourceCompletion.Add(scriptResource.Name, false); - internal void ScriptResourceCompleted(PowerShellScriptResource scriptResource) => scriptResourceCompletion[scriptResource.Name] = true; - internal bool ScriptsCompleted => scriptResourceCompletion.Any(r => r.Value == false); - - internal Task StartAsync(InitialSessionState sessionState, ResourceNotificationService notificationService, ILogger logger, CancellationToken token = default) + internal Task StartAsync(InitialSessionState sessionState, ResourceNotificationService notificationService, ILogger logger, IHostApplicationLifetime hostLifetime, CancellationToken token = default) { logger.LogInformation( "Starting PowerShell runspace pool '{PoolName}' with {MinRunspaces} to {MaxRunspaces} runspaces", @@ -53,6 +49,13 @@ internal Task StartAsync(InitialSessionState sessionState, ResourceNotificationS Pool = RunspaceFactory.CreateRunspacePool(MinRunspaces, MaxRunspaces, sessionState, new AspirePSHost(logger)); ConfigureStateChangeNotifications(notificationService, logger); + + hostLifetime.ApplicationStopping.Register(() => + { + // if we don't do this, xunit 3 will hang on exit because of open runspaces (foreground threads) + logger.LogInformation("Closing PowerShell runspace pool '{PoolName}'", Name); + Pool?.Close(); + }); return Task.Factory.FromAsync(Pool.BeginOpen, Pool.EndOpen, null); } diff --git a/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResourceBuilderExtensions.cs index 2497dcc4d..695a8ce97 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunspacePoolResourceBuilderExtensions.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Diagnostics.CodeAnalysis; -using System.Drawing; using System.Management.Automation; namespace CommunityToolkit.Aspire.Hosting.PowerShell; @@ -33,53 +32,7 @@ public static IResourceBuilder AddScript( var scriptResource = new PowerShellScriptResource(name, scriptBlock, builder.Resource); - builder.ApplicationBuilder.Eventing.Subscribe(scriptResource, async (e, ct) => - { - var loggerService = e.Services.GetRequiredService(); - var notificationService = e.Services.GetRequiredService(); - - var scriptName = scriptResource.Name; - var scriptLogger = loggerService.GetLogger(scriptName); - - try - { - // this will block until the runspace pool is started, which is implied by the WaitFor call - await builder.ApplicationBuilder.Eventing.PublishAsync( - new BeforeResourceStartedEvent(scriptResource, e.Services), ct); - - scriptLogger.LogInformation("Starting script '{ScriptName}'", scriptName); - - _ = scriptResource.StartAsync(scriptLogger, notificationService, ct); - - await notificationService.WaitForResourceAsync(scriptResource.Name, KnownResourceStates.Finished, ct); - - ((IDisposable)scriptResource).Dispose(); - - builder.Resource.ScriptResourceCompleted(scriptResource); - - if (builder.Resource.ScriptsCompleted) - { - await notificationService.PublishUpdateAsync(builder.Resource, state => state with - { - State = KnownResourceStates.Finished, - Properties = [ - .. state.Properties, - ], - StopTimeStamp = DateTime.Now, - }); - - ((IDisposable)builder.Resource).Dispose(); - } - } - catch (Exception ex) - { - scriptLogger.LogError(ex, "Failed to start script '{ScriptName}'", scriptName); - } - }); - - builder.Resource.AddScriptResource(scriptResource); - - return builder.ApplicationBuilder + var scriptBuilder = builder.ApplicationBuilder .AddResource(scriptResource) .WithParentRelationship(builder.Resource) .WaitFor(builder) @@ -111,6 +64,32 @@ await notificationService.PublishUpdateAsync(builder.Resource, state => state wi ResourceCommandState.Disabled : ResourceCommandState.Enabled }); + + scriptBuilder.OnInitializeResource(async (res, e, ct) => + { + var loggerService = e.Services.GetRequiredService(); + var notificationService = e.Services.GetRequiredService(); + + var scriptName = res.Name; + var scriptLogger = loggerService.GetLogger(scriptName); + + try + { + // this will block until the runspace pool is started, which is implied by the WaitFor call + await builder.ApplicationBuilder.Eventing.PublishAsync( + new BeforeResourceStartedEvent(res, e.Services), ct); + + scriptLogger.LogInformation("Starting script '{ScriptName}'", scriptName); + + _ = res.StartAsync(scriptLogger, notificationService, ct); + } + catch (Exception ex) + { + scriptLogger.LogError(ex, "Failed to start script '{ScriptName}'", scriptName); + } + }); + + return scriptBuilder; } /// diff --git a/tests/CommunityToolkit.Aspire.Hosting.PowerShell.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.PowerShell.Tests/AppHostTests.cs index 8e52a7f26..6472b383b 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.PowerShell.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.PowerShell.Tests/AppHostTests.cs @@ -39,9 +39,6 @@ public async Task ScriptsExecuteSuccessfully() .WaitAsync(TimeSpan.FromSeconds(90)); await Task.WhenAll([ready1, ready2]); - - Assert.True(ready1.IsCompletedSuccessfully); - Assert.True(ready2.IsCompletedSuccessfully); } }