< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.KestrunHost
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHost.cs
Tag: Kestrun/Kestrun@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
73%
Covered lines: 256
Uncovered lines: 90
Coverable lines: 346
Total lines: 919
Line coverage: 73.9%
Branch coverage
71%
Covered branches: 108
Total branches: 152
Branch coverage: 71%
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
get_Builder()100%11100%
get_App()100%22100%
get_ApplicationName()100%22100%
get_Options()100%11100%
.ctor(...)75%44100%
get_IsConfigured()100%11100%
get_RunspacePool()0%620%
get_KestrunRoot()100%11100%
get_HostLogger()100%11100%
get_Scheduler()100%11100%
get_RouteGroupStack()100%11100%
get_FeatureQueue()100%11100%
.ctor(...)100%11100%
LogConstructorArgs(...)100%22100%
SetWorkingDirectoryIfNeeded(...)100%44100%
AddKestrunModulePathIfMissing(...)75%8883.33%
InitializeOptions(...)100%22100%
AddUserModulePaths(...)70%111077.77%
ConfigureListener(...)100%88100%
ConfigureListener(...)100%11100%
ConfigureListener(...)100%11100%
EnableConfiguration(...)50%592460.6%
Build()75%131283.33%
AddService(...)100%11100%
Use(...)100%11100%
AddFeature(...)100%11100%
AddScheduling(...)100%1212100%
AddControllers(...)0%620%
AddPowerShellRuntime(...)100%22100%
AddSignalR(...)0%620%
Run()0%2040%
StartAsync()100%44100%
StopAsync()100%5466.66%
Stop()75%44100%
get_IsRunning()75%44100%
CreateRunspacePool(...)69.23%1012651.92%
Dispose()62.5%88100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHost.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Server.Kestrel.Core;
 2using System.Net;
 3using System.Management.Automation;
 4using System.Management.Automation.Runspaces;
 5using Kestrun.Utilities;
 6using Microsoft.CodeAnalysis;
 7using System.Reflection;
 8using System.Security.Cryptography.X509Certificates;
 9using Serilog;
 10using Serilog.Events;
 11using Microsoft.AspNetCore.SignalR;
 12using Kestrun.Scheduling;
 13using Kestrun.SharedState;
 14using Kestrun.Middleware;
 15using Kestrun.Scripting;
 16using Kestrun.Hosting.Options;
 17using System.Runtime.InteropServices;
 18using Microsoft.PowerShell;
 19
 20namespace Kestrun.Hosting;
 21
 22/// <summary>
 23/// Provides hosting and configuration for the Kestrun application, including service registration, middleware setup, an
 24/// </summary>
 25public class KestrunHost : IDisposable
 26{
 27    #region Fields
 31228    internal WebApplicationBuilder Builder { get; }
 29
 30    private WebApplication? _app;
 31
 5532    internal WebApplication App => _app ?? throw new InvalidOperationException("WebApplication is not built yet. Call Bu
 33
 34    /// <summary>
 35    /// Gets the application name for the Kestrun host.
 36    /// </summary>
 237    public string ApplicationName => Options.ApplicationName ?? "KestrunApp";
 38
 39    /// <summary>
 40    /// Gets the configuration options for the Kestrun host.
 41    /// </summary>
 48742    public KestrunOptions Options { get; private set; } = new();
 13843    private readonly List<string> _modulePaths = [];
 44
 45    /// <summary>
 46    /// Indicates whether the Kestrun host configuration has been applied.
 47    /// </summary>
 7248    public bool IsConfigured { get; private set; }
 49
 50    private KestrunRunspacePoolManager? _runspacePool;
 51
 052    internal KestrunRunspacePoolManager RunspacePool => _runspacePool ?? throw new InvalidOperationException("Runspace p
 53    /// <summary>
 54    /// Gets the root directory path for the Kestrun application.
 55    /// </summary>
 7856    public string? KestrunRoot { get; private set; }
 57
 58    /// <summary>
 59    /// Gets the Serilog logger instance used by the Kestrun host.
 60    /// </summary>
 168261    public Serilog.ILogger HostLogger { get; private set; }
 62
 63    /// <summary>
 64    /// Gets the scheduler service used for managing scheduled tasks in the Kestrun host.
 65    /// </summary>
 2566    public SchedulerService Scheduler { get; internal set; } = null!; // Initialized in ConfigureServices
 67
 68
 69    /// <summary>
 70    /// Gets the stack used for managing route groups in the Kestrun host.
 71    /// </summary>
 13872    public System.Collections.Stack RouteGroupStack { get; } = new();
 73
 74    // ── ✦ QUEUE #1 : SERVICE REGISTRATION ✦ ─────────────────────────────
 13875    private readonly List<Action<IServiceCollection>> _serviceQueue = [];
 76
 77    // ── ✦ QUEUE #2 : MIDDLEWARE STAGES ✦ ────────────────────────────────
 13878    private readonly List<Action<IApplicationBuilder>> _middlewareQueue = [];
 79
 18680    internal List<Action<KestrunHost>> FeatureQueue { get; } = [];
 81
 13882    internal readonly Dictionary<(string Pattern, string Method), MapRouteOptions> _registeredRoutes =
 13883    new(
 13884        new RouteKeyComparer());
 85
 86
 87    #endregion
 88
 89
 90    // Accepts optional module paths (from PowerShell)
 91    #region Constructor
 92
 93    /// <summary>
 94    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, root dire
 95    /// </summary>
 96    /// <param name="appName">The name of the application.</param>
 97    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 98    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 99    public KestrunHost(string? appName, string? kestrunRoot = null, string[]? modulePathsObj = null) :
 66100            this(appName, Log.Logger, kestrunRoot, modulePathsObj)
 66101    { }
 102
 103    /// <summary>
 104    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, logger, r
 105    /// </summary>
 106    /// <param name="appName">The name of the application.</param>
 107    /// <param name="logger">The Serilog logger instance to use.</param>
 108    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 109    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 138110    public KestrunHost(string? appName, Serilog.ILogger logger, string? kestrunRoot = null, string[]? modulePathsObj = n
 111    {
 112        // ① Logger
 138113        HostLogger = logger ?? Log.Logger;
 138114        LogConstructorArgs(appName, logger == null, kestrunRoot, modulePathsObj?.Length ?? 0);
 115
 116        // ② Working directory/root
 138117        SetWorkingDirectoryIfNeeded(kestrunRoot);
 118
 119        // ③ Ensure Kestrun module path is available
 138120        AddKestrunModulePathIfMissing(modulePathsObj);
 121
 122        // ④ Builder + logging
 138123        Builder = WebApplication.CreateBuilder();
 138124        _ = Builder.Host.UseSerilog();
 125
 126        // ⑤ Options
 138127        InitializeOptions(appName);
 128
 129        // ⑥ Add user-provided module paths
 138130        AddUserModulePaths(modulePathsObj);
 131
 138132        HostLogger.Information("Current working directory: {CurrentDirectory}", Directory.GetCurrentDirectory());
 138133    }
 134    #endregion
 135
 136    #region Helpers
 137
 138
 139    /// <summary>
 140    /// Logs constructor arguments at Debug level for diagnostics.
 141    /// </summary>
 142    private void LogConstructorArgs(string? appName, bool defaultLogger, string? kestrunRoot, int modulePathsLength)
 143    {
 138144        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 145        {
 88146            HostLogger.Debug(
 88147                "KestrunHost ctor: AppName={AppName}, DefaultLogger={DefaultLogger}, KestrunRoot={KestrunRoot}, ModulePa
 88148                appName, defaultLogger, kestrunRoot, modulePathsLength);
 149        }
 138150    }
 151
 152    /// <summary>
 153    /// Sets the current working directory to the provided Kestrun root if needed and stores it.
 154    /// </summary>
 155    /// <param name="kestrunRoot">The Kestrun root directory path.</param>
 156    private void SetWorkingDirectoryIfNeeded(string? kestrunRoot)
 157    {
 138158        if (string.IsNullOrWhiteSpace(kestrunRoot))
 159        {
 61160            return;
 161        }
 162
 77163        if (!string.Equals(Directory.GetCurrentDirectory(), kestrunRoot, StringComparison.Ordinal))
 164        {
 55165            Directory.SetCurrentDirectory(kestrunRoot);
 55166            HostLogger.Information("Changed current directory to Kestrun root: {KestrunRoot}", kestrunRoot);
 167        }
 168        else
 169        {
 22170            HostLogger.Verbose("Current directory is already set to Kestrun root: {KestrunRoot}", kestrunRoot);
 171        }
 172
 77173        KestrunRoot = kestrunRoot;
 77174    }
 175
 176    /// <summary>
 177    /// Ensures the core Kestrun module path is present; if missing, locates and adds it.
 178    /// </summary>
 179    /// <param name="modulePathsObj">The array of module paths to check.</param>
 180    private void AddKestrunModulePathIfMissing(string[]? modulePathsObj)
 181    {
 138182        var needsLocate = modulePathsObj is null ||
 160183                          (modulePathsObj?.Any(p => p.Contains("Kestrun.psm1", StringComparison.Ordinal)) == false);
 138184        if (!needsLocate)
 185        {
 22186            return;
 187        }
 188
 116189        var kestrunModulePath = PowerShellModuleLocator.LocateKestrunModule();
 116190        if (string.IsNullOrWhiteSpace(kestrunModulePath))
 191        {
 0192            HostLogger.Fatal("Kestrun module not found. Ensure the Kestrun module is installed.");
 0193            throw new FileNotFoundException("Kestrun module not found.");
 194        }
 195
 116196        HostLogger.Information("Found Kestrun module at: {KestrunModulePath}", kestrunModulePath);
 116197        HostLogger.Verbose("Adding Kestrun module path: {KestrunModulePath}", kestrunModulePath);
 116198        _modulePaths.Add(kestrunModulePath);
 116199    }
 200
 201    /// <summary>
 202    /// Initializes Kestrun options and sets the application name when provided.
 203    /// </summary>
 204    /// <param name="appName">The name of the application.</param>
 205    private void InitializeOptions(string? appName)
 206    {
 138207        if (string.IsNullOrEmpty(appName))
 208        {
 1209            HostLogger.Information("No application name provided, using default.");
 1210            Options = new KestrunOptions();
 211        }
 212        else
 213        {
 137214            HostLogger.Information("Setting application name: {AppName}", appName);
 137215            Options = new KestrunOptions { ApplicationName = appName };
 216        }
 137217    }
 218
 219    /// <summary>
 220    /// Adds user-provided module paths if they exist, logging warnings for invalid entries.
 221    /// </summary>
 222    /// <param name="modulePathsObj">The array of module paths to check.</param>
 223    private void AddUserModulePaths(string[]? modulePathsObj)
 224    {
 138225        if (modulePathsObj is IEnumerable<object> modulePathsEnum)
 226        {
 88227            foreach (var modPathObj in modulePathsEnum)
 228            {
 22229                if (modPathObj is string modPath && !string.IsNullOrWhiteSpace(modPath))
 230                {
 22231                    if (File.Exists(modPath))
 232                    {
 22233                        HostLogger.Information("[KestrunHost] Adding module path: {ModPath}", modPath);
 22234                        _modulePaths.Add(modPath);
 235                    }
 236                    else
 237                    {
 0238                        HostLogger.Warning("[KestrunHost] Module path does not exist: {ModPath}", modPath);
 239                    }
 240                }
 241                else
 242                {
 0243                    HostLogger.Warning("[KestrunHost] Invalid module path provided.");
 244                }
 245            }
 246        }
 138247    }
 248    #endregion
 249
 250
 251    #region ListenerOptions
 252
 253    /// <summary>
 254    /// Configures a listener for the Kestrun host with the specified port, optional IP address, certificate, protocols,
 255    /// </summary>
 256    /// <param name="port">The port number to listen on.</param>
 257    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 258    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 259    /// <param name="protocols">The HTTP protocols to use.</param>
 260    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 261    /// <returns>The current KestrunHost instance.</returns>
 262    public KestrunHost ConfigureListener(int port, IPAddress? ipAddress = null, X509Certificate2? x509Certificate = null
 263    {
 15264        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 265        {
 6266            HostLogger.Debug("ConfigureListener port={Port}, ipAddress={IPAddress}, protocols={Protocols}, useConnection
 267        }
 268
 15269        if (protocols == HttpProtocols.Http1AndHttp2AndHttp3 && !CcUtilities.PreviewFeaturesEnabled())
 270        {
 2271            HostLogger.Warning("Http3 is not supported in this version of Kestrun. Using Http1 and Http2 only.");
 2272            protocols = HttpProtocols.Http1AndHttp2;
 273        }
 274
 15275        Options.Listeners.Add(new ListenerOptions
 15276        {
 15277            IPAddress = ipAddress ?? IPAddress.Any,
 15278            Port = port,
 15279            UseHttps = x509Certificate != null,
 15280            X509Certificate = x509Certificate,
 15281            Protocols = protocols,
 15282            UseConnectionLogging = useConnectionLogging
 15283        });
 15284        return this;
 285    }
 286
 287    /// <summary>
 288    /// Configures a listener for the Kestrun host with the specified port, optional IP address, and connection logging.
 289    /// </summary>
 290    /// <param name="port">The port number to listen on.</param>
 291    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 292    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 9293    public void ConfigureListener(int port, IPAddress? ipAddress = null, bool useConnectionLogging = false) => _ = Confi
 294
 295    /// <summary>
 296    /// Configures a listener for the Kestrun host with the specified port and connection logging option.
 297    /// </summary>
 298    /// <param name="port">The port number to listen on.</param>
 299    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 1300    public void ConfigureListener(int port, bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAd
 301
 302    #endregion
 303
 304
 305
 306
 307    #region C#
 308
 309
 310    #endregion
 311
 312
 313
 314    #region Route
 315
 316
 317
 318    #endregion
 319    #region Configuration
 320
 321
 322    /// <summary>
 323    /// Applies the configured options to the Kestrel server and initializes the runspace pool.
 324    /// </summary>
 325    public void EnableConfiguration(Dictionary<string, object>? userVariables = null, Dictionary<string, string>? userFu
 326    {
 29327        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 328        {
 15329            HostLogger.Debug("EnableConfiguration(options) called");
 330        }
 331
 29332        if (IsConfigured)
 333        {
 7334            if (HostLogger.IsEnabled(LogEventLevel.Debug))
 335            {
 0336                HostLogger.Debug("Configuration already applied, skipping");
 337            }
 338
 7339            return; // Already configured
 340        }
 341        try
 342        {
 343            // This method is called to apply the configured options to the Kestrel server.
 344            // The actual application of options is done in the Run method.
 22345            _runspacePool = CreateRunspacePool(Options.MaxRunspaces, userVariables, userFunctions);
 22346            if (_runspacePool == null)
 347            {
 0348                throw new InvalidOperationException("Failed to create runspace pool.");
 349            }
 350
 22351            if (HostLogger.IsEnabled(LogEventLevel.Verbose))
 352            {
 0353                HostLogger.Verbose("Runspace pool created with max runspaces: {MaxRunspaces}", Options.MaxRunspaces);
 354            }
 355            // Configure Kestrel
 22356            _ = Builder.WebHost.UseKestrel(opts =>
 22357            {
 22358                opts.CopyFromTemplate(Options.ServerOptions);
 44359            });
 360
 22361            if (Options.NamedPipeOptions is not null)
 362            {
 0363                if (OperatingSystem.IsWindows())
 364                {
 0365                    _ = Builder.WebHost.UseNamedPipes(opts =>
 0366                    {
 0367                        opts.ListenerQueueCount = Options.NamedPipeOptions.ListenerQueueCount;
 0368                        opts.MaxReadBufferSize = Options.NamedPipeOptions.MaxReadBufferSize;
 0369                        opts.MaxWriteBufferSize = Options.NamedPipeOptions.MaxWriteBufferSize;
 0370                        opts.CurrentUserOnly = Options.NamedPipeOptions.CurrentUserOnly;
 0371                        opts.PipeSecurity = Options.NamedPipeOptions.PipeSecurity;
 0372                    });
 373                }
 374                else
 375                {
 0376                    HostLogger.Verbose("Named pipe listeners are supported only on Windows; skipping UseNamedPipes confi
 377                }
 378            }
 379
 380            // Apply Kestrel listeners and HTTPS settings
 22381            _ = Builder.WebHost.ConfigureKestrel(serverOptions =>
 22382            {
 22383                if (Options.HttpsConnectionAdapter is not null)
 22384                {
 0385                    HostLogger.Verbose("Applying HTTPS connection adapter options from KestrunOptions.");
 22386
 22387                    // Apply HTTPS defaults if needed
 0388                    serverOptions.ConfigureHttpsDefaults(httpsOptions =>
 0389                    {
 0390                        httpsOptions.SslProtocols = Options.HttpsConnectionAdapter.SslProtocols;
 0391                        httpsOptions.ClientCertificateMode = Options.HttpsConnectionAdapter.ClientCertificateMode;
 0392                        httpsOptions.ClientCertificateValidation = Options.HttpsConnectionAdapter.ClientCertificateValid
 0393                        httpsOptions.CheckCertificateRevocation = Options.HttpsConnectionAdapter.CheckCertificateRevocat
 0394                        httpsOptions.ServerCertificate = Options.HttpsConnectionAdapter.ServerCertificate;
 0395                        httpsOptions.ServerCertificateChain = Options.HttpsConnectionAdapter.ServerCertificateChain;
 0396                        httpsOptions.ServerCertificateSelector = Options.HttpsConnectionAdapter.ServerCertificateSelecto
 0397                        httpsOptions.HandshakeTimeout = Options.HttpsConnectionAdapter.HandshakeTimeout;
 0398                        httpsOptions.OnAuthenticate = Options.HttpsConnectionAdapter.OnAuthenticate;
 0399                    });
 22400                }
 22401                // Unix domain socket listeners
 44402                foreach (var unixSocket in Options.ListenUnixSockets)
 22403                {
 0404                    if (!string.IsNullOrWhiteSpace(unixSocket))
 22405                    {
 0406                        HostLogger.Verbose("Binding Unix socket: {Sock}", unixSocket);
 0407                        serverOptions.ListenUnixSocket(unixSocket);
 22408                        // NOTE: control access via directory perms/umask; UDS file perms are inherited from process uma
 22409                        // Prefer placing the socket under a group-owned dir (e.g., /var/run/kestrun) with 0770.
 22410                    }
 22411                }
 22412
 22413                // Named pipe listeners
 44414                foreach (var namedPipeName in Options.NamedPipeNames)
 22415                {
 0416                    if (!string.IsNullOrWhiteSpace(namedPipeName))
 22417                    {
 0418                        HostLogger.Verbose("Binding Named Pipe: {Pipe}", namedPipeName);
 0419                        serverOptions.ListenNamedPipe(namedPipeName);
 22420                    }
 22421                }
 22422
 22423                // TCP listeners
 60424                foreach (var opt in Options.Listeners)
 22425                {
 8426                    serverOptions.Listen(opt.IPAddress, opt.Port, listenOptions =>
 8427                    {
 8428                        listenOptions.Protocols = opt.Protocols;
 8429                        listenOptions.DisableAltSvcHeader = opt.DisableAltSvcHeader;
 8430                        if (opt.UseHttps && opt.X509Certificate is not null)
 8431                        {
 0432                            _ = listenOptions.UseHttps(opt.X509Certificate);
 8433                        }
 8434                        if (opt.UseConnectionLogging)
 8435                        {
 0436                            _ = listenOptions.UseConnectionLogging();
 8437                        }
 16438                    });
 22439                }
 44440            });
 441
 442            // build the app to validate configuration
 22443            _app = Build();
 22444            var dataSource = _app.Services.GetRequiredService<EndpointDataSource>();
 445
 22446            if (dataSource.Endpoints.Count == 0)
 447            {
 22448                HostLogger.Warning("EndpointDataSource is empty. No endpoints configured.");
 449            }
 450            else
 451            {
 0452                foreach (var ep in dataSource.Endpoints)
 453                {
 0454                    HostLogger.Information("➡️  Endpoint: {DisplayName}", ep.DisplayName);
 455                }
 456            }
 457
 22458            IsConfigured = true;
 22459            HostLogger.Information("Configuration applied successfully.");
 22460        }
 0461        catch (Exception ex)
 462        {
 0463            HostLogger.Error(ex, "Error applying configuration: {Message}", ex.Message);
 0464            throw new InvalidOperationException("Failed to apply configuration.", ex);
 465        }
 22466    }
 467
 468    #endregion
 469    #region Builder
 470    /* More information about the KestrunHost class
 471    https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication?view=aspnetcore-8.0
 472
 473    */
 474
 475    /// <summary>
 476    /// Builds the WebApplication.
 477    /// This method applies all queued services and middleware stages,
 478    /// and returns the built WebApplication instance.
 479    /// </summary>
 480    /// <returns>The built WebApplication.</returns>
 481    /// <exception cref="InvalidOperationException"></exception>
 482    public WebApplication Build()
 483    {
 45484        if (Builder == null)
 485        {
 0486            throw new InvalidOperationException("Call CreateBuilder() first.");
 487        }
 488
 489        // 1️⃣  Apply all queued services
 170490        foreach (var configure in _serviceQueue)
 491        {
 40492            configure(Builder.Services);
 493        }
 494
 495        // 2️⃣  Build the WebApplication
 45496        _app = Builder.Build();
 497
 45498        HostLogger.Information("CWD: {CWD}", Directory.GetCurrentDirectory());
 45499        HostLogger.Information("ContentRoot: {Root}", _app.Environment.ContentRootPath);
 45500        var pagesDir = Path.Combine(_app.Environment.ContentRootPath, "Pages");
 45501        HostLogger.Information("Pages Dir: {PagesDir}", pagesDir);
 45502        if (Directory.Exists(pagesDir))
 503        {
 0504            foreach (var file in Directory.GetFiles(pagesDir, "*.*", SearchOption.AllDirectories))
 505            {
 0506                HostLogger.Information("Pages file: {File}", file);
 507            }
 508        }
 509        else
 510        {
 45511            HostLogger.Warning("Pages directory does not exist: {PagesDir}", pagesDir);
 512        }
 513
 514        // 3️⃣  Apply all queued middleware stages
 160515        foreach (var stage in _middlewareQueue)
 516        {
 35517            stage(_app);
 518        }
 519
 94520        foreach (var feature in FeatureQueue)
 521        {
 2522            feature(this);
 523        }
 524        // 5️⃣  Terminal endpoint execution
 45525        return _app;
 526    }
 527
 528    /// <summary>
 529    /// Adds a service configuration action to the service queue.
 530    /// This action will be executed when the services are built.
 531    /// </summary>
 532    /// <param name="configure">The service configuration action.</param>
 533    /// <returns>The current KestrunHost instance.</returns>
 534    public KestrunHost AddService(Action<IServiceCollection> configure)
 535    {
 58536        _serviceQueue.Add(configure);
 58537        return this;
 538    }
 539
 540    /// <summary>
 541    /// Adds a middleware stage to the application pipeline.
 542    /// </summary>
 543    /// <param name="stage">The middleware stage to add.</param>
 544    /// <returns>The current KestrunHost instance.</returns>
 545    public KestrunHost Use(Action<IApplicationBuilder> stage)
 546    {
 69547        _middlewareQueue.Add(stage);
 69548        return this;
 549    }
 550
 551    /// <summary>
 552    /// Adds a feature configuration action to the feature queue.
 553    /// This action will be executed when the features are applied.
 554    /// </summary>
 555    /// <param name="feature">The feature configuration action.</param>
 556    /// <returns>The current KestrunHost instance.</returns>
 557    public KestrunHost AddFeature(Action<KestrunHost> feature)
 558    {
 2559        FeatureQueue.Add(feature);
 2560        return this;
 561    }
 562
 563    /// <summary>
 564    /// Adds a scheduling feature to the Kestrun host, optionally specifying the maximum number of runspaces for the sch
 565    /// </summary>
 566    /// <param name="MaxRunspaces">The maximum number of runspaces for the scheduler. If null, uses the default value.</
 567    /// <returns>The current KestrunHost instance.</returns>
 568    public KestrunHost AddScheduling(int? MaxRunspaces = null)
 569    {
 4570        return MaxRunspaces is not null and <= 0
 4571            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 4572            : AddFeature(host =>
 4573        {
 2574            if (HostLogger.IsEnabled(LogEventLevel.Debug))
 4575            {
 2576                HostLogger.Debug("AddScheduling (deferred)");
 4577            }
 4578
 2579            if (host.Scheduler is null)
 4580            {
 1581                if (MaxRunspaces is not null and > 0)
 4582                {
 1583                    HostLogger.Information("Setting MaxSchedulerRunspaces to {MaxRunspaces}", MaxRunspaces);
 1584                    host.Options.MaxSchedulerRunspaces = MaxRunspaces.Value;
 4585                }
 1586                HostLogger.Verbose("Creating SchedulerService with MaxSchedulerRunspaces={MaxRunspaces}",
 1587                    host.Options.MaxSchedulerRunspaces);
 1588                var pool = host.CreateRunspacePool(host.Options.MaxSchedulerRunspaces);
 1589                var logger = HostLogger.ForContext<KestrunHost>();
 1590                host.Scheduler = new SchedulerService(pool, logger);
 4591            }
 4592            else
 4593            {
 1594                HostLogger.Warning("SchedulerService already configured; skipping.");
 4595            }
 5596        });
 597    }
 598
 599    /// <summary>
 600    /// Adds MVC / API controllers to the application.
 601    /// </summary>
 602    /// <param name="cfg">The configuration options for MVC / API controllers.</param>
 603    /// <returns>The current KestrunHost instance.</returns>
 604    public KestrunHost AddControllers(Action<Microsoft.AspNetCore.Mvc.MvcOptions>? cfg = null)
 605    {
 0606        return AddService(services =>
 0607        {
 0608            var builder = services.AddControllers();
 0609            if (cfg != null)
 0610            {
 0611                _ = builder.ConfigureApplicationPartManager(pm => { }); // customise if you wish
 0612            }
 0613        });
 614    }
 615
 616
 617
 618
 619    /// <summary>
 620    /// Adds a PowerShell runtime to the application.
 621    /// This middleware allows you to execute PowerShell scripts in response to HTTP requests.
 622    /// </summary>
 623    /// <param name="routePrefix">The route prefix to use for the PowerShell runtime.</param>
 624    /// <returns>The current KestrunHost instance.</returns>
 625    public KestrunHost AddPowerShellRuntime(PathString? routePrefix = null)
 626    {
 1627        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 628        {
 1629            HostLogger.Debug("Adding PowerShell runtime with route prefix: {RoutePrefix}", routePrefix);
 630        }
 631
 1632        return Use(app =>
 1633        {
 1634            ArgumentNullException.ThrowIfNull(_runspacePool);
 1635            // ── mount PowerShell at the root ──
 1636            _ = app.UseLanguageRuntime(
 1637                ScriptLanguage.PowerShell,
 2638                b => b.UsePowerShellRunspace(_runspacePool));
 2639        });
 640    }
 641
 642
 643
 644
 645
 646
 647
 648    // ② SignalR
 649    /// <summary>
 650    /// Adds a SignalR hub to the application at the specified path.
 651    /// </summary>
 652    /// <typeparam name="T">The type of the SignalR hub.</typeparam>
 653    /// <param name="path">The path at which to map the SignalR hub.</param>
 654    /// <returns>The current KestrunHost instance.</returns>
 655    public KestrunHost AddSignalR<T>(string path) where T : Hub
 656    {
 0657        return AddService(s => s.AddSignalR())
 0658               .Use(app => ((IEndpointRouteBuilder)app).MapHub<T>(path));
 659    }
 660
 661    /*
 662        // ④ gRPC
 663        public KestrunHost AddGrpc<TService>() where TService : class
 664        {
 665            return AddService(s => s.AddGrpc())
 666                   .Use(app => app.MapGrpcService<TService>());
 667        }
 668    */
 669
 670    /*   public KestrunHost AddSwagger()
 671       {
 672           AddService(s =>
 673           {
 674               s.AddEndpointsApiExplorer();
 675               s.AddSwaggerGen();
 676           });
 677           //  ⚠️ Swagger’s middleware normally goes first in the pipeline
 678           return Use(app =>
 679           {
 680               app.UseSwagger();
 681               app.UseSwaggerUI();
 682           });
 683       }*/
 684
 685    // Add as many tiny helpers as you wish:
 686    // • AddAuthentication(jwt => { … })
 687    // • AddSignalR()
 688    // • AddHealthChecks()
 689    // • AddGrpc()
 690    // etc.
 691
 692    #endregion
 693    #region Run/Start/Stop
 694
 695    /// <summary>
 696    /// Runs the Kestrun web application, applying configuration and starting the server.
 697    /// </summary>
 698    public void Run()
 699    {
 0700        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 701        {
 0702            HostLogger.Debug("Run() called");
 703        }
 704
 0705        EnableConfiguration();
 706
 0707        _app?.Run();
 0708    }
 709
 710    /// <summary>
 711    /// Starts the Kestrun web application asynchronously.
 712    /// </summary>
 713    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 714    /// <returns>A task that represents the asynchronous start operation.</returns>
 715    public async Task StartAsync(CancellationToken cancellationToken = default)
 716    {
 8717        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 718        {
 1719            HostLogger.Debug("StartAsync() called");
 720        }
 721
 8722        EnableConfiguration();
 8723        if (_app != null)
 724        {
 8725            await _app.StartAsync(cancellationToken);
 726        }
 8727    }
 728
 729    /// <summary>
 730    /// Stops the Kestrun web application asynchronously.
 731    /// </summary>
 732    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 733    /// <returns>A task that represents the asynchronous stop operation.</returns>
 734    public async Task StopAsync(CancellationToken cancellationToken = default)
 735    {
 13736        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 737        {
 6738            HostLogger.Debug("StopAsync() called");
 739        }
 740
 13741        if (_app != null)
 742        {
 743            try
 744            {
 745                // Initiate graceful shutdown
 8746                await _app.StopAsync(cancellationToken);
 8747            }
 0748            catch (Exception ex) when (ex.GetType().FullName == "System.Net.Quic.QuicException")
 749            {
 750                // QUIC exceptions can occur during shutdown, especially if the server is not using QUIC.
 751                // We log this as a debug message to avoid cluttering the logs with expected exceptions.
 752                // This is a workaround for
 753
 0754                HostLogger.Debug("Ignored QUIC exception during shutdown: {Message}", ex.Message);
 0755            }
 756        }
 13757    }
 758
 759    /// <summary>
 760    /// Initiates a graceful shutdown of the Kestrun web application.
 761    /// </summary>
 762    public void Stop()
 763    {
 1764        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 765        {
 1766            HostLogger.Debug("Stop() called");
 767        }
 768        // This initiates a graceful shutdown.
 1769        _app?.Lifetime.StopApplication();
 1770    }
 771
 772    /// <summary>
 773    /// Determines whether the Kestrun web application is currently running.
 774    /// </summary>
 775    /// <returns>True if the application is running; otherwise, false.</returns>
 776    public bool IsRunning
 777    {
 778        get
 779        {
 8780            var appField = typeof(KestrunHost)
 8781                .GetField("_app", BindingFlags.NonPublic | BindingFlags.Instance);
 782
 8783            return appField?.GetValue(this) is WebApplication app && !app.Lifetime.ApplicationStopping.IsCancellationReq
 784        }
 785    }
 786
 787
 788    #endregion
 789
 790
 791
 792    #region Runspace Pool Management
 793
 794    /// <summary>
 795    /// Creates and returns a new <see cref="KestrunRunspacePoolManager"/> instance with the specified maximum number of
 796    /// </summary>
 797    /// <param name="maxRunspaces">The maximum number of runspaces to create. If not specified or zero, defaults to twic
 798    /// <param name="userVariables">A dictionary of user-defined variables to inject into the runspace pool.</param>
 799    /// <param name="userFunctions">A dictionary of user-defined functions to inject into the runspace pool.</param>
 800    /// <returns>A configured <see cref="KestrunRunspacePoolManager"/> instance.</returns>
 801    public KestrunRunspacePoolManager CreateRunspacePool(int? maxRunspaces = 0, Dictionary<string, object>? userVariable
 802    {
 23803        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 804        {
 16805            HostLogger.Debug("CreateRunspacePool() called: {@MaxRunspaces}", maxRunspaces);
 806        }
 807
 808        // Create a default InitialSessionState with an unrestricted policy:
 23809        var iss = InitialSessionState.CreateDefault();
 810
 23811        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 812        {
 0813            iss.ExecutionPolicy = ExecutionPolicy.Unrestricted;
 814        }
 815
 92816        foreach (var p in _modulePaths)
 817        {
 23818            iss.ImportPSModule([p]);
 819        }
 23820        iss.Variables.Add(
 23821            new SessionStateVariableEntry(
 23822                "KestrunHost",
 23823                this,
 23824                "The KestrunHost instance"
 23825            )
 23826        );
 827        // Inject global variables into all runspaces
 276828        foreach (var kvp in SharedStateStore.Snapshot())
 829        {
 830            // kvp.Key = "Visits", kvp.Value = 0
 115831            iss.Variables.Add(
 115832                new SessionStateVariableEntry(
 115833                    kvp.Key,
 115834                    kvp.Value,
 115835                    "Global variable"
 115836                )
 115837            );
 838        }
 839
 46840        foreach (var kvp in userVariables ?? [])
 841        {
 0842            if (kvp.Value is PSVariable psVar)
 843            {
 0844                iss.Variables.Add(
 0845                    new SessionStateVariableEntry(
 0846                        kvp.Key,
 0847                        psVar.Value,
 0848                        psVar.Description ?? "User-defined variable"
 0849                    )
 0850                );
 851            }
 852            else
 853            {
 0854                iss.Variables.Add(
 0855                    new SessionStateVariableEntry(
 0856                        kvp.Key,
 0857                        kvp.Value,
 0858                        "User-defined variable"
 0859                    )
 0860                );
 861            }
 862        }
 863
 46864        foreach (var r in userFunctions ?? [])
 865        {
 0866            var name = r.Key;
 0867            var def = r.Value;
 868
 869            // Use the string-based ctor available in 7.4 ref/net8.0
 0870            var entry = new SessionStateFunctionEntry(
 0871                name,
 0872                def,
 0873                ScopedItemOptions.ReadOnly,   // or ScopedItemOptions.None if you want them mutable
 0874                helpFile: null
 0875            );
 876
 0877            iss.Commands.Add(entry);
 878        }
 879
 880        // Determine max runspaces
 23881        var maxRs = (maxRunspaces.HasValue && maxRunspaces.Value > 0) ? maxRunspaces.Value : Environment.ProcessorCount 
 882
 23883        HostLogger.Information($"Creating runspace pool with max runspaces: {maxRs}");
 23884        var runspacePool = new KestrunRunspacePoolManager(Options?.MinRunspaces ?? 1, maxRunspaces: maxRs, initialSessio
 885        // Return the created runspace pool
 23886        return runspacePool;
 887    }
 888
 889
 890    #endregion
 891
 892
 893    #region Disposable
 894
 895    /// <summary>
 896    /// Releases all resources used by the <see cref="KestrunHost"/> instance.
 897    /// </summary>
 898    public void Dispose()
 899    {
 21900        if (HostLogger.IsEnabled(LogEventLevel.Debug))
 901        {
 21902            HostLogger.Debug("Dispose() called");
 903        }
 904
 21905        _runspacePool?.Dispose();
 21906        _runspacePool = null; // Clear the runspace pool reference
 21907        IsConfigured = false; // Reset configuration state
 21908        _app = null;
 21909        Scheduler?.Dispose();
 21910        (HostLogger as IDisposable)?.Dispose();
 21911        GC.SuppressFinalize(this);
 21912    }
 913    #endregion
 914
 915    #region Script Validation
 916
 917
 918    #endregion
 919}

Methods/Properties

get_Builder()
get_App()
get_ApplicationName()
get_Options()
.ctor(System.String,Serilog.ILogger,System.String,System.String[])
get_IsConfigured()
get_RunspacePool()
get_KestrunRoot()
get_HostLogger()
get_Scheduler()
get_RouteGroupStack()
get_FeatureQueue()
.ctor(System.String,System.String,System.String[])
LogConstructorArgs(System.String,System.Boolean,System.String,System.Int32)
SetWorkingDirectoryIfNeeded(System.String)
AddKestrunModulePathIfMissing(System.String[])
InitializeOptions(System.String)
AddUserModulePaths(System.String[])
ConfigureListener(System.Int32,System.Net.IPAddress,System.Security.Cryptography.X509Certificates.X509Certificate2,Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols,System.Boolean)
ConfigureListener(System.Int32,System.Net.IPAddress,System.Boolean)
ConfigureListener(System.Int32,System.Boolean)
EnableConfiguration(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.String>)
Build()
AddService(System.Action`1<Microsoft.Extensions.DependencyInjection.IServiceCollection>)
Use(System.Action`1<Microsoft.AspNetCore.Builder.IApplicationBuilder>)
AddFeature(System.Action`1<Kestrun.Hosting.KestrunHost>)
AddScheduling(System.Nullable`1<System.Int32>)
AddControllers(System.Action`1<Microsoft.AspNetCore.Mvc.MvcOptions>)
AddPowerShellRuntime(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
AddSignalR(System.String)
Run()
StartAsync()
StopAsync()
Stop()
get_IsRunning()
CreateRunspacePool(System.Nullable`1<System.Int32>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.String>)
Dispose()