< 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@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
80%
Covered lines: 91
Uncovered lines: 22
Coverable lines: 113
Total lines: 194
Line coverage: 80.5%
Branch coverage
63%
Covered branches: 23
Total branches: 36
Branch coverage: 63.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)100%2287.5%
GetPowerShellFromContext(...)66.66%66100%
GetKestrunContext(...)50%22100%
SetArgumentsAsVariables(...)33.33%19628.57%
AddScript(...)100%11100%
InvokeScriptAsync()100%11100%
LogTopResults(...)66.66%7675%
HandleErrorsIfAnyAsync()100%44100%
LogSideChannelMessagesIfAny(...)50%12860%
HandleRedirectIfAny(...)100%22100%
ApplyResponseAsync(...)100%11100%
CompleteResponseSafelyAsync()100%1140%

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.Utilities;
 5using Serilog.Events;
 6
 7namespace Kestrun.Languages;
 8
 9internal static class PowerShellDelegateBuilder
 10{
 11    public const string PS_INSTANCE_KEY = "PS_INSTANCE";
 12    public const string KR_CONTEXT_KEY = "KR_CONTEXT";
 13
 14    internal static RequestDelegate Build(string code, Serilog.ILogger log, Dictionary<string, object?>? arguments)
 15    {
 516        ArgumentNullException.ThrowIfNull(code);
 517        if (log.IsEnabled(LogEventLevel.Debug))
 18        {
 419            log.Debug("Building PowerShell delegate, script length={Length}", code.Length);
 20        }
 21
 522        return async context =>
 523        {
 524            if (log.IsEnabled(LogEventLevel.Debug))
 525            {
 426                log.DebugSanitized("PS delegate invoked for {Path}", context.Request.Path);
 527            }
 528
 529            var ps = GetPowerShellFromContext(context, log);
 530            // Ensure the runspace pool is open before executing the script
 531            try
 532            {
 433                SetArgumentsAsVariables(ps, arguments, log);
 534
 435                log.Verbose("Setting PowerShell variables for Request and Response in the runspace.");
 436                var krContext = GetKestrunContext(context);
 537
 438                AddScript(ps, code);
 439                var psResults = await InvokeScriptAsync(ps, log).ConfigureAwait(false);
 440                LogTopResults(log, psResults);
 541
 442                if (await HandleErrorsIfAnyAsync(context, ps).ConfigureAwait(false))
 543                {
 144                    return;
 545                }
 546
 347                LogSideChannelMessagesIfAny(log, ps);
 548
 349                if (HandleRedirectIfAny(context, krContext, log))
 550                {
 151                    return;
 552                }
 553
 254                log.Verbose("Applying response to HttpResponse...");
 255                await ApplyResponseAsync(context, krContext).ConfigureAwait(false);
 256            }
 557            // optional: catch client cancellation to avoid noisy logs
 058            catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
 559            {
 560                // client disconnected – nothing to send
 061            }
 062            catch (Exception ex)
 563            {
 564                // Log the exception (optional)
 065                log.Error(ex, "PowerShell script failed - {Preview}", code[..Math.Min(40, code.Length)]);
 066                context.Response.StatusCode = 500; // Internal Server Error
 067                context.Response.ContentType = "text/plain; charset=utf-8";
 068                await context.Response.WriteAsync("An error occurred while processing your request.");
 569            }
 570            finally
 571            {
 472                await CompleteResponseSafelyAsync(context, log).ConfigureAwait(false);
 573            }
 974        };
 75    }
 76
 77    private static PowerShell GetPowerShellFromContext(HttpContext context, Serilog.ILogger log)
 78    {
 579        if (!context.Items.ContainsKey(PS_INSTANCE_KEY))
 80        {
 181            throw new InvalidOperationException("PowerShell runspace not found in context items. Ensure PowerShellRunspa
 82        }
 83
 484        log.Verbose("Retrieving PowerShell instance from context items.");
 485        var ps = context.Items[PS_INSTANCE_KEY] as PowerShell
 486                 ?? throw new InvalidOperationException("PowerShell instance not found in context items.");
 487        return ps.Runspace == null
 488            ? throw new InvalidOperationException("PowerShell runspace is not set. Ensure PowerShellRunspaceMiddleware i
 489            : ps;
 90    }
 91
 92    private static KestrunContext GetKestrunContext(HttpContext context)
 493        => context.Items[KR_CONTEXT_KEY] as KestrunContext
 494           ?? throw new InvalidOperationException($"{KR_CONTEXT_KEY} key not found in context items.");
 95
 96    private static void SetArgumentsAsVariables(PowerShell ps, Dictionary<string, object?>? arguments, Serilog.ILogger l
 97    {
 498        if (arguments is null || arguments.Count == 0)
 99        {
 4100            return;
 101        }
 102
 0103        log.Verbose("Setting PowerShell variables from arguments: {Count}", arguments.Count);
 0104        var ss = ps.Runspace!.SessionStateProxy;
 0105        foreach (var arg in arguments)
 106        {
 0107            ss.SetVariable(arg.Key, arg.Value);
 108        }
 0109    }
 110
 4111    private static void AddScript(PowerShell ps, string code) => _ = ps.AddScript(code);
 112
 113    private static async Task<PSDataCollection<PSObject>> InvokeScriptAsync(PowerShell ps, Serilog.ILogger log)
 114    {
 4115        log.Verbose("Executing PowerShell script...");
 4116        var results = await ps.InvokeAsync().ConfigureAwait(false);
 4117        log.Verbose($"PowerShell script executed with {results.Count} results.");
 4118        return results;
 4119    }
 120
 121    private static void LogTopResults(Serilog.ILogger log, PSDataCollection<PSObject> psResults)
 122    {
 4123        if (!log.IsEnabled(LogEventLevel.Debug))
 124        {
 1125            return;
 126        }
 127
 3128        log.Debug("PowerShell script output:");
 6129        foreach (var r in psResults.Take(10))
 130        {
 0131            log.Debug("   • {Result}", r);
 132        }
 3133        if (psResults.Count > 10)
 134        {
 0135            log.Debug("   … {Count} more", psResults.Count - 10);
 136        }
 3137    }
 138
 139    private static async Task<bool> HandleErrorsIfAnyAsync(HttpContext context, PowerShell ps)
 140    {
 4141        if (ps.HadErrors || ps.Streams.Error.Count != 0)
 142        {
 1143            await BuildError.ResponseAsync(context, ps).ConfigureAwait(false);
 1144            return true;
 145        }
 3146        return false;
 4147    }
 148
 149    private static void LogSideChannelMessagesIfAny(Serilog.ILogger log, PowerShell ps)
 150    {
 3151        if (ps.Streams.Verbose.Count > 0 || ps.Streams.Debug.Count > 0 || ps.Streams.Warning.Count > 0 || ps.Streams.Inf
 152        {
 0153            log.Verbose("PowerShell script completed with verbose/debug/warning/info messages.");
 0154            log.Verbose(BuildError.Text(ps));
 155        }
 3156        log.Verbose("PowerShell script completed successfully.");
 3157    }
 158
 159    private static bool HandleRedirectIfAny(HttpContext context, KestrunContext krContext, Serilog.ILogger log)
 160    {
 3161        if (!string.IsNullOrEmpty(krContext.Response.RedirectUrl))
 162        {
 1163            log.Verbose($"Redirecting to {krContext.Response.RedirectUrl}");
 1164            context.Response.Redirect(krContext.Response.RedirectUrl);
 1165            return true;
 166        }
 2167        return false;
 168    }
 169
 170    private static Task ApplyResponseAsync(HttpContext context, KestrunContext krContext)
 2171        => krContext.Response.ApplyTo(context.Response);
 172
 173    private static async Task CompleteResponseSafelyAsync(HttpContext context, Serilog.ILogger log)
 174    {
 175        // CompleteAsync is idempotent – safe to call once more
 176        try
 177        {
 4178            log.Verbose("Completing response for " + context.Request.Path);
 4179            await context.Response.CompleteAsync().ConfigureAwait(false);
 4180        }
 0181        catch (ObjectDisposedException odex)
 182        {
 183            // This can happen if the response has already been completed
 184            // or the client has disconnected
 0185            log.DebugSanitized(odex, "Response already completed for {Path}", context.Request.Path);
 0186        }
 0187        catch (InvalidOperationException ioex)
 188        {
 189            // This can happen if the response has already been completed
 0190            log.DebugSanitized(ioex, "Response already completed for {Path}", context.Request.Path);
 191            // No action needed, as the response is already completed
 0192        }
 4193    }
 194}