< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Middleware.PowerShellRunspaceMiddleware
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Middleware/PowerShellRunspaceMiddleware.cs
Tag: Kestrun/Kestrun@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
93%
Covered lines: 96
Uncovered lines: 7
Coverable lines: 103
Total lines: 291
Line coverage: 93.2%
Branch coverage
91%
Covered branches: 31
Total branches: 34
Branch coverage: 91.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/19/2025 - 17:40:50 Line coverage: 84.6% (66/78) Branch coverage: 63.6% (14/22) Total lines: 142 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02601/02/2026 - 00:16:25 Line coverage: 84.2% (64/76) Branch coverage: 63.6% (14/22) Total lines: 140 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/24/2026 - 19:35:59 Line coverage: 83.3% (65/78) Branch coverage: 62.5% (15/24) Total lines: 145 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5103/26/2026 - 03:54:59 Line coverage: 91% (71/78) Branch coverage: 87.5% (21/24) Total lines: 145 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d03/27/2026 - 14:18:40 Line coverage: 93.2% (96/103) Branch coverage: 91.1% (31/34) Total lines: 291 Tag: Kestrun/Kestrun@63388ea9aed376ffbb41cd2727be2fb7646f6402 11/19/2025 - 17:40:50 Line coverage: 84.6% (66/78) Branch coverage: 63.6% (14/22) Total lines: 142 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02601/02/2026 - 00:16:25 Line coverage: 84.2% (64/76) Branch coverage: 63.6% (14/22) Total lines: 140 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/24/2026 - 19:35:59 Line coverage: 83.3% (65/78) Branch coverage: 62.5% (15/24) Total lines: 145 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5103/26/2026 - 03:54:59 Line coverage: 91% (71/78) Branch coverage: 87.5% (21/24) Total lines: 145 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d03/27/2026 - 14:18:40 Line coverage: 93.2% (96/103) Branch coverage: 91.1% (31/34) Total lines: 291 Tag: Kestrun/Kestrun@63388ea9aed376ffbb41cd2727be2fb7646f6402

Coverage delta

Coverage delta 25 -25

Metrics

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Middleware/PowerShellRunspaceMiddleware.cs

