Private.ps1
|
function Add-KrAspNetCoreType { [CmdletBinding()] [OutputType([bool])] param ( [Parameter()] [ValidateSet('net8.0', 'net9.0', 'net10.0')] [string]$Version = 'net8.0' ) $versionNumber = $Version -replace 'net(\d+).*', '$1' $dotnetPath = (Get-Command -Name 'dotnet' -ErrorAction Stop).Source $realDotnetPath = (Get-Item $dotnetPath).Target if (-not $realDotnetPath) { $realDotnetPath = $dotnetPath }elseif ($realDotnetPath -notmatch '^/') { # If the target is a relative path, resolve it from the parent of $dotnetPath (e.g. symlink in same folder) $realDotnetPath = Join-Path -Path (Split-Path -Path $dotnetPath -Parent) -ChildPath $realDotnetPath $realDotnetPath = [System.IO.Path]::GetFullPath($realDotnetPath) } $dotnetDir = Split-Path -Path $realDotnetPath -Parent if (-not $dotnetDir) { throw 'Could not determine the path to the dotnet executable.' } $baseDir = Join-Path -Path $dotnetDir -ChildPath 'shared\Microsoft.AspNetCore.App' if (-not (Test-Path -Path $baseDir -PathType Container)) { throw "ASP.NET Core shared framework not found at $baseDir." } $versionDirs = Get-ChildItem -Path $baseDir -Directory | Where-Object { $_.Name -like "$($versionNumber).*" } | Sort-Object Name -Descending foreach ($verDir in $versionDirs) { $assemblies = @() Get-ChildItem -Path $verDir.FullName -Filter 'Microsoft.*.dll' | ForEach-Object { if ($assemblies -notcontains $_.Name) { $assemblies += $_.Name } } $allFound = $true foreach ($asm in $assemblies) { $asmPath = Join-Path -Path $verDir.FullName -ChildPath $asm if (-not (Test-Path -Path $asmPath)) { Write-Verbose "Assembly $asm not found in $($verDir.FullName)" $allFound = $false break } } if ($allFound) { $result = $true foreach ($asm in $assemblies) { $asmPath = Join-Path -Path $verDir.FullName -ChildPath $asm $result = $result -and (Assert-KrAssemblyLoaded -AssemblyPath $asmPath) } Write-Verbose "Loaded ASP.NET Core assemblies from $($verDir.FullName)" return $result } return $false } Write-Error "Could not find ASP.NET Core assemblies for version $Version in $baseDir." Write-Warning "Please download the Runtime $Version from https://dotnet.microsoft.com/en-us/download/dotnet/$versionNumber.0" throw "Microsoft.AspNetCore.App version $Version not found in $baseDir." } function Add-KrCodeAnalysisType { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string]$ModuleRootPath, [Parameter(Mandatory = $true)] [string]$Version ) $passVerbose = $PSCmdlet.MyInvocation.BoundParameters['Verbose'] $codeAnalysisassemblyLoadPath = Join-Path -Path $ModuleRootPath -ChildPath 'lib' -AdditionalChildPath 'Microsoft.CodeAnalysis', $Version return( (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.dll') -Verbose:$passVerbose) -and (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.Workspaces.dll') -Verbose:$passVerbose) -and (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.CSharp.dll') -Verbose:$passVerbose) -and (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.CSharp.Scripting.dll') -Verbose:$passVerbose) -and #Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath "Microsoft.CodeAnalysis.Razor.dll") -Verbose:$passVerbose (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.VisualBasic.dll') -Verbose:$passVerbose) -and (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll') -Verbose:$passVerbose) -and (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.CSharp.Workspaces.dll') -Verbose:$passVerbose) -and (Assert-KrAssemblyLoaded -AssemblyPath (Join-Path -Path "$codeAnalysisassemblyLoadPath" -ChildPath 'Microsoft.CodeAnalysis.Scripting.dll') -Verbose:$passVerbose) ) } function Assert-KrAssemblyLoaded { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string]$AssemblyPath ) if (-not (Test-Path -Path $AssemblyPath -PathType Leaf)) { throw "Assembly not found at path: $AssemblyPath" } $assemblyName = [System.Reflection.AssemblyName]::GetAssemblyName($AssemblyPath).Name $loaded = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $assemblyName } if (-not $loaded) { if ($Verbose) { Write-Verbose "Loading assembly: $AssemblyPath" } try { Add-Type -LiteralPath $AssemblyPath } catch { Write-Error "Failed to load assembly: $AssemblyPath" Write-Error $_ return $false } } else { if ($Verbose) { Write-Verbose "Assembly already loaded: $AssemblyPath" } } return $true } function Get-KrCommandsByContext { [CmdletBinding(DefaultParameterSetName = 'AnyOf')] param( [Parameter(Mandatory, ParameterSetName = 'AnyOf')] [ValidateSet('Definition', 'Route', 'Schedule', 'ScheduleAndDefinition', 'Runtime', 'Everywhere')] [string[]]$AnyOf, [Parameter(Mandatory, ParameterSetName = 'AllOf')] [ValidateSet('Definition', 'Route', 'Schedule')] [string[]]$AllOf, [ValidateSet('Definition', 'Route', 'Schedule', 'ScheduleAndDefinition', 'Runtime', 'Everywhere')] [string[]]$Not, [string]$Module = 'Kestrun', [switch]$IncludeNonExported, [switch]$Exact, [object[]]$Functions ) function _KrNameToMask { param([Parameter(Mandatory)][string]$Name) switch ($Name) { 'Definition' { 1 } 'Route' { 2 } 'Schedule' { 4 } 'ScheduleAndDefinition' { 5 } 'Runtime' { 6 } 'Everywhere' { 7 } default { throw "Unknown context '$Name'." } } } $cmds = if ($Functions) { $Functions } else { if ($IncludeNonExported) { Get-Command -Module $Module -All } else { Get-Command -Module $Module } } $target = 0 if ($PSCmdlet.ParameterSetName -eq 'AnyOf') { foreach ($n in $AnyOf) { $target = $target -bor (_KrNameToMask $n) } } else { foreach ($n in $AllOf) { $target = $target -bor (_KrNameToMask $n) } } $notMask = 0 foreach ($n in ($Not | ForEach-Object { $_ })) { $notMask = $notMask -bor (_KrNameToMask $n) } $match = if ($PSCmdlet.ParameterSetName -eq 'AnyOf') { if ($Exact) { { param($m) $m -eq $target } } else { { param($m) ($m -band $target) -ne 0 } } } else { if ($Exact) { { param($m) $m -eq $target } } else { { param($m) ($m -band $target) -eq $target } } } foreach ($c in $cmds) { $m = 0 if ($c.CommandType -eq 'Function') { $m = Get-KrFunctionContextMask -Function $c } elseif ($c.CommandType -eq 'Cmdlet' -and $c.ImplementingType) { $a = $c.ImplementingType.GetCustomAttributes($true) | Where-Object { $_.GetType().Name -eq 'KestrunRuntimeApiAttribute' } | Select-Object -First 1 if ($a) { $m = [int]([KestrunApiContext]$a.Contexts) } } if ($m -eq 0) { continue } if ($notMask -ne 0 -and ($m -band $notMask) -ne 0) { continue } # exclude forbidden bits if (& $match $m) { $c } } } function Get-KrDocSet { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet( 'DefinitionOnly', 'RouteOnly', 'ScheduleOnly', 'RouteAndSchedule', 'ScheduleAndDefinition', 'Runtime', # Route|Schedule 'Everywhere', 'ConfigOnly' # Definition but not Route/Schedule )] [string]$Name, [string]$Module = 'Kestrun', [switch]$IncludeNonExported, [object[]]$Functions ) switch ($Name) { 'DefinitionOnly' { Get-KrCommandsByContext -AnyOf Definition -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'RouteOnly' { Get-KrCommandsByContext -AnyOf Route -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'ScheduleOnly' { Get-KrCommandsByContext -AnyOf Schedule -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'RouteAndSchedule' { Get-KrCommandsByContext -AllOf Route, Schedule -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'ScheduleAndDefinition' { Get-KrCommandsByContext -AnyOf ScheduleAndDefinition -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'Runtime' { Get-KrCommandsByContext -AnyOf Runtime -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'Everywhere' { Get-KrCommandsByContext -AnyOf Everywhere -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } 'ConfigOnly' { # Definition but NOT Route or Schedule Get-KrCommandsByContext -AnyOf Definition -Exact -Module $Module -IncludeNonExported:$IncludeNonExported -Functions $Functions } } } function Get-KrFunctionContextMask { param([System.Management.Automation.FunctionInfo]$Function) if (-not $Function.ScriptBlock) { return 0 } $fnAst = $Function.ScriptBlock.Ast. Find({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq $Function.Name }, $true) if (-not $fnAst) { return 0 } $attrs = @() if ($fnAst.Attributes) { $attrs += $fnAst.Attributes } if ($fnAst.Body -and $fnAst.Body.ParamBlock -and $fnAst.Body.ParamBlock.Attributes) { $attrs += $fnAst.Body.ParamBlock.Attributes } $kr = $attrs | Where-Object { $_.TypeName.Name -eq 'KestrunRuntimeApi' } | Select-Object -First 1 if (-not $kr) { return 0 } $txt = (($kr.PositionalArguments + $kr.NamedArguments.Expression) | Where-Object { $_ }).Extent.Text #| ForEach-Object { $_.Extent.Text } -join ' ' $mask = switch ($txt) { "'Everywhere'" { 7 } "'Runtime'" { 6 } "'ScheduleAndDefinition'" { 5 } "'Definition'" { 1 } "'Route'" { 2 } "'Schedule'" { 4 } } return $mask } function ConvertTo-DateTimeOffset { [CmdletBinding()] [OutputType('System.DateTimeOffset')] param( [Parameter(Mandatory)] [object]$InputObject ) # Already DateTimeOffset if ($InputObject -is [DateTimeOffset]) { return $InputObject } # DateTime -> DateTimeOffset (local) if ($InputObject -is [DateTime]) { return [DateTimeOffset]::new([DateTime]$InputObject) } # String → try absolute date/time first if ($InputObject -is [string]) { $s = $InputObject.Trim() $dto = $null if ([DateTimeOffset]::TryParse($s, [ref]$dto)) { return $dto } # If not a date, try treating the string as a duration and add to now try { $ts = ConvertTo-TimeSpan -InputObject $s return [DateTimeOffset]::Now.Add($ts) } catch { throw "Invalid Expires value '$s'. Provide an absolute date (e.g. '2025-09-10T23:00Z') or a duration (e.g. '2h', '1d')." } } # TimeSpan => relative expiry from now if ($InputObject -is [TimeSpan]) { return [DateTimeOffset]::Now.Add([TimeSpan]$InputObject) } # Numeric seconds => relative expiry if ($InputObject -is [int] -or $InputObject -is [long] -or $InputObject -is [double] -or $InputObject -is [decimal]) { return [DateTimeOffset]::Now.AddSeconds([double]$InputObject) } throw "Cannot convert value of type [$($InputObject.GetType().FullName)] to [DateTimeOffset]." } function ConvertTo-KrThreadSafeValue { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Value ) if ($null -eq $Value) { return $null } # --- Hashtable (@{}) --- if ($Value -is [hashtable]) { return [hashtable]::Synchronized($Value) } # --- OrderedDictionary ([ordered]@{}) --- if ($Value -is [System.Collections.Specialized.OrderedDictionary]) { # Copy into a normal hashtable and wrap $ht = @{} foreach ($entry in $Value.GetEnumerator()) { $ht[$entry.Key] = $entry.Value } return [hashtable]::Synchronized($ht) } # --- ArrayList --- if ($Value -is [System.Collections.ArrayList]) { return [System.Collections.ArrayList]::Synchronized($Value) } # --- Any other IDictionary (generic or not, but not handled above) --- if ($Value -is [System.Collections.IDictionary]) { $dict = [System.Collections.Concurrent.ConcurrentDictionary[object, object]]::new() foreach ($entry in $Value.GetEnumerator()) { $null = $dict.TryAdd($entry.Key, $entry.Value) } return $dict } # --- Arrays: treat as immutable snapshots --- if ($Value -is [Array]) { return $Value } # --- PSCustomObject, scalars, etc.: just return as-is --- return $Value } function ConvertTo-TimeSpan { [CmdletBinding()] [OutputType('System.TimeSpan')] param( [Parameter(Mandatory)] [object]$InputObject ) # 1) Already a TimeSpan if ($InputObject -is [TimeSpan]) { return $InputObject } # 2) Numeric => seconds if ($InputObject -is [int] -or $InputObject -is [long] -or $InputObject -is [double] -or $InputObject -is [decimal]) { return [TimeSpan]::FromSeconds([double]$InputObject) } # 3) String parsing if ($InputObject -is [string]) { $s = $InputObject.Trim() # Try .NET built-in formats first: "c", "g", "G" (e.g., "00:30:00", "1.02:03:04") $ts = [TimeSpan]::Zero if ([TimeSpan]::TryParse($s, [ref]$ts)) { return $ts } # Compact tokens: 1d2h30m15s250ms (order-insensitive, any subset) if ($s -match '^(?i)(?:\s*(?<d>\d+)\s*d)?(?:\s*(?<h>\d+)\s*h)?(?:\s*(?<m>\d+)\s*m)?(?:\s*(?<s>\d+)\s*s)?(?:\s*(?<ms>\d+)\s*ms)?\s*$') { $days = [int]::Parse(('0' + $Matches['d'])) $hrs = [int]::Parse(('0' + $Matches['h'])) $min = [int]::Parse(('0' + $Matches['m'])) $sec = [int]::Parse(('0' + $Matches['s'])) $msec = [int]::Parse(('0' + $Matches['ms'])) return [TimeSpan]::new($days, $hrs, $min, $sec, $msec) } throw "Invalid TimeSpan format: '$s'. Try '00:30:00', '1.02:03:04', or tokens like '1d2h30m15s'." } throw "Cannot convert value of type [$($InputObject.GetType().FullName)] to [TimeSpan]." } function Get-KrFormattedMessage { param( [Parameter(Mandatory = $true)] [Serilog.ILogger]$Logger, [Parameter(Mandatory = $true)] [Serilog.Events.LogEventLevel]$Level, [parameter(Mandatory = $true)] [AllowEmptyString()] [string]$Message, [Parameter(Mandatory = $false)] [AllowNull()] [object[]]$Values, [Parameter(Mandatory = $false)] [AllowNull()] [System.Exception]$Exception ) $parsedTemplate = $null $boundProperties = $null if ($Logger.BindMessage($Message, $Values, [ref]$parsedTemplate, [ref]$boundProperties)) { $logEvent = [Serilog.Events.LogEvent]::new([System.DateTimeOffset]::Now, $Level, $Exception, $parsedTemplate, $boundProperties) $strWriter = [System.IO.StringWriter]::new() # Use the global TextFormatter if available, otherwise use the default formatter from Kestrun.Logging [Kestrun.Logging]::TextFormatter.Format($logEvent, $strWriter) $message = $strWriter.ToString() $strWriter.Dispose() $message } } function Set-KrLogLevelToPreference { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [Serilog.Events.LogEventLevel]$Level ) if ($PSCmdlet.ShouldProcess('Set log level preferences')) { if ([int]$Level -le [int]([Serilog.Events.LogEventLevel]::Verbose)) { Set-Variable VerbosePreference -Value 'Continue' -Scope Global } else { Set-Variable VerbosePreference -Value 'SilentlyContinue' -Scope Global } if ([int]$Level -le [int]([Serilog.Events.LogEventLevel]::Debug)) { Set-Variable DebugPreference -Value 'Continue' -Scope Global } else { Set-Variable DebugPreference -Value 'SilentlyContinue' -Scope Global } if ([int]$Level -le [int]([Serilog.Events.LogEventLevel]::Information)) { Set-Variable InformationPreference -Value 'Continue' -Scope Global } else { Set-Variable InformationPreference -Value 'SilentlyContinue' -Scope Global } if ([int]$Level -le [int]([Serilog.Events.LogEventLevel]::Warning)) { Set-Variable WarningPreference -Value 'Continue' -Scope Global } else { Set-Variable WarningPreference -Value 'SilentlyContinue' -Scope Global } } } function Write-KrOutsideRouteWarning { param ( [string]$FunctionName = $PSCmdlet.MyInvocation.InvocationName ) if (Test-KrLogger) { Write-KrLog -Level Warning -Message '{function} must be called inside a route script block where $Context is available.' -Values $FunctionName } else { Write-Warning -Message "$FunctionName must be called inside a route script block where `$Context is available." } } function Write-KrSinkPowerShell { param( [Parameter(Mandatory = $true)] [Serilog.Events.LogEvent]$LogEvent, [Parameter(Mandatory = $true)] [string]$RenderedMessage ) switch ($LogEvent.Level) { Verbose { Write-Verbose -Message $RenderedMessage } Debug { Write-Debug -Message $RenderedMessage } Information { Write-Information -MessageData $RenderedMessage } Warning { Write-Warning -Message $RenderedMessage } default { Write-Information -MessageData $RenderedMessage -InformationAction 'Continue' } } } function Get-KrAnnotatedFunctionsLoaded { [CmdletBinding()] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [Kestrun.Hosting.KestrunHost]$Server, [Parameter()] [string]$DocId = [Kestrun.Authentication.IOpenApiAuthenticationOptions]::DefaultSchemeName ) begin { # Ensure the server instance is resolved $Server = Resolve-KestrunServer -Server $Server # All loaded functions now in the runspace $funcs = @(Get-Command -CommandType Function | Where-Object { $null -eq $_.Module -and $null -eq $_.PsDrive }) } process { if ( -not $Server.OpenApiDocumentDescriptor.ContainsKey($DocId)) { throw "OpenAPI document with ID '$DocId' does not exist on the server." } $doc = $Server.OpenApiDocumentDescriptor[$DocId] $doc.LoadAnnotatedFunctions( $funcs ) } } function _KrJoin-Route { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] param([string]$base, [string]$child) $b = ($base ?? '').TrimEnd('/') $c = ($child ?? '').TrimStart('/') if ([string]::IsNullOrWhiteSpace($b)) { "/$c".TrimEnd('/') -replace '^$', '/' } elseif ([string]::IsNullOrWhiteSpace($c)) { if ($b) { $b } else { '/' } } else { "$b/$c" } } function _KrMerge-Args { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param([hashtable]$a, [hashtable]$b) if (-not $a) { return $b } if (-not $b) { return $a } $m = @{} foreach ($k in $a.Keys) { $m[$k] = $a[$k] } foreach ($k in $b.Keys) { $m[$k] = $b[$k] } # child overrides $m } function _KrMerge-MRO { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] param( [Parameter(Mandatory)][Kestrun.Hosting.Options.MapRouteOptions]$Parent, [Parameter(Mandatory)][Kestrun.Hosting.Options.MapRouteOptions]$Child ) $pattern = if ($Child.Pattern) { if ($Parent.Pattern) { "$($Parent.Pattern)/$($Child.Pattern)" } else { $Child.Pattern } } else { $Parent.Pattern } $extraRefs = if ($null -ne $Child.ScriptCode.ExtraRefs) { if ($Parent.ScriptCode.ExtraRefs) { $Parent.ScriptCode.ExtraRefs + $Child.ScriptCode.ExtraRefs } else { $Child.ScriptCode.ExtraRefs } } else { $Parent.ScriptCode.ExtraRefs } $merged = @{ Pattern = $pattern.Replace('//', '/') HttpVerbs = if ($null -ne $Child.HttpVerbs -and ($Child.HttpVerbs.Count -gt 0)) { $Child.HttpVerbs } else { $Parent.HttpVerbs } RequireSchemes = _KrMerge-Unique $Parent.RequireSchemes $Child.RequireSchemes RequirePolicies = _KrMerge-Unique $Parent.RequirePolicies $Child.RequirePolicies CorsPolicy = if ($Child.CorsPolicy) { $Child.CorsPolicy } else { $Parent.CorsPolicy } OpenAPI = if ($Child.OpenAPI) { $Child.OpenAPI } else { $Parent.OpenAPI } ThrowOnDuplicate = $Child.ThrowOnDuplicate -or $Parent.ThrowOnDuplicate ScriptCode = @{ Code = if ($Child.ScriptCode.Code) { $Child.ScriptCode.Code } else { $Parent.ScriptCode.Code } Language = if ($null -ne $Child.ScriptCode.Language) { $Child.ScriptCode.Language } else { $Parent.ScriptCode.Language } ExtraImports = _KrMerge-Unique $Parent.ScriptCode.ExtraImports $Child.ScriptCode.ExtraImports ExtraRefs = $extraRefs Arguments = _KrMerge-Args $Parent.ScriptCode.Arguments $Child.ScriptCode.Arguments } } return New-KrMapRouteOption -Property $merged } function _KrMerge-Unique { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] param([string[]]$a, [string[]]$b) @(($a + $b | Where-Object { $_ -ne $null } | Select-Object -Unique)) } function _KrWith-MRO { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] param( [Parameter(Mandatory)][Kestrun.Hosting.Options.MapRouteOptions]$Base, [Parameter()][hashtable]$Override = @{} ) $h = @{ Pattern = $Base.Pattern HttpVerbs = $Base.HttpVerbs Code = $Base.Code Language = $Base.Language ExtraImports = $Base.ExtraImports ExtraRefs = $Base.ExtraRefs RequireSchemes = $Base.RequireSchemes RequirePolicies = $Base.RequirePolicies CorsPolicy = $Base.CorsPolicy Arguments = $Base.Arguments OpenAPI = $Base.OpenAPI ThrowOnDuplicate = $Base.ThrowOnDuplicate } foreach ($k in $Override.Keys) { $h[$k] = $Override[$k] } return New-KrMapRouteOption -Property $h } function Get-KrUserImportedModule { [CmdletBinding()] [OutputType([System.Management.Automation.PSModuleInfo])] param() # ----- constants ---------------------------------------------------------- $inboxRoot = [IO.Path]::GetFullPath( (Join-Path $PSHOME 'Modules') ) # regex fragment that matches “…\.vscode\extensions\ms-vscode.powershell…” $vsCodeRegex = [Regex]::Escape( [IO.Path]::Combine('.vscode', 'extensions', 'ms-vscode.powershell') ) -replace '\\\\', '[\\/]' # make path-separator agnostic # ------------------------------------------------------------------------- Get-Module | Where-Object { $path = [IO.Path]::GetFullPath($_.ModuleBase) $isInbox = $path.StartsWith($inboxRoot, $IsWindows ? 'OrdinalIgnoreCase' : 'Ordinal') $isVSCode = $path -match $vsCodeRegex $isMSPSSpace = $_.Name -like 'Microsoft.PowerShell.*' -not ($isInbox -or $isVSCode -or $isMSPSSpace) } } function Resolve-KestrunServer { param ( [Kestrun.Hosting.KestrunHost]$Server ) if ($null -eq $Server) { if ($null -ne $KrServer) { Write-KrLog -Level Verbose -Message "No server specified, using global `$KrServer variable." # If no server is specified, use the global $KrServer variable # This is useful for scripts that run in the context of a Kestrun server $Server = $KrServer } else { # Try to get the default Kestrun server instance $Server = [Kestrun.KestrunHostManager]::Default } if ($null -eq $Server) { throw 'No Kestrun server instance found. Please create a Kestrun server instance.' } } return $Server } function _NormalizeValueToDictionary([object]$Value, [int]$Depth, [int]$MaxRecursionDepth = 8) { if ($null -eq $Value) { return $null } if ($Depth -gt $MaxRecursionDepth) { return ($Value.ToString()) } # Unwrap PSObject shell if ($Value -is [System.Management.Automation.PSObject]) { $base = $Value.BaseObject if ($null -eq $base -or $base -eq $Value) { return $Value.ToString() } return _NormalizeValueToDictionary $base ($Depth + 1) } # Hashtable / IDictionary → new Dictionary[string, object] if ($Value -is [System.Collections.IDictionary]) { $out = [System.Collections.Generic.Dictionary[string, object]]::new() foreach ($key in $Value.Keys) { if ([string]::IsNullOrWhiteSpace([string]$key)) { continue } $nv = _NormalizeValue $Value[$key] ($Depth + 1) if ($null -ne $nv) { $out[[string]$key] = $nv } } return $out } # Enumerable (but not string) → List<object> if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) { $list = New-Object System.Collections.Generic.List[object] foreach ($item in $Value) { $list.Add((_NormalizeValueToDictionary $item ($depth + 1))) } return $list } return $Value # primitive / POCO } function Get-KrAssignedVariable { [CmdletBinding(DefaultParameterSetName = 'Given')] param( [Parameter(ParameterSetName = 'Given', Position = 0)] [scriptblock]$ScriptBlock, # NEW: use the caller's scriptblock (parent frame) [Parameter(ParameterSetName = 'FromParent', Mandatory)] [switch]$FromParent, # How many frames up to climb (1 = immediate caller) [Parameter(ParameterSetName = 'FromParent')] [int]$Up = 1, # Optional: skip frames from these modules when searching [Parameter(ParameterSetName = 'FromParent')] [string[]]$ExcludeModules = @('Kestrun'), [switch]$IncludeSetVariable, [switch]$ResolveValues, [ValidateSet('Local', 'Script', 'Global')] [string]$DefaultScope = 'Script' ) # ---------- resolve $ScriptBlock source ---------- if ($FromParent.IsPresent) { $allFrames = Get-PSCallStack # 0 = this function, 1 = immediate caller, 2+ = higher parents $frames = $allFrames | Select-Object -Skip 1 if ($ExcludeModules.Count) { $frames = $frames | Where-Object { $mn = $_.InvocationInfo.MyCommand.ModuleName -not ($mn -and ($mn -in $ExcludeModules)) } } # pick the desired parent frame $frame = $frames | Select-Object -Skip ($Up - 1) -First 1 if (-not $frame) { throw "No parent frame found (Up=$Up)." } # Figure out how far “up” that is compared to the original call stack $scopeUp = ($allFrames.IndexOf($frame)) + 1 if ($scopeUp -lt 1) { throw "Parent frame not found." } # prefer its live ScriptBlock; if null, rebuild from file $ScriptBlock = $frame.InvocationInfo.MyCommand.ScriptBlock if (-not $ScriptBlock -and $frame.ScriptName) { $ScriptBlock = [scriptblock]::Create((Get-Content -Raw -LiteralPath $frame.ScriptName)) } if (-not $ScriptBlock) { throw "Parent frame has no scriptblock or script file to parse." } } if (-not $ScriptBlock) { throw "No scriptblock provided. Use -FromParent or pass a ScriptBlock." } $ast = ($ScriptBlock.Ast).ToString() $endstring = $ast.IndexOf("Enable-KrConfiguration", [StringComparison]::OrdinalIgnoreCase) if ($endstring -lt 0) { throw "The provided scriptblock does not appear to contain 'Enable-KrConfiguration' call." } $ast = $ast.Substring(0, $endstring).Trim() if ($ast.StartsWith('{')) { $ast += "`n}" } $ScriptBlock = [scriptblock]::Create($ast) function _IsInFunction([System.Management.Automation.Language.Ast] $node) { $p = $node.Parent while ($p) { if ($p -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $true } $p = $p.Parent } return $false } $assignAsts = $ScriptBlock.Ast.FindAll( { param($n) $n -is [System.Management.Automation.Language.AssignmentStatementAst] }, $true) foreach ($a in $assignAsts) { $varAst = $a.Left.Find( { param($n) $n -is [System.Management.Automation.Language.VariableExpressionAst] }, $true ) | Select-Object -First 1 if (-not $varAst) { continue } $vp = $varAst.VariablePath $name = $vp.UnqualifiedPath if (-not $name) { $name = $vp.UserPath } # ← fallback if (-not $name) { continue } $name = $name -replace '^[^:]*:', '' if ($name.Contains('.') -or $name.Contains('[')) { continue } if ($name.StartsWith('{') -and $name.EndsWith('}')) { $name = $name.Substring(1, $name.Length - 2) # ← ${foo} → foo } $val = Get-Variable -Name $name -Scope $scopeUp -ValueOnly -ErrorAction SilentlyContinue $type = if ($null -ne $val) { $val.GetType().FullName } else { $null } [pscustomobject]@{ Name = $name ScopeHint = $scope ProviderPath = $provider Source = 'Assignment' Operator = $a.Operator.ToString() Type = $type Value = $val } } if ($IncludeSetVariable) { $cmdAsts = $ScriptBlock.Ast.FindAll( { param($n) $n -is [System.Management.Automation.Language.CommandAst] -and -not (_IsInFunction $n) }, $true) foreach ($c in $cmdAsts) { $cmd = $c.GetCommandName() if ($cmd -notin 'Set-Variable', 'New-Variable') { continue } $named = @{} foreach ($e in $c.CommandElements) { if ($e -is [System.Management.Automation.Language.CommandParameterAst] -and $e.ParameterName -in 'Name', 'Scope') { $arg = $e.Argument if ($arg -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $named[$e.ParameterName] = $arg.Value } } } if ($named.ContainsKey('Name')) { $name = $named['Name'] $scope = $named['Scope'] ?? $DefaultScope $provider = "variable:$($scope):$name" $val = $null; $type = $null if ($ResolveValues) { try { $val = (Get-Item -EA SilentlyContinue $provider).Value if ($null -ne $val) { $type = $val.GetType().FullName } } catch { Write-Warning "Failed to resolve variable '$name' in scope '$scope': $_" } } [pscustomobject]@{ Name = $name ScopeHint = $scope ProviderPath = $provider Source = $cmd Operator = $null Type = $type Value = $val } | ForEach-Object { [void]$rows.Add($_) } } } } # keep last occurrence per (ScopeHint, Name) $rows | Group-Object ScopeHint, Name | ForEach-Object { $_.Group[-1] } } function Convert-DateTimeOffsetToDateTime { param( [Parameter()] [AllowNull()] $InputObject ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [DateTimeOffset]) { # Preserve naive timestamps (those without explicit zone) as 'Unspecified' kind so no offset is appended $dto = [DateTimeOffset]$InputObject return [DateTime]::SpecifyKind($dto.DateTime, [System.DateTimeKind]::Unspecified) } if ($InputObject -is [System.Collections.IDictionary]) { foreach ($k in @($InputObject.Keys)) { $InputObject[$k] = Convert-DateTimeOffsetToDateTime $InputObject[$k] } return $InputObject } if ($InputObject -is [System.Collections.IList]) { for ($j = 0; $j -lt $InputObject.Count; $j++) { $InputObject[$j] = Convert-DateTimeOffsetToDateTime $InputObject[$j] } return $InputObject } return $InputObject } # Portions derived from PowerShell-Yaml (https://github.com/cloudbase/powershell-yaml) # Copyright (c) 2016–2024 Cloudbase Solutions Srl # Licensed under the Apache License, Version 2.0 (Apache-2.0). # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # Modifications Copyright (c) 2025 Kestrun Contributors function Convert-HashtableToDictionary { param( [Parameter(Mandatory = $true)] [hashtable]$Data ) # Preserve original insertion order: PowerShell hashtable preserves insertion order internally $ordered = [System.Collections.Specialized.OrderedDictionary]::new() foreach ($k in $Data.Keys) { $ordered.Add($k, (Convert-PSObjectToGenericObject $Data[$k])) } return $ordered } # Portions derived from PowerShell-Yaml (https://github.com/cloudbase/powershell-yaml) # Copyright (c) 2016–2024 Cloudbase Solutions Srl # Licensed under the Apache License, Version 2.0 (Apache-2.0). # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # Modifications Copyright (c) 2025 Kestrun Contributors function Convert-ListToGenericList { param( [Parameter(Mandatory = $false)] [array]$Data = @() ) $ret = [System.Collections.Generic.List[object]]::new() for ($i = 0; $i -lt $Data.Count; $i++) { $ret.Add((Convert-PSObjectToGenericObject $Data[$i])) } # Return the generic list directly (do NOT wrap in a single-element array) so single-element # sequences remain proper YAML sequences and do not collapse into mappings during round-trip. return $ret } # Portions derived from PowerShell-Yaml (https://github.com/cloudbase/powershell-yaml) # Copyright (c) 2016–2024 Cloudbase Solutions Srl # Licensed under the Apache License, Version 2.0 (Apache-2.0). # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # Modifications Copyright (c) 2025 Kestrun Contributors function Convert-OrderedHashtableToDictionary { param( [Parameter(Mandatory = $true)] [System.Collections.Specialized.OrderedDictionary] $Data ) foreach ($i in $($data.PSBase.Keys)) { $Data[$i] = Convert-PSObjectToGenericObject $Data[$i] } return $Data } # Portions derived from PowerShell-Yaml (https://github.com/cloudbase/powershell-yaml) # Copyright (c) 2016–2024 Cloudbase Solutions Srl # Licensed under the Apache License, Version 2.0 (Apache-2.0). # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # Modifications Copyright (c) 2025 Kestrun Contributors function Convert-PSObjectToGenericObject { param( [Parameter(Mandatory = $false)] [System.Object]$InputObject ) if ($null -eq $InputObject) { return $InputObject } # Use PowerShell's -is operator for efficient type checking if ($InputObject -is [System.Collections.Specialized.OrderedDictionary]) { return Convert-OrderedHashtableToDictionary $InputObject } elseif ($InputObject -is [System.Collections.IDictionary]) { return Convert-HashtableToDictionary $InputObject } elseif ($InputObject -is [System.Collections.IList]) { return Convert-ListToGenericList $InputObject } return $InputObject } |