Files
privatae-bastion/client-go/computer_tree_linux.go
T
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
3.6 KiB
Go

//go:build linux
package main
import (
"errors"
"os/exec"
"strings"
)
// Linux: AT-SPI 2 through python3 + gi (Atspi), a fixed script. The
// accessibility bus is per session and found through the X display, so the
// display discovered for computer:windows is handed to the helper. A box
// without the binding says so, with the package to install.
const computerTreeKind = "atspi-python"
const linuxTreeScript = `
import json, os, sys
try:
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
except Exception as e:
print(json.dumps({"error": "AT-SPI binding not available: %s (install python3-gi and gir1.2-atspi-2.0)" % e})); sys.exit(0)
pid = int(os.environ["PRIVATAE_TARGET_PID"]); cap = int(os.environ["PRIVATAE_MAX_NODES"])
count = 0; trunc = False
def node(el, depth):
global count, trunc
if count >= cap or depth > 40:
trunc = True; return None
count += 1
n = {"role": Atspi.role_get_name(el.get_role()).replace(" ", "_")}
name = el.get_name() or ""
if name: n["name"] = name
try:
st = el.get_state_set()
n["enabled"] = bool(st.contains(Atspi.StateType.ENABLED))
if st.contains(Atspi.StateType.FOCUSED): n["focused"] = True
if not st.contains(Atspi.StateType.SHOWING): n["offscreen"] = True
except Exception: pass
try:
r = el.get_extents(Atspi.CoordType.SCREEN)
if r.width or r.height: n.update(x=r.x, y=r.y, w=r.width, h=r.height)
except Exception: pass
try:
# Accessible carries a deprecated zero-arg get_text() that shadows
# Text.get_text(start, end) on the gi class, so call the interface
# method explicitly.
t = el.get_text_iface()
if t is not None:
v = Atspi.Text.get_text(t, 0, min(Atspi.Text.get_character_count(t), 200))
if v: n["value"] = v
except Exception: pass
kids = []
try:
for i in range(el.get_child_count()):
k = node(el.get_child_at_index(i), depth + 1)
if k: kids.append(k)
if trunc: break
except Exception: pass
if kids: n["children"] = kids
return n
def fail(msg):
print(json.dumps({"error": msg})); sys.exit(0)
try:
desk = Atspi.get_desktop(0)
n_apps = desk.get_child_count()
except Exception as e:
fail("could not reach the accessibility bus: %s" % e)
app = None
for i in range(n_apps):
try:
a = desk.get_child_at_index(i)
if a is not None and a.get_process_id() == pid: app = a; break
except Exception: continue
if app is None:
fail("pid %d has no AT-SPI application (not running, no toolkit support, or accessibility off)" % pid)
wins = []
try:
for i in range(app.get_child_count()):
w = node(app.get_child_at_index(i), 0)
if w: wins.append(w)
name = app.get_name() or ""
except Exception as e:
fail("the application stopped answering the accessibility bus: %s" % e)
print(json.dumps({"app": name, "truncated": trunc, "windows": wins}))
`
func readTree(pid, maxNodes int) ([]byte, error) {
if !execOnPath("python3") {
return nil, errors.New("python3 not installed — the AT-SPI reader needs python3 with gi (python3-gi, gir1.2-atspi-2.0)")
}
env := helperEnv(pid, maxNodes)
if d := x11Display(); d != "" {
env = append(env, "DISPLAY="+d)
}
out, err := runFixedEnv(env, "python3", "-c", linuxTreeScript)
if err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) && len(ee.Stderr) > 0 {
return nil, errors.New("AT-SPI walk failed: " + truncate(strings.TrimSpace(string(ee.Stderr)), 300))
}
return nil, errors.New("python3 failed: " + err.Error())
}
return out, nil
}