Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions reai_toolkit/app/components/dialogs/auto_unstrip_dialog.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Optional

import ida_kernwin
import ida_name

from libbs.decompilers.ida.compat import execute_read
from revengai import MatchedFunctionSuggestion

from reai_toolkit.app.components.dialogs.base_dialog import DialogBase
Expand All @@ -20,16 +19,12 @@
)


def get_safe_name(ea: int) -> Optional[str]:
name = None

def _do():
nonlocal name
name = ida_name.get_name(ea=ea)
if name is None:
name = "<unnamed>"
@execute_read
def get_function_name(ea: int) -> str | None:
name: str | None = ida_name.get_name(ea=ea)
if name is None:
name = "<unnamed>"

ida_kernwin.execute_sync(_do, ida_kernwin.MFF_FAST)
return name


Expand Down Expand Up @@ -91,7 +86,7 @@ def _populate_table(self, matches: list[MatchedFunctionSuggestion]) -> None:
table.setItem(row, 0, addr_item)

# 3. current name cell
current_name = get_safe_name(m.function_vaddr) or "<unnamed>"
current_name = get_function_name(m.function_vaddr) or "<unnamed>"
cur_item = QtWidgets.QTableWidgetItem(current_name)
cur_item.setFlags(flags)
cur_item.setToolTip(current_name)
Expand Down
23 changes: 10 additions & 13 deletions reai_toolkit/app/components/tabs/ai_decomp_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from loguru import logger

import ida_kernwin as kw
from libbs.decompilers.ida.compat import execute_ui

from reai_toolkit.app.core.qt_compat import QtCore, QtGui, QtWidgets

Expand Down Expand Up @@ -85,22 +86,18 @@ def OnClose(self, form) -> None:
self._parent_window = None

# --- public API ------------------------------------------------
@execute_ui
def update_view_content(self, code: str) -> None:
def _apply() -> None:
if not self._editor:
return
if not self._editor:
return

self._editor.blockSignals(True)
try:
self._editor.setPlainText(code)
finally:
self._editor.blockSignals(False)

# ensure we’re on IDA’s UI thread
self._editor.blockSignals(True)
try:
kw.execute_sync(_apply, kw.MFF_FAST)
except Exception:
_apply() # best-effort fallback
self._editor.setPlainText(code)
finally:
self._editor.blockSignals(False)



def clear(self) -> None:
self.update_view_content("")
Expand Down
29 changes: 16 additions & 13 deletions reai_toolkit/app/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ida_funcs
import ida_kernwin
from revengai import FunctionMapping

from reai_toolkit.app.coordinators.about_coordinator import AboutCoordinator
from reai_toolkit.app.coordinators.ai_decomp_coordinator import AiDecompCoordinator
Expand Down Expand Up @@ -105,17 +106,19 @@ def run_dialog(self):
"""Run all necessary dialogs at startup."""
pass

def redirect_analysis_portal(self):
binary_id = self.app.netstore_service.get_binary_id()
portal_url = self.app.config_service.portal_url + f"/analyses/{binary_id}"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(portal_url))
def redirect_analysis_portal(self) -> None:
binary_id: int | None = self.app.netstore_service.get_binary_id()
if binary_id is not None:
portal_url: str = self.app.config_service.portal_url + f"/analyses/{binary_id}"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(portal_url))

def redirect_function_portal(self):
func_map = self.app.analysis_sync_service.safe_get_function_mapping_local()
current_ea = ida_kernwin.get_screen_ea()
current_func = ida_funcs.get_func(current_ea)
function_id = func_map.inverse_function_map.get(
str(current_func.start_ea), None
)
portal_url = self.app.config_service.portal_url + f"/function/{function_id}"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(portal_url))
def redirect_function_portal(self) -> None:
func_map: FunctionMapping | None = self.app.analysis_sync_service.netstore_service.get_function_mapping()
current_ea: int = ida_kernwin.get_screen_ea()
current_func: ida_funcs.func_t | None = ida_funcs.get_func(current_ea)
if func_map and current_func:
function_id: int | None = func_map.inverse_function_map.get(
str(current_func.start_ea), None
)
portal_url: str = self.app.config_service.portal_url + f"/function/{function_id}"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(portal_url))
7 changes: 5 additions & 2 deletions reai_toolkit/app/coordinators/about_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from typing import TYPE_CHECKING

from reai_toolkit.app.coordinators.base_coordinator import BaseCoordinator
from libbs.decompilers.ida.compat import execute_ui


