| | 1 | | using System.Management.Automation; |
| | 2 | | using System.Diagnostics; |
| | 3 | | using Kestrun.Hosting; |
| | 4 | | using Kestrun.Languages; |
| | 5 | | using Kestrun.Models; |
| | 6 | | using Kestrun.Scripting; |
| | 7 | | using Serilog; |
| | 8 | | using Serilog.Events; |
| | 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 | |
|
| | 23 | | /// <summary> |
| | 24 | | /// Processes an HTTP request using a PowerShell runspace from the pool. |
| | 25 | | /// </summary> |
| | 26 | | /// <param name="context">The HTTP context for the current request.</param> |
| | 27 | | public async Task InvokeAsync(HttpContext context) |
| | 28 | | { |
| | 29 | | // Concurrency diagnostics |
| 4 | 30 | | var start = DateTime.UtcNow; |
| 4 | 31 | | var threadId = Environment.CurrentManagedThreadId; |
| 4 | 32 | | var current = Interlocked.Increment(ref _inFlight); |
| 4 | 33 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 34 | | { |
| 4 | 35 | | Log.Debug("ENTER InvokeAsync path={Path} inFlight={InFlight} thread={Thread} time={Start}", |
| 4 | 36 | | context.Request.Path, current, threadId, start.ToString("O")); |
| | 37 | | } |
| | 38 | |
|
| | 39 | | try |
| | 40 | | { |
| 4 | 41 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 42 | | { |
| 4 | 43 | | Log.Debug("PowerShellRunspaceMiddleware started for {Path}", context.Request.Path); |
| | 44 | | } |
| | 45 | |
|
| | 46 | | // Acquire a runspace from the pool asynchronously (avoid blocking thread pool while waiting) |
| 4 | 47 | | var acquireStart = Stopwatch.GetTimestamp(); |
| 4 | 48 | | var runspace = await _pool.AcquireAsync(context.RequestAborted); |
| 3 | 49 | | var acquireMs = (Stopwatch.GetTimestamp() - acquireStart) * 1000.0 / Stopwatch.Frequency; |
| 3 | 50 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 51 | | { |
| 3 | 52 | | Log.Debug("Runspace acquired for {Path} in {AcquireMs} ms (inFlight={InFlight})", context.Request.Path, |
| | 53 | | } |
| | 54 | |
|
| 3 | 55 | | using var ps = PowerShell.Create(); |
| 3 | 56 | | ps.Runspace = runspace; |
| 3 | 57 | | var krRequest = await KestrunRequest.NewRequest(context); |
| 3 | 58 | | var krResponse = new KestrunResponse(krRequest); |
| | 59 | |
|
| | 60 | | // Store the PowerShell instance in the context for later use |
| 3 | 61 | | context.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps; |
| | 62 | |
|
| 3 | 63 | | KestrunContext kestrunContext = new(krRequest, krResponse, context); |
| | 64 | | // Set the KestrunContext in the HttpContext.Items for later use |
| 3 | 65 | | context.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kestrunContext; |
| | 66 | |
|
| 3 | 67 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 68 | | { |
| 3 | 69 | | Log.Debug("PowerShellRunspaceMiddleware - Setting KestrunContext in HttpContext.Items for {Path}", conte |
| | 70 | | } |
| | 71 | |
|
| 3 | 72 | | Log.Verbose("Setting PowerShell variables for Request and Response in the runspace."); |
| | 73 | | // Set the PowerShell variables for the request and response |
| 3 | 74 | | var ss = ps.Runspace.SessionStateProxy; |
| 3 | 75 | | ss.SetVariable("Context", kestrunContext); |
| | 76 | |
|
| | 77 | | try |
| | 78 | | { |
| 3 | 79 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 80 | | { |
| 3 | 81 | | Log.Debug("PowerShellRunspaceMiddleware - Continuing Pipeline for {Path}", context.Request.Path); |
| | 82 | | } |
| | 83 | |
|
| 3 | 84 | | await _next(context); // continue the pipeline |
| 3 | 85 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 86 | | { |
| 3 | 87 | | Log.Debug("PowerShellRunspaceMiddleware completed for {Path}", context.Request.Path); |
| | 88 | | } |
| 3 | 89 | | } |
| | 90 | | finally |
| | 91 | | { |
| 3 | 92 | | if (ps != null) |
| | 93 | | { |
| 3 | 94 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 95 | | { |
| 3 | 96 | | Log.Debug("Returning runspace to pool: {RunspaceId}", ps.Runspace.InstanceId); |
| | 97 | | } |
| | 98 | |
|
| 3 | 99 | | _pool.Release(ps.Runspace); // return the runspace to the pool |
| 3 | 100 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 101 | | { |
| 3 | 102 | | Log.Debug("Disposing PowerShell instance: {InstanceId}", ps.InstanceId); |
| | 103 | | } |
| | 104 | | // Dispose the PowerShell instance |
| 3 | 105 | | ps.Dispose(); |
| 3 | 106 | | _ = context.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY); // just in case someone re-uses |
| | 107 | | } |
| | 108 | | } |
| 3 | 109 | | } |
| 1 | 110 | | catch (Exception ex) |
| | 111 | | { |
| 1 | 112 | | Log.Error(ex, "Error occurred in PowerShellRunspaceMiddleware"); |
| 1 | 113 | | } |
| | 114 | | finally |
| | 115 | | { |
| 4 | 116 | | var remaining = Interlocked.Decrement(ref _inFlight); |
| 4 | 117 | | var durationMs = (DateTime.UtcNow - start).TotalMilliseconds; |
| 4 | 118 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 119 | | { |
| 4 | 120 | | Log.Debug("PowerShellRunspaceMiddleware ended for {Path} durationMs={Duration} inFlight={InFlight}", |
| 4 | 121 | | context.Request.Path, durationMs, remaining); |
| | 122 | | } |
| | 123 | | } |
| 4 | 124 | | } |
| | 125 | | } |