108 lines
4.1 KiB
Go
108 lines
4.1 KiB
Go
//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
|
|
}
|