if TYPE_CHECKING:
from reai_toolkit.app.app import App
from reai_toolkit.app.factory import DialogFactory


class AboutCoordinator(BaseCoordinator):
def __init__(self, *, app: "App", factory: "DialogFactory", log):
def __init__(self, *, app: "App", factory: "DialogFactory", log) -> None:
super().__init__(app=app, factory=factory, log=log)

@execute_ui
def run_dialog(self) -> None:
self.safe_ui_exec(lambda: self.factory.about_dialog().open_modal())
self.factory.about_dialog().open_modal()
9 changes: 5 additions & 4 deletions reai_toolkit/app/coordinators/ai_decomp_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from logging import Logger

from loguru import logger
import ida_kernwin
from libbs.decompilers.ida.compat import execute_ui

from revengai.models.get_ai_decompilation_task import GetAiDecompilationTask

from reai_toolkit.app.app import App
Expand Down Expand Up @@ -38,6 +38,7 @@ def disable_function_tracking(self) -> None:
self._decomp_hooks.unhook()
self._decomp_hooks = None

@execute_ui
def run_dialog(self) -> None:
self._decomp_view = self.factory.ai_decomp(on_closed=self._on_pane_closed)
self._decomp_view.Create(self._decomp_view.TITLE)
Expand All @@ -58,7 +59,7 @@ def _on_decomp_complete(
) -> None:
if response.success is False:
if response.error_message:
self.safe_error(message=response.error_message)
self.show_error_dialog(message=response.error_message)

