aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/pane.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-31 09:14:41 +0200
committeruser <user@clank>2026-07-31 09:14:41 +0200
commitf720a16f8e4ac538e6b880960fd411357114656d (patch)
treed371c53a8d2e666e25f85acab960dab141520b26 /idatui/pane.py
parenttrace: a stale navigation no longer drags the view back (diff)
downloadida-tui-f720a16f8e4ac538e6b880960fd411357114656d.tar.gz
ida-tui-f720a16f8e4ac538e6b880960fd411357114656d.tar.xz
ida-tui-f720a16f8e4ac538e6b880960fd411357114656d.zip
rpc: stop lying to the driver about renames, modals and teardown
Five defects found while an agent drove a long RE session over the socket. Each one was reproduced on a live spawned pane first (an in-process pilot would not have shown any of them), then fixed: pane stop truncated the save. `stop` asked the app to quit, slept 400 ms, then unconditionally killed the pane. Quitting runs App.on_unmount, which writes every dirty database; a 90 MB .i64 takes tens of seconds, so the kill landed mid-write and a whole session's annotations went to /dev/null with a cheerful {"stopped": [...]} on stdout. Now it waits for the pane to actually exit (--timeout, default 600 s) and only force-kills on timeout, saying so. The quit verb bypassed the dirty check. It called app.exit() directly rather than the path a human gets, so the "unsaved changes" logic never ran. It now routes through _on_quit_choice and reports {saving, dirty}. Naming a function start from the listing never reached the function index. `goto <addr>` puts the cursor on the address token, so `n` takes the name-an-address path, which called bump_items() but left FunctionIndex holding the old name. Result: the rename response snapshot showed the section label, and functions()/names()/the palette all reported the rename had not happened — so a driver that trusts its readbacks redoes work it already did. Twice, in the session that prompted this. _do_name_addr now updates the index, the nav stack and the table cell when the address is a function start. A stripped binary with no entry function started up *inside a modal*. _auto_land pushed the symbol palette when main() was missing, while ping still answered ready:true. Every keystroke an RPC driver injected went into the palette's search box and was silently swallowed. It now lands on the first function instead and hints at Ctrl+N. Verbs that inject keystrokes now refuse when a modal is on top, naming it, instead of failing with "'goto' prompt did not open (word under cursor?)" — a message that blamed the cursor for what was always a focus problem. Also: `drive raw` passes k=v values through as strings, so `view lines=8` died with "'<' not supported between instances of 'int' and 'str'". Numeric params are now coerced centrally rather than at each call site. tests/test_scenarios.py: 212 passed, 0 failed.
Diffstat (limited to 'idatui/pane.py')
-rw-r--r--idatui/pane.py22
1 files changed, 21 insertions, 1 deletions
diff --git a/idatui/pane.py b/idatui/pane.py
index 1a46a4f..37a5f0c 100644
--- a/idatui/pane.py
+++ b/idatui/pane.py
@@ -248,18 +248,29 @@ def stop(args) -> int:
if not rows:
print("error: no matching pane (need --sock or --pane)", file=sys.stderr)
return 2
+ killed: list[str] = []
for r in rows:
sock, pane = r.get("sock"), r.get("pane")
+ quit_ok = False
if sock and os.path.exists(sock):
try: # ask it to quit gracefully first
with RpcClient(sock) as c:
c.call("quit")
- time.sleep(0.4)
+ quit_ok = True
except (OSError, RpcError, ConnectionError):
pass
+ # Wait for the pane to actually go away. Quitting runs App.on_unmount,
+ # which writes every dirty database; a 90 MB .i64 takes tens of seconds.
+ # Killing the pane on a fixed short sleep truncated that save and
+ # silently destroyed the session's work, so block on the real signal.
+ if pane and quit_ok:
+ deadline = time.monotonic() + float(args.timeout)
+ while time.monotonic() < deadline and _pane_alive(pane):
+ time.sleep(0.25)
if pane and _pane_alive(pane):
subprocess.run(["tmux", "kill-pane", "-t", pane],
capture_output=True)
+ killed.append(pane)
if sock:
try:
os.unlink(sock)
@@ -271,6 +282,12 @@ def stop(args) -> int:
out = {"stopped": [r.get("sock") or r.get("pane") for r in rows]}
if reaped:
out["reaped_workers"] = reaped
+ if killed:
+ # Only ever reached on timeout: say so, because it means a save may have
+ # been cut short rather than "clean teardown".
+ out["force_killed"] = killed
+ out["warning"] = (f"pane(s) did not exit within {args.timeout}s and were "
+ "killed; unsaved database changes may be lost")
print(json.dumps(out))
return 0
@@ -335,6 +352,9 @@ def main(argv: list[str]) -> int:
st = sub.add_parser("stop", help="graceful quit + kill the pane")
st.add_argument("--sock")
st.add_argument("--pane")
+ st.add_argument("--timeout", type=float, default=600.0,
+ help="seconds to wait for the pane to exit (it saves dirty "
+ "databases on the way out) before force-killing it")
st.set_defaults(fn=stop)
ls = sub.add_parser("list", help="list tracked panes")