diff options
| author | blasty <blasty@local> | 2026-08-07 00:33:12 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-07 00:33:28 +0200 |
| commit | 2b0ae8df463edb597007186688ee557c18f1ac18 (patch) | |
| tree | 006045dd73f4049d98e20a8c0830406490445f08 /idatui/edit_ctl.py | |
| parent | launch: the sweep could delete the file it was asked to open (diff) | |
| download | ida-tui-2b0ae8df463edb597007186688ee557c18f1ac18.tar.gz ida-tui-2b0ae8df463edb597007186688ee557c18f1ac18.tar.xz ida-tui-2b0ae8df463edb597007186688ee557c18f1ac18.zip | |
diag: somewhere for swallowed errors to go
A TUI must not die because one background load failed, so this codebase catches
broadly -- ~50 `except Exception` sites, two dozen resolving to `pass`. Right
policy, one bad consequence: with 44 `@work(thread=True)` workers, a failure in
a background load leaves no trace whatsoever. The view stays empty and there is
nothing to read afterwards, because the app owns the screen.
kittygfx already solved this for itself with $IDATUI_KITTY_LOG. idatui/diag.py
is the same idea for everything else: $IDATUI_LOG writes every swallowed error
plus its traceback to a file, and the last 50 are kept in memory regardless so a
driver can ask a live app what went wrong. Unset, it costs an environ lookup.
Wired in where losing the error changes a DECISION rather than just a pixel:
* rename: a resolve() that throws renames as DATA instead of as a function.
* name: a function_of() that throws means we never learn the address is a
function start, so the index keeps the old name and every readback says the
rename didn't happen.
* retype: a resolve() that throws retypes the ENCLOSING function instead.
* decompile: a failed full-body fetch silently returns CLIPPED pseudocode.
* trail: a failed decomp_map stops the pseudocode being painted, silently.
Deliberately NOT wired into the query_one guards -- a modal owning the screen is
normal and constant, and logging it would bury the real entries in noise.
New RPC verb `diag {n?, clear?}`, documented in docs/RPC.md: the answer to "the
verb reported success and the pane shows nothing".
Also a flake, same shape as the others: follow_xrefs waited on the nav depth but
asserted on _cur, and a follow pushes the source entry BEFORE opening the
target -- so the check could run in between and see the function it jumped
from. About one run in ten. It waits on the postcondition it asserts now; three
clean full runs since.
833 checks; --fast is 344 in 3.5s.
Diffstat (limited to 'idatui/edit_ctl.py')
| -rw-r--r-- | idatui/edit_ctl.py | 21 |
1 files changed, 18 insertions, 3 deletions
diff --git a/idatui/edit_ctl.py b/idatui/edit_ctl.py index 7803a7a..54f4d89 100644 --- a/idatui/edit_ctl.py +++ b/idatui/edit_ctl.py @@ -26,6 +26,7 @@ from typing import TYPE_CHECKING from textual.widgets import DataTable +from . import diag from .errors import IDAToolError if TYPE_CHECKING: # pragma: no cover @@ -210,7 +211,11 @@ class EditController: resolved: int | None = None try: resolved = prog.resolve(old) - except Exception: # noqa: BLE001 + except Exception as e: # noqa: BLE001 + # Not cosmetic: an unresolved name is renamed as DATA instead of as + # a function, so a lookup that failed for a transport reason quietly + # applies the wrong kind of edit. + diag.note(f"rename: resolve({old!r})", e) resolved = None if resolved is not None: fn = prog.function_of(resolved) @@ -291,7 +296,11 @@ class EditController: # it already did. try: fn = app.program.function_of(addr) - except Exception: # noqa: BLE001 + except Exception as e: # noqa: BLE001 + # If this throws we don't learn the address IS a function start, so + # the index keeps the old name and every readback says the rename + # never happened. + diag.note(f"name: function_of({addr:#x})", e) fn = None is_func_start = fn is not None and fn.addr == addr lm = app.program.listing(addr) @@ -403,7 +412,10 @@ class EditController: if kind is None and app._looks_like_symbol(word): try: tgt = app.program.resolve(word) - except Exception: # noqa: BLE001 + except Exception as e: # noqa: BLE001 + # Falls through to case (3), which retypes the enclosing + # function -- a different edit from the one asked for. + diag.note(f"retype: resolve({word!r})", e) tgt = None if tgt is not None: tft = app.program.func_types(tgt) @@ -495,6 +507,7 @@ class EditController: try: app.program.make_data(ea, type_decl) except Exception as e: # noqa: BLE001 + diag.note(f"make_data({ea:#x}, {type_decl!r})", e) app.call_from_thread(app._status, f"make data: {e}") return app.program.bump_items() @@ -555,6 +568,7 @@ class EditController: app.call_from_thread(app._status, f"format: {e.message}", True) return except Exception as e: # noqa: BLE001 -- surface transport failures too + diag.note(f"op_format({where}, {ea:#x})", e) app.call_from_thread(app._status, f"format: {e}", True) return text = " ".join((r.get("text") or "").split()) @@ -690,6 +704,7 @@ class EditController: anchor.refresh_functions = True app.program.undefine(ea) except Exception as e: # noqa: BLE001 -- surface soft/hard tool errors + diag.note(f"edit_item({kind}, {ea:#x})", e) app.call_from_thread(app._status, f"{kind}: {e}") return # Structure changed everywhere: drop all item/function/decomp caches. |
