mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:07:03 +01:00
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
166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
"""Decide what a shell command's ``adb`` calls need before they may run.
|
|
|
|
Pure: no adb, no filesystem. The hook feeds it the Bash tool's command and
|
|
applies the answer. Every ``adb`` invocation in the command gets one
|
|
:class:`Need`:
|
|
|
|
* nothing -- reads (``logcat``, ``dumpsys``, ``getprop``, ``pull``,
|
|
``screencap``), file transfer, and anything aimed at the caller's own
|
|
virtual display;
|
|
* a lease on the real screen -- input or launches on display 0;
|
|
* a lease on one app -- ``force-stop``, ``install``, per-app network cuts;
|
|
* the whole-phone lease -- reboot, Wi-Fi/airplane/Doze/battery tricks,
|
|
``settings put``: everything that changes the phone for every session;
|
|
* a block -- key/text input without ``-d`` (it goes to whichever display last
|
|
took focus, i.e. possibly another session's app), or input aimed at another
|
|
session's virtual display.
|
|
|
|
Unknown commands pass: this guards against interrupting other sessions, it is
|
|
not a sandbox.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shlex
|
|
|
|
from python_pkg.phone_guard._need import SCREEN, WHOLE_PHONE, Need
|
|
from python_pkg.phone_guard._remote import REMOTE, _display_flag
|
|
|
|
_SEPARATORS = {";", "&&", "||", "|", "&", "(", ")", "\n"}
|
|
_GLOBAL_WITH_ARG = {"-s", "-t", "-H", "-P", "-L"}
|
|
_HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1[^\n]*\n.*?\n\s*\2\s*(?=\n|$)", re.DOTALL)
|
|
|
|
_WHOLE_PHONE_ADB = {
|
|
"reboot",
|
|
"root",
|
|
"unroot",
|
|
"remount",
|
|
"disable-verity",
|
|
"enable-verity",
|
|
"tcpip",
|
|
"usb",
|
|
"kill-server",
|
|
"sideload",
|
|
}
|
|
_READ_ADB = {
|
|
"devices",
|
|
"get-serialno",
|
|
"get-state",
|
|
"version",
|
|
"help",
|
|
"logcat",
|
|
"pull",
|
|
"push",
|
|
"wait-for-device",
|
|
"forward",
|
|
"reverse",
|
|
"bugreport",
|
|
"start-server",
|
|
"sync",
|
|
}
|
|
_KEYS = {"text", "keyevent", "keycombination"}
|
|
_POINTER = {"tap", "swipe", "draganddrop", "press", "roll", "motionevent", "scroll"}
|
|
|
|
|
|
def _tokens(command: str) -> list[str]:
|
|
"""Shell tokens, heredoc bodies removed (a script being WRITTEN is not run)."""
|
|
lexer = shlex.shlex(
|
|
_HEREDOC.sub("\n", command), posix=True, punctuation_chars=";&|()"
|
|
)
|
|
lexer.whitespace = " \t\r"
|
|
lexer.whitespace_split = True
|
|
try:
|
|
return list(lexer)
|
|
except ValueError:
|
|
return []
|
|
|
|
|
|
def _split(tokens: list[str]) -> list[list[str]]:
|
|
"""Split a token stream into simple commands."""
|
|
groups: list[list[str]] = [[]]
|
|
for word in tokens:
|
|
if word in _SEPARATORS or set(word) <= set(";&|"):
|
|
groups.append([])
|
|
else:
|
|
groups[-1].append(word)
|
|
return [g for g in groups if g]
|
|
|
|
|
|
def adb_calls(command: str) -> list[list[str]]:
|
|
"""Every ``adb ...`` simple command, as its argument list (no ``adb``)."""
|
|
calls = []
|
|
for simple in _split(_tokens(command)):
|
|
for i, word in enumerate(simple):
|
|
if word == "adb" or word.endswith("/adb"):
|
|
calls.append(simple[i + 1 :])
|
|
break
|
|
return calls
|
|
|
|
|
|
def _input(args: list[str], serial: str | None) -> Need:
|
|
display = _display_flag(args, "-d")
|
|
verb = next((a for a in args if a in _KEYS | _POINTER), "")
|
|
if display is not None and display != 0:
|
|
return Need(display=display, serial=serial)
|
|
if display is None and verb in _KEYS:
|
|
return Need(
|
|
block=f"`input {verb}` without -d goes to whichever display last took "
|
|
"focus -- possibly another session's app. Add `-d 0` for the real "
|
|
"screen or `-d <your display>` (phone_vd.sh id).",
|
|
serial=serial,
|
|
)
|
|
return Need(scope=SCREEN, serial=serial)
|
|
|
|
|
|
def _remote(args: list[str], serial: str | None) -> list[Need]:
|
|
"""Needs of one ``adb shell`` command line."""
|
|
found = []
|
|
for simple in _split(_tokens(" ".join(args))):
|
|
tool, rest = simple[0], simple[1:]
|
|
found.append(_remote_one(tool, rest, serial))
|
|
return found
|
|
|
|
|
|
def _remote_one(tool: str, rest: list[str], serial: str | None) -> Need:
|
|
"""The need of one remote command, stamped with the call's serial."""
|
|
if tool == "input":
|
|
return _input(rest, serial)
|
|
need = REMOTE.get(tool, lambda _rest: Need())(rest)
|
|
return Need(scope=need.scope, block=need.block, display=need.display, serial=serial)
|
|
|
|
|
|
def needs(command: str, apk_package: dict[str, str] | None = None) -> list[Need]:
|
|
"""Every adb call's :class:`Need` in ``command``.
|
|
|
|
``apk_package`` maps an APK path to its package, resolved by the caller
|
|
(reading an APK is I/O); an unknown APK's install needs the screen.
|
|
"""
|
|
known = apk_package or {}
|
|
result: list[Need] = []
|
|
for call in adb_calls(command):
|
|
serial = None
|
|
i = 0
|
|
while i < len(call) and call[i].startswith("-"):
|
|
if call[i] in _GLOBAL_WITH_ARG and i + 1 < len(call):
|
|
serial = call[i + 1] if call[i] == "-s" else serial
|
|
i += 2
|
|
else:
|
|
i += 1
|
|
sub, args = (call[i], call[i + 1 :]) if i < len(call) else ("", [])
|
|
if sub in {"shell", "exec-out"}:
|
|
result.extend(_remote(args, serial))
|
|
elif sub in _WHOLE_PHONE_ADB:
|
|
result.append(Need(scope=WHOLE_PHONE, serial=serial))
|
|
elif sub in {"install", "install-multiple", "uninstall"}:
|
|
target = next((a for a in reversed(args) if not a.startswith("-")), "")
|
|
package = known.get(target) or (target if sub == "uninstall" else None)
|
|
result.append(
|
|
Need(scope=f"pkg:{package}" if package else SCREEN, serial=serial)
|
|
)
|
|
elif sub in _READ_ADB or not sub:
|
|
result.append(Need(serial=serial))
|
|
else:
|
|
result.append(Need(serial=serial))
|
|
return result
|