private/implicitwindow/Invoke-UiImplicitWindow.ps1
|
function Invoke-UiImplicitWindow { <# .SYNOPSIS Called when a control finds no session. This decides whether building a window makes sense at all, and when it does, reads the calling script and builds one. Returns how the script should end, or $false when it leaves and allows the a no-window error to throw instead. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$CallerName, [Parameter(Mandatory)] [System.Management.Automation.SessionState]$CallerState, [System.Management.Automation.PSCmdlet]$CallerCmdlet ) # A PsUi background thread reports no session too, and a window holds a pool thread on Join. if ([PsUi.SessionManager]::CurrentSessionId -ne [Guid]::Empty) { return $false } if ($Global:__PsUiSessionId) { return $false } if ($Global:AsyncExecutor) { return $false } if ([PsUi.AsyncExecutor]::CurrentExecutor) { return $false } # No interactive desktop to show a window on, and New-UiWindow holds its thread until one closes. if (![Environment]::UserInteractive) { return $false } if ($Host.Name -eq 'ServerRemoteHost') { return $false } foreach ($argument in [Environment]::GetCommandLineArgs()) { if ($argument -notmatch '^(--?|/)(.+)$') { continue } $switch = $Matches[2] if ('file'.StartsWith($switch, [System.StringComparison]::OrdinalIgnoreCase)) { break } if ($switch.Length -ge 4 -and 'noninteractive'.StartsWith($switch, [System.StringComparison]::OrdinalIgnoreCase)) { return $false } } # For a test run that would rather have the old error than a window. if ($env:PsUiNoImplicitWindow) { return $false } $span = Get-UiImplicitSpan -CallerName $CallerName if (!$span) { return $false } # A script file is a submission. At a cmd the read covers only the line that was typed. But pasting multiple lines? # A console without bracketed paste sends a paste's contents over one line at a time, so the rest of it is still in PSReadLine's queue while the first line runs, resulting in a window for each call. # Those lines join this window instead of each opening one of their own. if (!$span.ScriptName -and $span.Complete) { $span = Join-UiPendingInput -Span $span } # A window built from a file takes its title from the file. At a prompt there is no file to take one from, so the title says what built it rather than fall back on New-UiWindow's UI $title = 'PsUi' if ($span.ScriptName) { $title = [System.IO.Path]::GetFileNameWithoutExtension($span.ScriptName) } $where = "line $($span.StartLine)" if ($span.ScriptName) { $where = "$($span.ScriptName):$($span.StartLine)" } Write-Verbose "$CallerName has no window, so PsUi is building one around $where. The script stops when it closes." $content = New-UiImplicitContent -Span $span # A module literal block would capture PsUi's variables rather than the script's, so the block is built from parsed text instead. # Parsed rather than Created so the call carries the script's name. New-UiWindow reads MyInvocation for the file it puts in front of a content error. # A created block leaves that empty, and the error comes out saying script. $tokens = $null $errors = $null $call = 'New-UiWindow -Title $args[0] -Content $args[1] 2>&1 | & $args[2]' $parsed = [System.Management.Automation.Language.Parser]::ParseInput($call, $span.ScriptName, [ref]$tokens, [ref]$errors) # InvokeScript drops New-UiWindow's error stream into $Error, ergo the 2>&1 $writer = $PSCmdlet if ($CallerCmdlet) { $writer = $CallerCmdlet } $relay = { process { if ($_ -isnot [System.Management.Automation.ErrorRecord]) { return } # The redirect already put it in $Error once $global:Error.Remove($_) $writer.WriteError($_) } }.GetNewClosure() # InvokeScript hands a stopped window back as a MethodInvocationException try { $null = $ExecutionContext.InvokeCommand.InvokeScript($CallerState, $parsed.GetScriptBlock(), $title, $content, $relay) } catch [System.Management.Automation.MethodInvocationException] { $inner = $_.Exception.InnerException if ($inner -isnot [System.Management.Automation.IContainsErrorRecord]) { throw } # Every wrap on the way out left its own rec in $Error $source = $inner.ErrorRecord $chain = [System.Collections.Generic.HashSet[object]]::new() for ($link = $_.Exception; $link; $link = $link.InnerException) { $null = $chain.Add($link) } if ($source.Exception) { $null = $chain.Add($source.Exception) } $stale = @($global:Error | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] -and $chain.Contains($_.Exception) }) foreach ($record in $stale) { $global:Error.Remove($record) } # New-UiWindow's name from the id, need to add the cotnrol name $errorId = $source.FullyQualifiedErrorId.Split(',')[0] $exception = [System.Exception]::new($source.ToString(), $source.Exception) $failure = [System.Management.Automation.ErrorRecord]::new($exception, $errorId, $source.CategoryInfo.Category, $source.TargetObject) $stamp = [System.Management.Automation.ErrorRecord].GetMethod('SetInvocationInfo', [System.Reflection.BindingFlags]'Instance, NonPublic') if ($stamp) { $null = $stamp.Invoke($failure, @($writer.MyInvocation)) } # The control's -ErrorAction SilentlyContinue gets this far and would silent the throw $ErrorActionPreference = 'Stop' throw $failure } # exit ends the nearest running script file with code 0 and leaves the console it ran from alone. # With no script file anywhere on the stack, a module function called at a prompt, exit would take the console with it, so that one gets its pipeline stopped instead. foreach ($frame in (Get-PSCallStack)) { if ($frame.InvocationInfo.MyCommand.CommandType -eq 'ExternalScript') { return 'Exit' } } # Under -Command the stopped pipeline exits 1 on a normal close but -NoExit keeps its console and -Command still has stdin to read $arguments = [Environment]::GetCommandLineArgs() $launched = $false for ($i = 1; $i -lt $arguments.Count; $i++) { if ($arguments[$i] -notmatch '^(--?|/)(.+)$') { continue } $switch = $Matches[2] if ($switch.Length -ge 3 -and 'noexit'.StartsWith($switch, [System.StringComparison]::OrdinalIgnoreCase)) { return 'Stop' } if ($switch -eq 'ec' -or 'encodedcommand'.StartsWith($switch, [System.StringComparison]::OrdinalIgnoreCase)) { $launched = $true; continue } # Everything after -Command is the command text if (!'command'.StartsWith($switch, [System.StringComparison]::OrdinalIgnoreCase)) { continue } $launched = $i + 1 -ge $arguments.Count -or $arguments[$i + 1] -ne '-' break } if ($launched) { return 'Exit' } return 'Stop' } |