aboutsummaryrefslogtreecommitdiffstats
path: root/.auto/diff_spans.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 01:31:57 +0200
committeruser <user@clank>2026-08-07 01:31:57 +0200
commit93240e2f0c0d716758c43154dd37802f556ac6d0 (patch)
tree564ec41d72e9f4261b582a2b9c5ceb8995162fac /.auto/diff_spans.py
parentautoresearch: checks gate names the failing scenario and retries it alone (diff)
downloadida-tui-93240e2f0c0d716758c43154dd37802f556ac6d0.tar.gz
ida-tui-93240e2f0c0d716758c43154dd37802f556ac6d0.tar.xz
ida-tui-93240e2f0c0d716758c43154dd37802f556ac6d0.zip
Stop ida-pro-mcp installing a sys.setprofile hook around every tool call. Its deadline mechanism profiles every python call/return so a pure-python tool loop can be interrupted; our tools are call-heavy, so it taxed the whole backend 3.3x. Worker now sets IDA_MCP_TOOL_TIMEOUT_SEC=0 and arms the deadline itself with one polling watchdog thread + ida_kernwin.set_cancelled() (the half that actually frees the IDA main thread). Also rewrote _idatui_spans to jump between colour tags instead of walking characters (byte-identical over 258k real lines).
Result: {"status":"keep","total_ms":26923.9,"lg_boot_ms":762.2,"lg_decomp_ms":2631.7,"lg_graph_ms":941.8,"lg_hex_ms":1052,"lg_index_ms":67.2,"lg_listing_cold_ms":440.6,"lg_listing_warm_ms":530.5,"lg_nav_ms":10598.3,"lg_palette_ms":4.6,"lg_render_ms":227.8,"lg_search_ms":5265.5,"pure_graph_ms":237.9,"sm_boot_ms":535.3,"sm_decomp_ms":631.8,"sm_graph_ms":702.1,"sm_hex_ms":841.1,"sm_index_ms":0,"sm_listing_cold_ms":268.2,"sm_listing_warm_ms":269.4,"sm_nav_ms":443.6,"sm_palette_ms":0.3,"sm_render_ms":277.8,"sm_search_ms":194.4,"fails":0}
Diffstat (limited to '.auto/diff_spans.py')
-rw-r--r--.auto/diff_spans.py106
1 files changed, 106 insertions, 0 deletions
diff --git a/.auto/diff_spans.py b/.auto/diff_spans.py
new file mode 100644
index 0000000..95721a5
--- /dev/null
+++ b/.auto/diff_spans.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+"""Differential check: the current `_idatui_spans` vs the one at a git ref.
+
+The span walker turns IDA's colour-tagged disassembly line into (spans, ops).
+It is on the hot path of every listing row, so it is worth optimising -- but its
+output drives highlighting, operand marking and the cursor's column arithmetic,
+so "faster" is only acceptable if it is byte-identical.
+
+This pulls both implementations out of `server/patch_server.py` (the current
+working tree, and whatever `--ref` names), runs them over every tagged line of
+a real binary, and reports the first disagreement.
+
+ /usr/bin/python3 .auto/diff_spans.py [--ref HEAD] [--target targets/bash]
+ [--limit 60000]
+"""
+from __future__ import annotations
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+def load_impl(path: str, name: str):
+ """Exec just the span-walker section of a patch_server.py's injected BODY.
+
+ BODY is a normal (non-raw) triple-quoted string, so the escapes in it are
+ resolved once by importing the module -- slicing the file text instead would
+ compile the *undecoded* source and silently test a different program (an
+ escaped backslash became a literal one, and every 's' in a comment turned
+ into a space).
+ """
+ import importlib.util
+ spec = importlib.util.spec_from_file_location(f"_ps_{name}", path)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod) # IDA-free at import time
+ body = mod.BODY
+ a = body.index("#: IDA colour tag -> the semantic kind")
+ b = body.index("def _idatui_unknown_row")
+ g = {"__name__": name}
+ exec(compile(body[a:b], name, "exec"), g) # noqa: S102
+ return g["_idatui_spans"]
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--ref", default="HEAD")
+ ap.add_argument("--target", default="targets/bash")
+ ap.add_argument("--limit", type=int, default=60000)
+ a = ap.parse_args()
+
+ d = tempfile.mkdtemp(prefix="diffspans-")
+ old_path = os.path.join(d, "patch_server_old.py")
+ with open(old_path, "w") as fh:
+ fh.write(subprocess.run(
+ ["git", "-C", ROOT, "show", f"{a.ref}:server/patch_server.py"],
+ capture_output=True, text=True, check=True).stdout)
+ new = load_impl(os.path.join(ROOT, "server", "patch_server.py"), "new")
+ old = load_impl(old_path, "old")
+
+ binary = os.path.join(ROOT, a.target)
+ tgt = os.path.join(d, os.path.basename(binary))
+ shutil.copy2(binary, tgt)
+ shutil.copy2(binary + ".i64", tgt + ".i64")
+ try:
+ import idapro
+ if idapro.open_database(tgt, run_auto_analysis=False) != 0:
+ raise SystemExit("could not open database")
+ import ida_bytes
+ import ida_lines
+ import ida_segment
+ import idaapi
+
+ checked = bad = 0
+ for si in range(ida_segment.get_segm_qty()):
+ seg = ida_segment.getnseg(si)
+ if seg is None:
+ continue
+ ea = seg.start_ea
+ while ea < seg.end_ea and checked < a.limit:
+ line = ida_lines.generate_disasm_line(ea, 0)
+ if line:
+ checked += 1
+ ra, rb = old(line), new(line)
+ if ra != rb:
+ bad += 1
+ if bad <= 3:
+ print(f"MISMATCH @ {ea:#x}\n line={line!r}\n"
+ f" old={ra!r}\n new={rb!r}")
+ nxt = ida_bytes.get_item_end(ea)
+ ea = nxt if nxt > ea else ea + 1
+ if checked >= a.limit:
+ break
+ print(f"checked {checked} lines, {bad} mismatches")
+ idapro.close_database(False)
+ return 1 if bad else 0
+ finally:
+ shutil.rmtree(d, ignore_errors=True)
+
+
+if __name__ == "__main__":
+ sys.exit(main())