testsAndMisc/python_pkg/phone_guard/vd_state.py
Krzysztof kuhy Rudnicki aa37852296 phone_guard: lease or block raw adb; android_ui drives a session's own virtual display
A raw `adb shell input tap` took no lease, so one session's tap landed in
another session's app (2026-09-25). python_pkg.phone_guard classifies every
adb call in a Bash command and, as the Claude Code PreToolUse hook, takes
the matching phone_lease (one app / real screen / whole phone) or exits 2:
key/text input without -d, input on another session's virtual display, or
a lease still held elsewhere after 40 s.

android_ui gains --display self|<id>: it drives only this session's
phone_vd display under that app's lease, and every input carries -d.
uiautomator sees display 0 only, so virtual-display trees come from a tiny
instrumentation APK (A11yDump.java) built from source without Gradle.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpA6wW2DKiovQADTdjWJoH
2026-09-25 16:09:45 +02:00

84 lines
2.5 KiB
Python

"""Which session owns which virtual display, read from ``phone_vd.sh`` state.
``phone_vd.sh`` keeps one directory per session under
``$PHONE_VD_STATE_DIR/<serial>/<owner>/`` holding ``display`` (the logical id
``input -d`` takes), ``sf`` (the SurfaceFlinger id ``screencap -d`` takes) and
``package``. The owner directory name is the phone_lease owner id with
anything outside ``[A-Za-z0-9_.-]`` replaced by ``_``, exactly as the script
writes it.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
from pathlib import Path
import re
import tempfile
_UNSAFE = re.compile(r"[^A-Za-z0-9_.-]")
def state_root() -> Path:
"""The directory ``phone_vd.sh`` keeps its per-phone state in.
``$PHONE_VD_STATE_DIR``, else ``phone_vd`` under the temp dir -- the same
``${TMPDIR:-/tmp}`` rule the script applies, so both sides agree.
"""
default = Path(tempfile.gettempdir()) / "phone_vd"
return Path(os.environ.get("PHONE_VD_STATE_DIR", str(default)))
def owner_dir_name(owner: str) -> str:
"""``owner`` as ``phone_vd.sh`` names its state directory."""
return _UNSAFE.sub("_", owner)
@dataclass(frozen=True)
class SessionDisplay:
"""One running virtual display and whose it is."""
owner_dir: str
display: int
sf: str
package: str
def _read(path: Path) -> str:
try:
return path.read_text(encoding="utf-8").strip()
except OSError:
return ""
def displays(serial: str) -> list[SessionDisplay]:
"""Every session's virtual display on ``serial``."""
root = state_root() / serial
found: list[SessionDisplay] = []
if not root.is_dir():
return found
for entry in sorted(root.iterdir()):
display = _read(entry / "display")
if not display.isdigit():
continue
found.append(
SessionDisplay(
owner_dir=entry.name,
display=int(display),
sf=_read(entry / "sf"),
package=_read(entry / "package"),
)
)
return found
def own_display(serial: str, owner: str) -> SessionDisplay | None:
"""``owner``'s virtual display on ``serial``, if it has one running."""
mine = owner_dir_name(owner)
return next((d for d in displays(serial) if d.owner_dir == mine), None)
def display_owner(serial: str, display: int) -> SessionDisplay | None:
"""The session whose virtual display has logical id ``display``."""
return next((d for d in displays(serial) if d.display == display), None)