if self._decomp_view:
self._decomp_view.update_view_content(
Expand All @@ -72,7 +73,7 @@ def _on_decomp_complete(

# Open a dialog to show the decompilation result
if self._decomp_view is None:
ida_kernwin.execute_sync(self.run_dialog, ida_kernwin.MFF_FAST)
self.run_dialog()

if response.data is None or response.data.decompilation is None:
return
Expand Down
7 changes: 4 additions & 3 deletions reai_toolkit/app/coordinators/auth_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING

from reai_toolkit.app.coordinators.base_coordinator import BaseCoordinator
from libbs.decompilers.ida.compat import execute_ui

if TYPE_CHECKING:
from reai_toolkit.app.app import App
Expand All @@ -11,10 +12,10 @@ class AuthCoordinator(BaseCoordinator):
def __init__(self, *, app: "App", factory: "DialogFactory", log):
super().__init__(app=app, factory=factory, log=log)

@execute_ui
def run_dialog(self) -> None:
self.safe_ui_exec(lambda: self.factory.auth().open_modal())
# After dialog closes, update auth status
self.safe_refresh()
self.factory.auth().open_modal()
self.refresh_disassembly_view()

def is_authed(self) -> bool:
return self.app.auth_service.is_authenticated()
10 changes: 4 additions & 6 deletions reai_toolkit/app/coordinators/auto_unstrip_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,23 @@ def __init__(

def run_dialog(self) -> None:
if self.auto_unstrip_service.is_worker_running():
self.safe_info(msg="Auto-unstrip is already running.")
self.show_info_dialog(msg="Auto-unstrip is already running.")
return

if self.last_response:
self._open_auto_unstrip_dialog()
return
self.safe_info(msg="Starting auto-unstrip process, may take a while.")
self.show_info_dialog(msg="Starting auto-unstrip process, may take a while.")
self.auto_unstrip_service.start_unstrip_polling(callback=self._on_complete)

pass

def _open_auto_unstrip_dialog(self) -> None:
self.factory.auto_unstrip(response=self.last_response.data).open_modal()

def _on_complete(self, response: GenericApiReturn[AutoUnstripResponse]) -> None:
print("Auto-unstrip process completed.")

if not response.success:
self.safe_error(message=response.error_message)
self.show_error_dialog(message=response.error_message)
return

rename_list = []
Expand All @@ -76,4 +74,4 @@ def _on_complete(self, response: GenericApiReturn[AutoUnstripResponse]) -> None:

ida_kernwin.execute_ui_requests([self._open_auto_unstrip_dialog])

self.safe_refresh()
self.refresh_disassembly_view()
62 changes: 20 additions & 42 deletions reai_toolkit/app/coordinators/base_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from abc import ABC, abstractmethod
from logging import Logger
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING

from libbs.decompilers.ida.compat import execute_ui
import ida_kernwin

if TYPE_CHECKING:
Expand All @@ -13,7 +14,6 @@ class BaseCoordinator(ABC):
"""
Base class providing:
- Common references (app, factory, log)
- Thread-safe UI helpers (safe_* methods)
- Authentication helpers (require_auth)
"""

Expand All @@ -22,52 +22,30 @@ def __init__(self, *, app: "App", factory: "DialogFactory", log: Logger) -> None
self.factory: "DialogFactory" = factory
self.log: Logger = log

# ======================================================
# IDA-safe helpers — executed on the UI thread safely
# ======================================================

def safe_ui_exec(self, fn: Callable[[], Any], fast: bool = True) -> Any:
"""Run a function safely on IDA’s main UI thread."""
try:
flags = ida_kernwin.MFF_FAST if fast else ida_kernwin.MFF_NOWAIT
return ida_kernwin.execute_sync(fn, flags)
except Exception as e:
print(f"[Coordinator] safe_ui_exec failed: {e}")
self.log.error(f"[Coordinator] safe_ui_exec failed: {e}")
return None

def safe_info(self, msg: str) -> None:
@execute_ui
def show_info_dialog(self, msg: str) -> None:
"""Display an info dialog safely."""
try:
ida_kernwin.info(msg)
except Exception:
self.log.warning(f"Failed to show info: {msg}")

def _do():
try:
ida_kernwin.info(msg)
except Exception:
self.log.warning(f"Failed to show info: {msg}")

self.safe_ui_exec(_do)

def safe_refresh(self) -> None:
"""Safely refresh the disassembly view."""

def _do():
try:
ida_kernwin.refresh_idaview_anyway()
except Exception:
self.log.warning("Failed to refresh IDA view.")

self.safe_ui_exec(_do)
@execute_ui
def refresh_disassembly_view(self) -> None:
try:
ida_kernwin.refresh_idaview_anyway()
except Exception:
self.log.warning("Failed to refresh IDA view.")

def safe_error(self, message: str) -> None:
@execute_ui
def show_error_dialog(self, message: str) -> None:
"""Show an error dialog safely."""
try:
self.factory.error_dialog(message=message).open_modal()
except Exception:
self.log.warning(f"Failed to show error dialog: {message}")

def _do():
try:
self.factory.error_dialog(message=message).open_modal()
except Exception:
self.log.warning(f"Failed to show error dialog: {message}")

self.safe_ui_exec(_do)

# ======================================================
# Authentication helper
Expand Down
17 changes: 10 additions & 7 deletions reai_toolkit/app/coordinators/create_analysis_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING

from revengai import AnalysisCreateResponse
from libbs.decompilers.ida.compat import execute_ui

from reai_toolkit.app.components.dialogs.analyse_dialog import AnalyseDialog
from reai_toolkit.app.coordinators.base_coordinator import BaseCoordinator
Expand Down Expand Up @@ -29,26 +30,28 @@ def __init__(

self.analysis_status_coord = analysis_status_coord

@execute_ui
def run_dialog(self) -> None:
dialog: AnalyseDialog = self.factory.create_analysis(service_callback=self._on_complete)
# only call open_modal safely on the UI thread
self.safe_ui_exec(lambda: dialog.open_modal())
self.safe_refresh()
dialog.open_modal()
self.refresh_disassembly_view()

def is_authed(self) -> bool:
return self.app.auth_service.is_authenticated()

def _on_complete(self, service_response: GenericApiReturn) -> None:
"""Handle completion of analysis creation."""
if service_response.success and isinstance(service_response.data, AnalysisCreateResponse):
self.safe_info(
self.show_info_dialog(
msg="Analysis created successfully, please wait while it is processed."
)
data: AnalysisCreateResponse = service_response.data

# Should have analysis id - refresh to update menu options
self.safe_refresh()
self.refresh_disassembly_view()

# Call Sync Task to poll status
self.analysis_status_coord.poll_status(analysis_id=service_response.data.analysis_id)
self.analysis_status_coord.poll_status(analysis_id=data.analysis_id)
else:
error_message: str = service_response.error_message or "Unknown error"
self.safe_error(error_message)
self.show_error_dialog(message=error_message)
4 changes: 2 additions & 2 deletions reai_toolkit/app/coordinators/detach_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def _detach_analysis(self) -> None:

self.app.netstore_service.clear_all_ns()

self.safe_info(msg="Analysis detached successfully.")
self.safe_refresh()
self.show_info_dialog(msg="Analysis detached successfully.")
self.refresh_disassembly_view()

def run_dialog(self) -> None:
msg = QtWidgets.QMessageBox()
Expand Down
Loading