private/session/Get-UiActionContext.ps1
|
# Cached reflection members to avoid repeated lookups (perf optimization for many buttons) $script:SessionStateFieldInfo = $null $script:SessionStatePropertyInfo = $null $script:GetVariableMethodInfo = $null <# .SYNOPSIS Captures execution context from a scriptblock for async execution. #> function Get-UiActionContext { [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$Action, [string[]]$LinkedVariables, [string[]]$LinkedFunctions, [string[]]$LinkedModules, [hashtable]$ExplicitVariables, # Caller's SessionState for scope lookup [System.Management.Automation.SessionState]$CallerSessionState ) if (!$CallerSessionState) { try { $flags = [System.Reflection.BindingFlags]'Instance, NonPublic, Public' $prop = [System.Management.Automation.ScriptBlock].GetProperty('SessionState', $flags) if ($prop) { $CallerSessionState = $prop.GetValue($Action) } } catch { # SessionState extraction failed } } $autoDetectedVars = [System.Collections.Generic.List[string]]::new() try { $ast = $Action.Ast $varExpressions = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.VariableExpressionAst] }, $true) # Built-in/automatic variables to exclude from capture $excludeVars = @( '_', 'args', 'ConsoleFileName', 'Error', 'Event', 'EventArgs', 'EventSubscriber', 'ExecutionContext', 'false', 'foreach', 'HOME', 'Host', 'input', 'IsCoreCLR', 'IsLinux', 'IsMacOS', 'IsWindows', 'LastExitCode', 'Matches', 'MyInvocation', 'NestedPromptLevel', 'null', 'PID', 'PROFILE', 'PSBoundParameters', 'PSCmdlet', 'PSCommandPath', 'PSCulture', 'PSDebugContext', 'PSHOME', 'PSItem', 'PSScriptRoot', 'PSSenderInfo', 'PSUICulture', 'PSVersionTable', 'PWD', 'Sender', 'ShellId', 'StackTrace', 'switch', 'this', 'true', 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'Confirm', 'WhatIf', 'state', 'AsyncExecutor', 'env' ) foreach ($varExpr in $varExpressions) { $varName = $varExpr.VariablePath.UserPath if ($excludeVars -contains $varName) { continue } # Skip scope-qualified variables if ($varExpr.VariablePath.IsScript -or $varExpr.VariablePath.IsGlobal -or $varExpr.VariablePath.IsLocal -or $varExpr.VariablePath.IsPrivate) { continue } # Skip assignment targets (left side of =) $astParent = $varExpr.Parent if ($astParent -is [System.Management.Automation.Language.AssignmentStatementAst]) { if ($astParent.Left -eq $varExpr) { continue } } if ($autoDetectedVars -contains $varName) { continue } $autoDetectedVars.Add($varName) } } catch { # AST variable detection failed } $allLinkedVariables = @($autoDetectedVars) + @($LinkedVariables) | Select-Object -Unique # TODO: Memory bloat risk? Each button stores references to captured variables in its .Tag property. # If a large object (e.g. 100MB dataset) is in scope when many buttons are created, all buttons # hold references preventing GC. Consider session-based ID lookup instead of direct Tag storage? $capturedVars = @{} if ($ExplicitVariables) { foreach ($key in $ExplicitVariables.Keys) { $capturedVars[$key] = $ExplicitVariables[$key] } } if ($allLinkedVariables -and $allLinkedVariables.Count -gt 0) { foreach ($name in $allLinkedVariables) { if ([string]::IsNullOrWhiteSpace($name)) { continue } if ($capturedVars.ContainsKey($name)) { continue } # Explicit takes precedence $found = $false $value = $null # Try SessionState.PSVariable first if ($CallerSessionState -and !$found) { try { $var = $CallerSessionState.PSVariable.Get($name) if ($null -ne $var) { $value = $var.Value $found = $true } } catch { # PSVariable.Get failed } } # Try internal session state (reflection is cached) if (!$found -and $CallerSessionState) { try { $internal = $null # Cache the FieldInfo on first use if ($null -eq $script:SessionStateFieldInfo) { $script:SessionStateFieldInfo = $CallerSessionState.GetType().GetField( '_sessionState', [System.Reflection.BindingFlags]'Instance, NonPublic' ) } if ($script:SessionStateFieldInfo) { $internal = $script:SessionStateFieldInfo.GetValue($CallerSessionState) } # Fallback to property if field not found if (!$internal) { if ($null -eq $script:SessionStatePropertyInfo) { $script:SessionStatePropertyInfo = $CallerSessionState.GetType().GetProperty( 'Internal', [System.Reflection.BindingFlags]'Instance, NonPublic' ) } if ($script:SessionStatePropertyInfo) { $internal = $script:SessionStatePropertyInfo.GetValue($CallerSessionState) } } if ($internal) { # Cache the GetVariable method if ($null -eq $script:GetVariableMethodInfo) { $script:GetVariableMethodInfo = $internal.GetType().GetMethod('GetVariable', [Type[]]@([string])) } if ($script:GetVariableMethodInfo) { $varObj = $script:GetVariableMethodInfo.Invoke($internal, @($name)) if ($null -ne $varObj) { $value = $varObj.Value $found = $true } } } } catch { Write-Debug "Module variable capture failed: $_" } } # Fall back to global scope if (!$found) { $globalVar = Get-Variable -Name $name -Scope Global -ErrorAction SilentlyContinue if ($globalVar) { $value = $globalVar.Value $found = $true } } if ($found) { $capturedVars[$name] = $value } } } $capturedFuncs = @{} $autoDetectedFuncs = [System.Collections.Generic.List[string]]::new() try { $commandAsts = $Action.Ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.CommandAst] }, $true) # Built-in cmdlets to exclude $excludeFuncs = @( 'Write-Host', 'Write-Output', 'Write-Error', 'Write-Warning', 'Write-Verbose', 'Write-Debug', 'Write-Information', 'Write-Progress', 'Get-Item', 'Set-Item', 'Get-ChildItem', 'Get-Content', 'Set-Content', 'Get-Process', 'Get-Service', 'Get-Date', 'Get-Random', 'Get-Location', 'Get-Member', 'Get-Command', 'Get-Help', 'Get-Variable', 'Set-Variable', 'New-Object', 'New-Item', 'Remove-Item', 'Copy-Item', 'Move-Item', 'Select-Object', 'Where-Object', 'ForEach-Object', 'Sort-Object', 'Group-Object', 'Measure-Object', 'Compare-Object', 'Tee-Object', 'Format-Table', 'Format-List', 'Format-Wide', 'Format-Custom', 'Out-Null', 'Out-String', 'Out-File', 'Out-Host', 'Out-Default', 'Import-Module', 'Export-ModuleMember', 'Get-Module', 'Remove-Module', 'Invoke-Command', 'Invoke-Expression', 'Invoke-RestMethod', 'Invoke-WebRequest', 'Start-Process', 'Stop-Process', 'Start-Job', 'Stop-Job', 'Get-Job', 'Wait-Job', 'Start-Sleep', 'Wait-Event', 'Test-Path', 'Join-Path', 'Split-Path', 'Resolve-Path', 'Convert-Path', 'Add-Member', 'Add-Type', 'ConvertTo-Json', 'ConvertFrom-Json', 'ConvertTo-Csv', 'ConvertFrom-Csv', 'Import-Csv', 'Export-Csv', 'Import-Clixml', 'Export-Clixml', 'Read-Host', 'Clear-Host', 'Get-UiSession', 'Get-UiTheme', 'Set-UiTheme', 'Show-UiMessageDialog', 'Show-UiConfirmDialog', 'Show-UiInputDialog', 'if', 'else', 'elseif', 'switch', 'foreach', 'for', 'while', 'do', 'try', 'catch', 'finally', 'throw', 'return', 'break', 'continue', 'exit' ) foreach ($cmdAst in $commandAsts) { $cmdName = $cmdAst.GetCommandName() if ([string]::IsNullOrWhiteSpace($cmdName)) { continue } if ($excludeFuncs -contains $cmdName) { continue } if ($autoDetectedFuncs -contains $cmdName) { continue } if ($capturedFuncs.ContainsKey($cmdName)) { continue } $cmd = Get-Command -Name $cmdName -ErrorAction SilentlyContinue if ($cmd) { if ($cmd.CommandType -eq 'Function' -or $cmd.CommandType -eq 'Filter') { $autoDetectedFuncs.Add($cmdName) } } else { $autoDetectedFuncs.Add($cmdName) } } } catch { # AST function detection failed } $allLinkedFunctions = @($autoDetectedFuncs) + @($LinkedFunctions) | Select-Object -Unique if ($allLinkedFunctions -and $allLinkedFunctions.Count -gt 0) { foreach ($item in $allLinkedFunctions) { if ([string]::IsNullOrWhiteSpace($item)) { continue } $funcName = $null $funcDef = $null $found = $false if ($item -is [System.Management.Automation.CommandInfo]) { $funcName = $item.Name $funcDef = $item.Definition $found = $true } else { $funcName = $item.ToString() if ($capturedFuncs.ContainsKey($funcName)) { continue } # Try caller's SessionState if ($CallerSessionState -and !$found) { try { $cmd = $CallerSessionState.InvokeCommand.GetCommand( $funcName, [System.Management.Automation.CommandTypes]::Function ) if ($cmd) { $funcDef = $cmd.Definition $found = $true } } catch { Write-Debug "InvokeCommand lookup failed: $_" } } # Try internal session state if (!$found -and $CallerSessionState) { try { $internal = $null $field = $CallerSessionState.GetType().GetField( '_sessionState', [System.Reflection.BindingFlags]'Instance, NonPublic' ) if ($field) { $internal = $field.GetValue($CallerSessionState) } if (!$internal) { $prop = $CallerSessionState.GetType().GetProperty( 'Internal', [System.Reflection.BindingFlags]'Instance, NonPublic' ) if ($prop) { $internal = $prop.GetValue($CallerSessionState) } } if ($internal) { $methods = $internal.GetType().GetMethods( [System.Reflection.BindingFlags]'Instance, Public, NonPublic' ) | Where-Object { $_.Name -eq 'GetFunction' } foreach ($method in $methods) { try { $params = $method.GetParameters() if ($params.Count -eq 1 -and $params[0].ParameterType -eq [string]) { $funcInfo = $method.Invoke($internal, @($funcName)) if ($funcInfo -and $funcInfo.ScriptBlock) { $funcDef = $funcInfo.ScriptBlock.ToString() $found = $true break } } } catch { Write-Debug "GetFunction invoke failed: $_" } } } } catch { Write-Debug "Session state reflection failed: $_" } } # Try Action's module if (!$found -and $Action.Module) { try { $cmd = & $Action.Module { param($functionName) Get-Command -Name $functionName -CommandType Function -ErrorAction SilentlyContinue } $funcName if ($cmd) { $funcDef = $cmd.Definition $found = $true } } catch { Write-Debug "Action module lookup failed: $_" } } # Fall back to global if (!$found) { $cmd = Get-Command -Name $funcName -CommandType Function -ErrorAction SilentlyContinue if ($cmd) { $funcDef = $cmd.Definition $found = $true } } } if ($found -and $funcDef) { $capturedFuncs[$funcName] = $funcDef } } } $resolvedModules = @($LinkedModules | Where-Object { $_ }) # Auto-include the PsUi module $currentModule = $MyInvocation.MyCommand.Module if ($currentModule) { $modulePath = $currentModule.Path $resolvedModules = [string[]]@(@($modulePath) + @($resolvedModules) | Where-Object { $_ } | Select-Object -Unique) } # Credentials are injected at CLICK TIME in New-UiButton.ps1, not here (empty at button creation) [PSCustomObject]@{ Action = $Action CapturedVars = $capturedVars CapturedFuncs = $capturedFuncs LinkedModules = $resolvedModules CallerSessionState = $CallerSessionState AutoDetectedVars = $autoDetectedVars AutoDetectedFuncs = $autoDetectedFuncs } } |