| | | 1 | | using System.Management.Automation; |
| | | 2 | | using System.Diagnostics; |
| | | 3 | | using Kestrun.Languages; |
| | | 4 | | using Kestrun.Models; |
| | | 5 | | using Kestrun.Scripting; |
| | | 6 | | using Serilog.Events; |
| | | 7 | | using Kestrun.Hosting; |
| | | 8 | | using Kestrun.Logging; |
| | | 9 | | |
| | | 10 | | namespace Kestrun.Middleware; |
| | | 11 | | |
| | | 12 | | /// <summary> |
| | | 13 | | /// Initializes a new instance of the <see cref="PowerShellRunspaceMiddleware"/> class. |
| | | 14 | | /// </summary> |
| | | 15 | | /// <param name="next">The next middleware in the pipeline.</param> |
| | | 16 | | /// <param name="pool">The runspace pool manager.</param> |
| | 4 | 17 | | public sealed class PowerShellRunspaceMiddleware(RequestDelegate next, KestrunRunspacePoolManager pool) |
| | | 18 | | { |
| | 4 | 19 | | private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next)); |
| | 4 | 20 | | private readonly KestrunRunspacePoolManager _pool = pool ?? throw new ArgumentNullException(nameof(pool)); |
| | | 21 | | private static int _inFlight; // diagnostic concurrency counter |
| | | 22 | | |
| | 36 | 23 | | private KestrunHost Host => _pool.Host; |
| | 33 | 24 | | private Serilog.ILogger Log => Host.Logger; |
| | | 25 | | /// <summary> |
| | | 26 | | /// Processes an HTTP request using a PowerShell runspace from the pool. |
| | | 27 | | /// </summary> |
| | | 28 | | /// <param name="context">The HTTP context for the current request.</param> |
| | | 29 | | public async Task InvokeAsync(HttpContext context) |
| | | 30 | | { |
| | | 31 | | // Concurrency diagnostics |
| | 4 | 32 | | var start = DateTime.UtcNow; |
| | 4 | 33 | | var threadId = Environment.CurrentManagedThreadId; |
| | 4 | 34 | | var current = Interlocked.Increment(ref _inFlight); |
| | 4 | 35 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 36 | | { |
| | 1 | 37 | | Log.DebugSanitized("ENTER InvokeAsync path={Path} inFlight={InFlight} thread={Thread} time={Start}", |
| | 1 | 38 | | context.Request.Path, current, threadId, start.ToString("O")); |
| | | 39 | | } |
| | | 40 | | |
| | | 41 | | try |
| | | 42 | | { |
| | 4 | 43 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 44 | | { |
| | 1 | 45 | | Log.DebugSanitized("PowerShellRunspaceMiddleware started for {Path}", context.Request.Path); |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | // Acquire a runspace from the pool asynchronously (avoid blocking thread pool while waiting) |
| | 4 | 49 | | var acquireStart = Stopwatch.GetTimestamp(); |
| | 4 | 50 | | var runspace = await _pool.AcquireAsync(context.RequestAborted); |
| | 3 | 51 | | var acquireMs = (Stopwatch.GetTimestamp() - acquireStart) * 1000.0 / Stopwatch.Frequency; |
| | 3 | 52 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 53 | | { |
| | 0 | 54 | | Log.DebugSanitized("Runspace acquired for {Path} in {AcquireMs} ms (inFlight={InFlight})", context.Reque |
| | | 55 | | } |
| | | 56 | | |
| | 3 | 57 | | var ps = PowerShell.Create(); |
| | 3 | 58 | | ps.Runspace = runspace; |
| | 3 | 59 | | var krRequest = await KestrunRequest.NewRequest(context); |
| | 3 | 60 | | var krResponse = new KestrunResponse(krRequest); |
| | | 61 | | |
| | | 62 | | // Store the PowerShell instance in the context for later use |
| | 3 | 63 | | context.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps; |
| | | 64 | | |
| | 3 | 65 | | KestrunContext kestrunContext = new(Host, krRequest, krResponse, context); |
| | | 66 | | // Set the KestrunContext in the HttpContext.Items for later use |
| | 3 | 67 | | context.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kestrunContext; |
| | | 68 | | |
| | 3 | 69 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 70 | | { |
| | 0 | 71 | | Log.DebugSanitized("PowerShellRunspaceMiddleware - Setting KestrunContext in HttpContext.Items for {Path |
| | | 72 | | } |
| | | 73 | | |
| | 3 | 74 | | Log.Verbose("Setting PowerShell variables for Request and Response in the runspace."); |
| | | 75 | | // Set the PowerShell variables for the request and response |
| | 3 | 76 | | var ss = ps.Runspace.SessionStateProxy; |
| | 3 | 77 | | ss.SetVariable("Context", kestrunContext); |
| | | 78 | | |
| | | 79 | | // Defer cleanup until the response is fully completed. This ensures |
| | | 80 | | // post-endpoint middleware (e.g., StatusCodePages) can still access the runspace. |
| | 3 | 81 | | context.Response.OnCompleted(() => |
| | 3 | 82 | | { |
| | 3 | 83 | | try |
| | 3 | 84 | | { |
| | 1 | 85 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 3 | 86 | | { |
| | 0 | 87 | | Log.Debug("OnCompleted: Returning runspace to pool: {RunspaceId}", ps.Runspace.InstanceId); |
| | 3 | 88 | | } |
| | 1 | 89 | | _pool.Release(ps.Runspace); |
| | 1 | 90 | | } |
| | 0 | 91 | | catch (Exception ex) |
| | 3 | 92 | | { |
| | 0 | 93 | | Log.Debug(ex, "OnCompleted: Error returning runspace to pool"); |
| | 0 | 94 | | } |
| | 3 | 95 | | finally |
| | 3 | 96 | | { |
| | 3 | 97 | | try |
| | 3 | 98 | | { |
| | 1 | 99 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 3 | 100 | | { |
| | 0 | 101 | | Log.Debug("OnCompleted: Disposing PowerShell instance: {InstanceId}", ps.InstanceId); |
| | 3 | 102 | | } |
| | 1 | 103 | | ps.Dispose(); |
| | 1 | 104 | | } |
| | 0 | 105 | | catch (Exception ex) |
| | 3 | 106 | | { |
| | 0 | 107 | | Log.Debug(ex, "OnCompleted: Error disposing PowerShell instance"); |
| | 0 | 108 | | } |
| | 1 | 109 | | _ = context.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY); |
| | 1 | 110 | | _ = context.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY); |
| | 1 | 111 | | } |
| | 1 | 112 | | return Task.CompletedTask; |
| | 3 | 113 | | }); |
| | | 114 | | |
| | 3 | 115 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 116 | | { |
| | 0 | 117 | | Log.DebugSanitized("PowerShellRunspaceMiddleware - Continuing Pipeline for {Path}", context.Request.Pat |
| | | 118 | | } |
| | | 119 | | |
| | 3 | 120 | | await _next(context); // continue the pipeline |
| | 3 | 121 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 122 | | { |
| | 0 | 123 | | Log.DebugSanitized("PowerShellRunspaceMiddleware completed for {Path}", context.Request.Path); |
| | | 124 | | } |
| | 3 | 125 | | } |
| | 1 | 126 | | catch (Exception ex) |
| | | 127 | | { |
| | 1 | 128 | | Log.Error(ex, "Error occurred in PowerShellRunspaceMiddleware"); |
| | 1 | 129 | | throw; // allow ExceptionHandler to catch and handle (re-exec or JSON) |
| | | 130 | | } |
| | | 131 | | finally |
| | | 132 | | { |
| | 4 | 133 | | var remaining = Interlocked.Decrement(ref _inFlight); |
| | 4 | 134 | | var durationMs = (DateTime.UtcNow - start).TotalMilliseconds; |
| | 4 | 135 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | | 136 | | { |
| | 1 | 137 | | Log.DebugSanitized("PowerShellRunspaceMiddleware ended for {Path} durationMs={durationMs} inFlight={rema |
| | 1 | 138 | | context.Request.Path, durationMs, remaining); |
| | | 139 | | } |
| | | 140 | | } |
| | 3 | 141 | | } |
| | | 142 | | } |