bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// Windows: UI Automation through the UIAutomationClient .NET assembly, driven
|
||||
// by a fixed PowerShell script. The script is one string, handed over as
|
||||
// -EncodedCommand (UTF-16LE base64) so no quoting question arises; the pid
|
||||
// and node cap arrive as environment variables it reads and casts to [int].
|
||||
// It walks every top-level window owned by the pid with the control-view
|
||||
// TreeWalker and prints one compact JSON document.
|
||||
//
|
||||
// ConvertTo-Json counts a depth level for every dictionary AND every array,
|
||||
// so a node at tree depth d sits at JSON depth 2+2d; the walk's depth cap of
|
||||
// 40 needs -Depth 83 — 100 is the serializer's
|
||||
// maximum and is used. A control that vanishes mid-walk (a closing menu)
|
||||
// throws ElementNotAvailableException; it is skipped, the rest of the tree
|
||||
// still answers.
|
||||
|
||||
const computerTreeKind = "uiautomation-powershell"
|
||||
|
||||
const windowsTreeScript = `
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName UIAutomationClient
|
||||
Add-Type -AssemblyName UIAutomationTypes
|
||||
$targetPid = [int]$env:PRIVATAE_TARGET_PID
|
||||
$maxNodes = [int]$env:PRIVATAE_MAX_NODES
|
||||
$script:count = 0
|
||||
$script:trunc = $false
|
||||
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||
function Node($el, $depth) {
|
||||
if ($script:count -ge $maxNodes -or $depth -gt 40) { $script:trunc = $true; return $null }
|
||||
try { $c = $el.Current } catch { return $null }
|
||||
$script:count++
|
||||
$n = [ordered]@{ role = ($c.ControlType.ProgrammaticName -replace '^ControlType\.', '').ToLower() }
|
||||
if ($c.Name) { $n.name = $c.Name }
|
||||
if ($c.AutomationId) { $n.id = $c.AutomationId }
|
||||
$r = $c.BoundingRectangle
|
||||
if (-not [double]::IsInfinity($r.X)) { $n.x = [int]$r.X; $n.y = [int]$r.Y; $n.w = [int]$r.Width; $n.h = [int]$r.Height }
|
||||
$n.enabled = [bool]$c.IsEnabled
|
||||
if ($c.HasKeyboardFocus) { $n.focused = $true }
|
||||
if ($c.IsOffscreen) { $n.offscreen = $true }
|
||||
try {
|
||||
$vp = $null
|
||||
if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) {
|
||||
$v = $vp.Current.Value
|
||||
if ($v -and $v.Length -le 200) { $n.value = $v }
|
||||
}
|
||||
} catch {}
|
||||
$kids = @()
|
||||
try { $ch = $walker.GetFirstChild($el) } catch { $ch = $null }
|
||||
while ($ch -ne $null) {
|
||||
$k = Node $ch ($depth + 1)
|
||||
if ($k -ne $null) { $kids += ,$k }
|
||||
if ($script:trunc) { break }
|
||||
try { $ch = $walker.GetNextSibling($ch) } catch { $ch = $null }
|
||||
}
|
||||
if ($kids.Count -gt 0) { $n.children = $kids }
|
||||
return $n
|
||||
}
|
||||
$root = [System.Windows.Automation.AutomationElement]::RootElement
|
||||
$cond = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ProcessIdProperty, $targetPid)
|
||||
$tops = $root.FindAll([System.Windows.Automation.TreeScope]::Children, $cond)
|
||||
$wins = @()
|
||||
$app = ''
|
||||
foreach ($w in $tops) {
|
||||
if (-not $app) { try { $app = (Get-Process -Id $targetPid).ProcessName } catch {} }
|
||||
$t = Node $w 0
|
||||
if ($t -ne $null) { $wins += ,$t }
|
||||
}
|
||||
if ($tops.Count -eq 0) {
|
||||
@{ error = "no top-level window owned by pid $targetPid" } | ConvertTo-Json -Compress
|
||||
} else {
|
||||
@{ app = $app; truncated = $script:trunc; windows = $wins } | ConvertTo-Json -Compress -Depth 100
|
||||
}
|
||||
`
|
||||
|
||||
// encodedCommand is the script as PowerShell's -EncodedCommand wants it:
|
||||
// UTF-16LE, base64. Computed once.
|
||||
var windowsTreeEncoded = func() string {
|
||||
u := utf16.Encode([]rune(windowsTreeScript))
|
||||
b := make([]byte, 0, len(u)*2)
|
||||
for _, c := range u {
|
||||
b = append(b, byte(c), byte(c>>8))
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}()
|
||||
|
||||
func readTree(pid, maxNodes int) ([]byte, error) {
|
||||
out, err := runFixedEnv(helperEnv(pid, maxNodes), "powershell.exe",
|
||||
"-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", windowsTreeEncoded)
|
||||
if err != nil {
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && len(ee.Stderr) > 0 {
|
||||
return nil, errors.New("UI Automation walk failed: " + truncate(strings.TrimSpace(string(ee.Stderr)), 300))
|
||||
}
|
||||
return nil, errors.New("powershell failed: " + err.Error())
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user