#LineLine coverage
 1using System.Management.Automation;
 2using System.Diagnostics;
 3using System.Management.Automation.Runspaces;
 4using Kestrun.Languages;
 5using Kestrun.Models;
 6using Kestrun.Scripting;
 7using Serilog.Events;
 8using Kestrun.Hosting;
 9using Kestrun.Logging;
 10
 11namespace Kestrun.Middleware;
 12
 13/// <summary>
 14/// Initializes a new instance of the <see cref="PowerShellRunspaceMiddleware"/> class.
 15/// </summary>
 16/// <param name="next">The next middleware in the pipeline.</param>
 17/// <param name="pool">The runspace pool manager.</param>
 418public sealed class PowerShellRunspaceMiddleware(RequestDelegate next, KestrunRunspacePoolManager pool)
 19{
 420    private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next));
 421    private readonly KestrunRunspacePoolManager _pool = pool ?? throw new ArgumentNullException(nameof(pool));
 22    private static int _inFlight; // diagnostic concurrency counter
 23
 5924    private KestrunHost Host => _pool.Host;
 5625    private Serilog.ILogger Log => Host.Logger;
 26
 27    /// <summary>
 28    /// Processes an HTTP request using a PowerShell runspace from the pool.
 29    /// </summary>
 30    /// <param name="context">The HTTP context for the current request.</param>
 31    public async Task InvokeAsync(HttpContext context)
 32    {
 433        var start = DateTime.UtcNow;
 434        var current = BeginRequestDiagnostics(context, start);
 435        Runspace? runspace = null;
 436        PowerShell? ps = null;
 437        var cleanupTransferredToResponse = false;
 38
 39        try
 40        {
 441            LogMiddlewareStarted(context);
 442            runspace = await AcquireRunspaceAsync(context, current);
 343            ps = CreatePowerShellInstance(runspace);
 344            InitializeRequestContext(context, ps);
 345            RegisterDeferredCleanup(context, ps, runspace);
 346            cleanupTransferredToResponse = true;
 47
 348            LogPipelineContinuation(context);
 349            await _next(context); // continue the pipeline
 350            LogMiddlewareCompleted(context);
 351        }
 152        catch (Exception ex)
 53        {
 154            Log.Error(ex, "Error occurred in PowerShellRunspaceMiddleware");
 155            throw; // allow ExceptionHandler to catch and handle (re-exec or JSON)
 56        }
 57        finally
 58        {
 459            if (!cleanupTransferredToResponse)
 60            {
 161                CleanupRequestResources(context, ps, runspace);
 62            }
 63
 464            CompleteRequestDiagnostics(context, start);
 65        }
 366    }
 67
 68    /// <summary>
 69    /// Records the initial request diagnostics and returns the current in-flight count.
 70    /// </summary>
 71    /// <param name="context">The current HTTP context.</param>
 72    /// <param name="start">The request start time in UTC.</param>
 73    /// <returns>The number of requests currently in flight.</returns>
 74    private int BeginRequestDiagnostics(HttpContext context, DateTime start)
 75    {
 476        var current = Interlocked.Increment(ref _inFlight);
 477        if (Log.IsEnabled(LogEventLevel.Debug))
 78        {
 479            Log.DebugSanitized("ENTER InvokeAsync path={Path} inFlight={InFlight} thread={Thread} time={Start}",
 480                context.Request.Path, current, Environment.CurrentManagedThreadId, start.ToString("O"));
 81        }
 82
 483        return current;
 84    }
 85
 86    /// <summary>
 87    /// Logs the middleware entry for the current request when debug logging is enabled.
 88    /// </summary>
 89    /// <param name="context">The current HTTP context.</param>
 90    private void LogMiddlewareStarted(HttpContext context)
 91    {
 492        if (Log.IsEnabled(LogEventLevel.Debug))
 93        {
 494            Log.DebugSanitized("PowerShellRunspaceMiddleware started for {Path}", context.Request.Path);
 95        }
 496    }
 97
 98    /// <summary>
 99    /// Acquires a runspace for the request and logs the acquisition duration.
 100    /// </summary>
 101    /// <param name="context">The current HTTP context.</param>
 102    /// <param name="inFlight">The current in-flight request count.</param>
 103    /// <returns>The acquired runspace.</returns>
 104    private async Task<Runspace> AcquireRunspaceAsync(HttpContext context, int inFlight)
 105    {
 4106        var acquireStart = Stopwatch.GetTimestamp();
 4107        var runspace = await _pool.AcquireAsync(context.RequestAborted);
 3108        var acquireMs = (Stopwatch.GetTimestamp() - acquireStart) * 1000.0 / Stopwatch.Frequency;
 3109        if (Log.IsEnabled(LogEventLevel.Debug))
 110        {
 3111            Log.DebugSanitized("Runspace acquired for {Path} in {AcquireMs} ms (inFlight={InFlight})", context.Request.P
 112        }
 113
 3114        return runspace;
 3115    }
 116
 117    /// <summary>
 118    /// Creates a PowerShell instance bound to the provided runspace.
 119    /// </summary>
 120    /// <param name="runspace">The runspace assigned to the current request.</param>
 121    /// <returns>A PowerShell instance that uses the provided runspace.</returns>
 122    private static PowerShell CreatePowerShellInstance(Runspace runspace)
 123    {
 3124        var ps = PowerShell.Create();
 3125        ps.Runspace = runspace;
 3126        return ps;
 127    }
 128
 129    /// <summary>
 130    /// Initializes the request-specific PowerShell and Kestrun context state.
 131    /// </summary>
 132    /// <param name="context">The current HTTP context.</param>
 133    /// <param name="ps">The PowerShell instance serving the request.</param>
 134    private void InitializeRequestContext(HttpContext context, PowerShell ps)
 135    {
 3136        context.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 137
 3138        var kestrunContext = new KestrunContext(Host, context);
 3139        context.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kestrunContext;
 140
 3141        if (Log.IsEnabled(LogEventLevel.Debug))
 142        {
 3143            Log.DebugSanitized("PowerShellRunspaceMiddleware - Setting KestrunContext in HttpContext.Items for {Path}", 
 144        }
 145
 3146        Log.Verbose("Setting PowerShell variables for Request and Response in the runspace.");
 3147        var sessionState = ps.Runspace.SessionStateProxy;
 3148        sessionState.SetVariable("Context", kestrunContext);
 149
 3150        if (context.Items.TryGetValue("KrLocalizer", out var localizer))
 151        {
 0152            sessionState.SetVariable("Localizer", localizer);
 153        }
 3154    }
 155
 156    /// <summary>
 157    /// Registers response completion cleanup so the runspace remains available to later middleware.
 158    /// </summary>
 159    /// <param name="context">The current HTTP context.</param>
 160    /// <param name="ps">The PowerShell instance serving the request.</param>
 161    /// <param name="runspace">The runspace serving the request.</param>
 162    private void RegisterDeferredCleanup(HttpContext context, PowerShell ps, Runspace runspace)
 163    {
 3164        context.Response.OnCompleted(() =>
 3165        {
 1166            CleanupPowerShellInstance(ps, "OnCompleted: Error disposing PowerShell instance", "OnCompleted: Disposing Po
 1167            ReleaseRunspace(runspace, "OnCompleted: Error returning runspace to pool", "OnCompleted: Returning runspace 
 1168            ClearRequestItems(context);
 1169            return Task.CompletedTask;
 3170        });
 3171    }
 172
 173    /// <summary>
 174    /// Logs that the middleware is continuing to the next pipeline component.
 175    /// </summary>
 176    /// <param name="context">The current HTTP context.</param>
 177    private void LogPipelineContinuation(HttpContext context)
 178    {
 3179        if (Log.IsEnabled(LogEventLevel.Debug))
 180        {
 3181            Log.DebugSanitized("PowerShellRunspaceMiddleware - Continuing Pipeline  for {Path}", context.Request.Path);
 182        }
 3183    }
 184
 185    /// <summary>
 186    /// Logs successful middleware completion for the current request.
 187    /// </summary>
 188    /// <param name="context">The current HTTP context.</param>
 189    private void LogMiddlewareCompleted(HttpContext context)
 190    {
 3191        if (Log.IsEnabled(LogEventLevel.Debug))
 192        {
 3193            Log.DebugSanitized("PowerShellRunspaceMiddleware completed for {Path}", context.Request.Path);
 194        }
 3195    }
 196
 197    /// <summary>
 198    /// Cleans up request resources immediately when response completion cleanup was not registered.
 199    /// </summary>
 200    /// <param name="context">The current HTTP context.</param>
 201    /// <param name="ps">The PowerShell instance serving the request.</param>
 202    /// <param name="runspace">The runspace serving the request.</param>
 203    private void CleanupRequestResources(HttpContext context, PowerShell? ps, Runspace? runspace)
 204    {
 1205        CleanupPowerShellInstance(ps, "Error disposing PowerShell instance during middleware cleanup");
 1206        ReleaseRunspace(runspace, "Error returning runspace to pool during middleware cleanup");
 1207        ClearRequestItems(context);
 1208    }
 209
 210    /// <summary>
 211    /// Disposes the PowerShell instance with debug-level error handling.
 212    /// </summary>
 213    /// <param name="ps">The PowerShell instance to dispose.</param>
 214    /// <param name="errorMessage">The message to log if disposal fails.</param>
 215    /// <param name="successMessageTemplate">An optional debug log template used before disposal.</param>
 216    private void CleanupPowerShellInstance(PowerShell? ps, string errorMessage, string? successMessageTemplate = null)
 217    {
 218        try
 219        {
 2220            if (ps is null)
 221            {
 1222                return;
 223            }
 224
 1225            if (successMessageTemplate is not null && Log.IsEnabled(LogEventLevel.Debug))
 226            {
 1227                Log.Debug(successMessageTemplate, ps.InstanceId);
 228            }
 229
 1230            ps.Dispose();
 1231        }
 0232        catch (Exception ex)
 233        {
 0234            Log.Debug(ex, errorMessage);
 0235        }
 2236    }
 237
 238    /// <summary>
 239    /// Returns the runspace to the pool with debug-level error handling.
 240    /// </summary>
 241    /// <param name="runspace">The runspace to release.</param>
 242    /// <param name="errorMessage">The message to log if release fails.</param>
 243    /// <param name="successMessageTemplate">An optional debug log template used before release.</param>
 244    private void ReleaseRunspace(Runspace? runspace, string errorMessage, string? successMessageTemplate = null)
 245    {
 246        try
 247        {
 2248            if (runspace is null)
 249            {
 1250                return;
 251            }
 252
 1253            if (successMessageTemplate is not null && Log.IsEnabled(LogEventLevel.Debug))
 254            {
 1255                Log.Debug(successMessageTemplate, runspace.InstanceId, runspace.Name, runspace.Id);
 256            }
 257
 1258            _pool.Release(runspace);
 1259        }
 0260        catch (Exception ex)
 261        {
 0262            Log.Debug(ex, errorMessage);
 0263        }
 2264    }
 265
 266    /// <summary>
 267    /// Removes request-scoped middleware state from the HTTP context.
 268    /// </summary>
 269    /// <param name="context">The current HTTP context.</param>
 270    private static void ClearRequestItems(HttpContext context)
 271    {
 2272        _ = context.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 2273        _ = context.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 2274    }
 275
 276    /// <summary>
 277    /// Records the final request diagnostics after the middleware finishes processing.
 278    /// </summary>
 279    /// <param name="context">The current HTTP context.</param>
 280    /// <param name="start">The request start time in UTC.</param>
 281    private void CompleteRequestDiagnostics(HttpContext context, DateTime start)
 282    {
 4283        var remaining = Interlocked.Decrement(ref _inFlight);
 4284        var durationMs = (DateTime.UtcNow - start).TotalMilliseconds;
 4285        if (Log.IsEnabled(LogEventLevel.Debug))
 286        {
 4287            Log.DebugSanitized("PowerShellRunspaceMiddleware ended for {Path} durationMs={durationMs} inFlight={remainin
 4288                context.Request.Path, durationMs, remaining);
 289        }
 4290    }
 291}