Files
privatae-bastion/client-go/computer_tree_windows.go
Privatae e4c9b69b46
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s
bastion 0.53.2
2026-10-20 04:42:14 +00:00

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
}