aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_keepalive.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 11:55:43 +0200
committerblasty <blasty@local>2026-07-09 11:55:43 +0200
commit21d2cbbfcdbb712b907573b784bc9d98d5c5e648 (patch)
tree8d2c94975c30e13501f0c8c8785230cae235ae98 /tests/test_keepalive.py
parentdomain/paging layer: FunctionIndex, block-cached DisasmModel, decompile, resolve (diff)
downloadida-tui-21d2cbbfcdbb712b907573b784bc9d98d5c5e648.tar.gz
ida-tui-21d2cbbfcdbb712b907573b784bc9d98d5c5e648.tar.xz
ida-tui-21d2cbbfcdbb712b907573b784bc9d98d5c5e648.zip
keep idle workers alive: bump_idle_ttl + KeepAlive heartbeat
Root cause: idalib workers run an idle watchdog (worker_lifecycle.py) that self-exits after idle_ttl_sec (default 600s) of no requests -- deliberate resource hygiene for a shared/ephemeral supervisor, wrong for an interactive TUI. CLI-opened startup binary always gets 600s (no flag). Fix (both verified against a 12s-TTL session): - IDAClient.bump_idle_ttl(1e9): re-open (idempotent) re-applies TTL, no upper cap -> effectively immortal. Primary fix, call once at startup. - IDAClient.keepalive()/KeepAlive: background server_health heartbeat resets the watchdog; covers adopted sessions too. Safety net. tests/test_keepalive.py: 4/4 (heartbeat + bump both keep a short-TTL worker alive past 2x TTL). docs updated with why + fix.
Diffstat (limited to 'tests/test_keepalive.py')
-rw-r--r--tests/test_keepalive.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/tests/test_keepalive.py b/tests/test_keepalive.py
new file mode 100644
index 0000000..2910e1a
--- /dev/null
+++ b/tests/test_keepalive.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""Prove that an interactive session can 'just chill' without the worker idling
+out. Uses a deliberately short worker TTL to keep the test fast.
+
+ python3 tests/test_keepalive.py
+
+Needs ~/ida-venv + a running server (tests/serverctl.sh) and a writable target.
+"""
+import os
+import sys
+import time
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+REPO = os.path.dirname(HERE)
+sys.path.insert(0, REPO)
+from idatui.client import IDAClient, IDAToolError # noqa: E402
+
+PASS = FAIL = 0
+
+
+def check(name, cond, detail=""):
+ global PASS, FAIL
+ if cond:
+ PASS += 1
+ print(f" ok {name}")
+ else:
+ FAIL += 1
+ print(f" FAIL {name} {detail}")
+
+
+def alive(c, sid):
+ try:
+ c.set_db(sid)
+ c.health()
+ return True
+ except IDAToolError:
+ return False
+
+
+def main():
+ target = os.path.join(REPO, "targets", "ls_ttl")
+ if not os.path.exists(target):
+ src = os.path.join(REPO, "bin", "ls")
+ os.makedirs(os.path.dirname(target), exist_ok=True)
+ import shutil
+ shutil.copy(src, target)
+
+ c = IDAClient(timeout=300)
+ c.connect()
+ SHORT = 12 # worker self-exits after ~12s idle unless kept alive
+
+ print("[heartbeat keeps a short-TTL worker alive]")
+ sid = c.call("idb_open", input_path=target, idle_ttl_sec=SHORT)["session"]["session_id"]
+ c.set_db(sid)
+ ka = c.keepalive(interval=4.0).start()
+ time.sleep(SHORT * 2 + 2) # idle well past the TTL, but heartbeat is beating
+ ok = alive(c, sid)
+ check("alive past 2x TTL with heartbeat", ok, f"beats={ka.beats}")
+ check("heartbeat actually beat", ka.beats >= 4, f"beats={ka.beats}")
+ check("heartbeat had no failures", ka.failures == 0, f"failures={ka.failures}")
+ ka.stop()
+
+ print("\n[bump_idle_ttl makes it effectively immortal]")
+ sid = c.call("idb_open", input_path=target, idle_ttl_sec=SHORT)["session"]["session_id"]
+ c.set_db(sid)
+ c.bump_idle_ttl() # ~1e9 seconds
+ time.sleep(SHORT * 2 + 6) # zero requests during this window
+ check("alive past 2x TTL after bump, zero requests", alive(c, sid))
+
+ c.close()
+ print(f"\n{PASS} passed, {FAIL} failed")
+ return 1 if FAIL else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())