< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.PowerShellDelegateBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
86%
Covered lines: 103
Uncovered lines: 16
Coverable lines: 119
Total lines: 208
Line coverage: 86.5%
Branch coverage
70%
Covered branches: 21
Total branches: 30
Branch coverage: 70%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 14:53:17 Line coverage: 75.2% (85/113) Branch coverage: 52.7% (19/36) Total lines: 194 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/06/2025 - 18:30:33 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/14/2025 - 12:29:34 Line coverage: 87.6% (92/105) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 87.7% (93/106) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.5% (103/119) Branch coverage: 70% (21/30) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd 08/26/2025 - 14:53:17 Line coverage: 75.2% (85/113) Branch coverage: 52.7% (19/36) Total lines: 194 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/06/2025 - 18:30:33 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/14/2025 - 12:29:34 Line coverage: 87.6% (92/105) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 87.7% (93/106) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.5% (103/119) Branch coverage: 70% (21/30) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)100%2285.71%
<Build()50%1818100%
GetPowerShellFromContext(...)66.66%66100%
GetKestrunContext(...)50%22100%
LogTopResults(...)66.66%7675%
HandleErrorsIfAnyAsync()100%44100%
LogSideChannelMessagesIfAny(...)50%12860%
HandleRedirectIfAny(...)100%22100%
ApplyResponseAsync(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs

#LineLine coverage
 1using System.Management.Automation;
 2using Kestrun.Hosting;
 3using Kestrun.Logging;
 4using Kestrun.Models;
 5using Kestrun.Utilities;
 6using Serilog.Events;
 7
 8namespace Kestrun.Languages;
 9
 10internal static class PowerShellDelegateBuilder
 11{
 12    public const string PS_INSTANCE_KEY = "PS_INSTANCE";
 13    public const string KR_CONTEXT_KEY = "KR_CONTEXT";
 14
 15    internal static RequestDelegate Build(KestrunHost host, string code, Dictionary<string, object?>? arguments)
 16    {
 617        var log = host.Logger;
 618        ArgumentNullException.ThrowIfNull(code);
 619        if (log.IsEnabled(LogEventLevel.Debug))
 20        {
 421            log.Debug("Building PowerShell delegate, script length={Length}", code.Length);
 22        }
 23
 624        return async context =>
 625        {
 626            // Log invocation
 527            if (log.IsEnabled(LogEventLevel.Debug))
 628            {
 329                log.DebugSanitized("PS delegate invoked for {Path}", context.Request.Path);
 630            }
 631            // Prepare for execution
 532            KestrunContext? krContext = null;
 633            // Get the PowerShell instance from the context (set by middleware)
 534            var ps = GetPowerShellFromContext(context, log);
 635
 636            // Ensure the runspace pool is open before executing the script
 637            try
 638            {
 439                PowerShellExecutionHelpers.SetVariables(ps, arguments, log);
 440                if (log.IsEnabled(LogEventLevel.Verbose))
 641                {
 042                    log.Verbose("Setting PowerShell variables for Request and Response in the runspace.");
 643                }
 444                krContext = GetKestrunContext(context);
 645
 446                PowerShellExecutionHelpers.AddScript(ps, code);
 647
 648                // Extract and add parameters for injection
 449                ParameterForInjectionInfo.InjectParameters(krContext, ps);
 650
 651                // Execute the script
 452                if (log.IsEnabled(LogEventLevel.Verbose))
 653                {
 054                    log.Verbose("Invoking PowerShell script...");
 655                }
 456                var psResults = await ps.InvokeAsync(log, context.RequestAborted).ConfigureAwait(false);
 457                LogTopResults(log, psResults);
 658
 459                if (await HandleErrorsIfAnyAsync(context, ps).ConfigureAwait(false))
 660                {
 161                    return;
 662                }
 663
 364                LogSideChannelMessagesIfAny(log, ps);
 665
 366                if (HandleRedirectIfAny(context, krContext, log))
 667                {
 168                    return;
 669                }
 270                if (log.IsEnabled(LogEventLevel.Verbose))
 671                {
 072                    log.Verbose("No redirect detected; applying response to HttpResponse...");
 673                }
 274                await ApplyResponseAsync(context, krContext).ConfigureAwait(false);
 275            }
 676            // optional: catch client cancellation to avoid noisy logs
 077            catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
 678            {
 679                // client disconnected – nothing to send
 080            }
 081            catch (Exception ex)
 682            {
 683                // If we have exception options, set a 500 status code and generic message.
 684                // Otherwise rethrow to let higher-level middleware handle it (e.g., Developer Exception Page
 085                if (krContext?.Host?.ExceptionOptions is null)
 686                { // Log and handle script errors
 087                    log.Error(ex, "PowerShell script failed - {Preview}", code[..Math.Min(40, code.Length)]);
 088                    context.Response.StatusCode = 500; // Internal Server Error
 089                    context.Response.ContentType = "text/plain; charset=utf-8";
 090                    await context.Response.WriteAsync("An error occurred while processing your request.");
 691                }
 692                else
 693                {
 694                    // re-throw to let higher-level middleware handle it (e.g., Developer Exception Page)
 095                    throw;
 696                }
 697            }
 698            finally
 699            {
 6100                // Do not call Response.CompleteAsync here; leaving the response open allows
 6101                // downstream middleware like StatusCodePages to generate a body for status-only responses.
 6102            }
 6103        };
 4104    }
 105
 106    /// <summary>
 107    /// Retrieves the PowerShell instance from the HttpContext items.
 108    /// </summary>
 109    /// <param name="context">The HttpContext from which to retrieve the PowerShell instance.</param>
 110    /// <param name="log">The logger to use for logging.</param>
 111    /// <returns>The PowerShell instance associated with the current request.</returns>
 112    /// <exception cref="InvalidOperationException">Thrown if the PowerShell instance is not found in the context items.
 113    private static PowerShell GetPowerShellFromContext(HttpContext context, Serilog.ILogger log)
 114    {
 5115        if (!context.Items.ContainsKey(PS_INSTANCE_KEY))
 116        {
 1117            throw new InvalidOperationException("PowerShell runspace not found in context items. Ensure PowerShellRunspa
 118        }
 119
 4120        log.Verbose("Retrieving PowerShell instance from context items.");
 4121        var ps = context.Items[PS_INSTANCE_KEY] as PowerShell
 4122                 ?? throw new InvalidOperationException("PowerShell instance not found in context items.");
 4123        return ps.Runspace == null
 4124            ? throw new InvalidOperationException("PowerShell runspace is not set. Ensure PowerShellRunspaceMiddleware i
 4125            : ps;
 126    }
 127
 128    /// <summary>
 129    /// Retrieves the KestrunContext from the HttpContext items.
 130    /// </summary>
 131    /// <param name="context">The HttpContext from which to retrieve the KestrunContext.</param>
 132    /// <returns>The KestrunContext associated with the current request.</returns>
 133    /// <exception cref="InvalidOperationException">Thrown if the KestrunContext is not found in the context items.</exc
 134    private static KestrunContext GetKestrunContext(HttpContext context)
 4135        => context.Items[KR_CONTEXT_KEY] as KestrunContext
 4136           ?? throw new InvalidOperationException($"{KR_CONTEXT_KEY} key not found in context items.");
 137
 138    ///<summary>
 139    /// Logs the top results from the PowerShell script output for debugging purposes.
 140    /// Only logs if the log level is set to Debug.
 141    /// </summary>
 142    /// <param name="log">The logger to use for logging.</param>
 143    /// <param name="psResults">The collection of PSObject results from the PowerShell script.</param>
 144    private static void LogTopResults(Serilog.ILogger log, PSDataCollection<PSObject> psResults)
 145    {
 4146        if (!log.IsEnabled(LogEventLevel.Debug))
 147        {
 2148            return;
 149        }
 150
 2151        log.Debug("PowerShell script output:");
 4152        foreach (var r in psResults.Take(10))
 153        {
 0154            log.Debug("   • {Result}", r);
 155        }
 2156        if (psResults.Count > 10)
 157        {
 0158            log.Debug("   … {Count} more", psResults.Count - 10);
 159        }
 2160    }
 161
 162    /// <summary>
 163    /// Handles any errors that occurred during the PowerShell script execution.
 164    /// </summary>
 165    /// <param name="context">The HttpContext for the current request.</param>
 166    /// <param name="ps">The PowerShell instance used for script execution.</param>
 167    /// <returns>True if errors were handled, false otherwise.</returns>
 168    private static async Task<bool> HandleErrorsIfAnyAsync(HttpContext context, PowerShell ps)
 169    {
 4170        if (ps.HadErrors || ps.Streams.Error.Count != 0)
 171        {
 1172            await BuildError.ResponseAsync(context, ps).ConfigureAwait(false);
 1173            return true;
 174        }
 3175        return false;
 4176    }
 177
 178    /// <summary>
 179    /// Logs any side-channel messages (Verbose, Debug, Warning, Information) produced by the PowerShell script.
 180    /// </summary>
 181    /// <param name="log">The logger to use for logging.</param>
 182    /// <param name="ps">The PowerShell instance used to invoke the script.</param>
 183    private static void LogSideChannelMessagesIfAny(Serilog.ILogger log, PowerShell ps)
 184    {
 3185        if (ps.Streams.Verbose.Count > 0 || ps.Streams.Debug.Count > 0 || ps.Streams.Warning.Count > 0 || ps.Streams.Inf
 186        {
 0187            log.Verbose("PowerShell script completed with verbose/debug/warning/info messages.");
 0188            log.Verbose(BuildError.Text(ps));
 189        }
 3190        log.Verbose("PowerShell script completed successfully.");
 3191    }
 192
 193    private static bool HandleRedirectIfAny(HttpContext context, KestrunContext krContext, Serilog.ILogger log)
 194    {
 3195        if (!string.IsNullOrEmpty(krContext.Response.RedirectUrl))
 196        {
 1197            log.Verbose($"Redirecting to {krContext.Response.RedirectUrl}");
 1198            context.Response.Redirect(krContext.Response.RedirectUrl);
 1199            return true;
 200        }
 2201        return false;
 202    }
 203
 204    private static Task ApplyResponseAsync(HttpContext context, KestrunContext krContext)
 2205        => krContext.Response.ApplyTo(context.Response);
 206
 207    // Removed explicit Response.CompleteAsync to allow StatusCodePages to run after endpoints when appropriate.
 208}