Skip to content

Commit 8a1d9ae

Browse files
authored
Merge pull request #104 from RevEngAI/feat-PLU-245-use-thread-safe-decorators
feat(PLU-245): use thread safe decorators
2 parents f51ac2c + 06126b4 commit 8a1d9ae

32 files changed

Lines changed: 336 additions & 561 deletions

reai_toolkit/app/components/dialogs/auto_unstrip_dialog.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from typing import Optional
2-
3-
import ida_kernwin
41
import ida_name
2+
3+
from libbs.decompilers.ida.compat import execute_read
54
from revengai import MatchedFunctionSuggestion
65

76
from reai_toolkit.app.components.dialogs.base_dialog import DialogBase
@@ -20,16 +19,12 @@
2019
)
2120

2221

23-
def get_safe_name(ea: int) -> Optional[str]:
24-
name = None
25-
26-
def _do():
27-
nonlocal name
28-
name = ida_name.get_name(ea=ea)
29-
if name is None:
30-
name = "<unnamed>"
22+
@execute_read
23+
def get_function_name(ea: int) -> str | None:
24+
name: str | None = ida_name.get_name(ea=ea)
25+
if name is None:
26+
name = "<unnamed>"
3127

32-
ida_kernwin.execute_sync(_do, ida_kernwin.MFF_FAST)
3328
return name
3429

3530

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

9388
# 3. current name cell
94-
current_name = get_safe_name(m.function_vaddr) or "<unnamed>"
89+
current_name = get_function_name(m.function_vaddr) or "<unnamed>"
9590
cur_item = QtWidgets.QTableWidgetItem(current_name)
9691
cur_item.setFlags(flags)
9792
cur_item.setToolTip(current_name)

reai_toolkit/app/components/tabs/ai_decomp_tab.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from loguru import logger
33

44
import ida_kernwin as kw
5+
from libbs.decompilers.ida.compat import execute_ui
56

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

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

8788
# --- public API ------------------------------------------------
89+
@execute_ui
8890
def update_view_content(self, code: str) -> None:
89-
def _apply() -> None:
90-
if not self._editor:
91-
return
91+
if not self._editor:
92+
return
9293

93-
self._editor.blockSignals(True)
94-
try:
95-
self._editor.setPlainText(code)
96-
finally:
97-
self._editor.blockSignals(False)
98-
99-
# ensure we’re on IDA’s UI thread
94+
self._editor.blockSignals(True)
10095
try:
101-
kw.execute_sync(_apply, kw.MFF_FAST)
102-
except Exception:
103-
_apply() # best-effort fallback
96+
self._editor.setPlainText(code)
97+
finally:
98+
self._editor.blockSignals(False)
99+
100+
104101

105102
def clear(self) -> None:
106103
self.update_view_content("")

reai_toolkit/app/coordinator.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ida_funcs
22
import ida_kernwin
3+
from revengai import FunctionMapping
34

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

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

113-
def redirect_function_portal(self):
114-
func_map = self.app.analysis_sync_service.safe_get_function_mapping_local()
115-
current_ea = ida_kernwin.get_screen_ea()
116-
current_func = ida_funcs.get_func(current_ea)
117-
function_id = func_map.inverse_function_map.get(
118-
str(current_func.start_ea), None
119-
)
120-
portal_url = self.app.config_service.portal_url + f"/function/{function_id}"
121-
QtGui.QDesktopServices.openUrl(QtCore.QUrl(portal_url))
115+
def redirect_function_portal(self) -> None:
116+
func_map: FunctionMapping | None = self.app.analysis_sync_service.netstore_service.get_function_mapping()
117+
current_ea: int = ida_kernwin.get_screen_ea()
118+
current_func: ida_funcs.func_t | None = ida_funcs.get_func(current_ea)
119+
if func_map and current_func:
120+
function_id: int | None = func_map.inverse_function_map.get(
121+
str(current_func.start_ea), None
122+
)
123+
portal_url: str = self.app.config_service.portal_url + f"/function/{function_id}"
124+
QtGui.QDesktopServices.openUrl(QtCore.QUrl(portal_url))
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
from typing import TYPE_CHECKING
22

33
from reai_toolkit.app.coordinators.base_coordinator import BaseCoordinator
4+
from libbs.decompilers.ida.compat import execute_ui
5+
46

57
if TYPE_CHECKING:
68
from reai_toolkit.app.app import App
79
from reai_toolkit.app.factory import DialogFactory
810

911

1012
class AboutCoordinator(BaseCoordinator):
11-
def __init__(self, *, app: "App", factory: "DialogFactory", log):
13+
def __init__(self, *, app: "App", factory: "DialogFactory", log) -> None:
1214
super().__init__(app=app, factory=factory, log=log)
1315

16+
@execute_ui
1417
def run_dialog(self) -> None:
15-
self.safe_ui_exec(lambda: self.factory.about_dialog().open_modal())
18+
self.factory.about_dialog().open_modal()

reai_toolkit/app/coordinators/ai_decomp_coordinator.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from logging import Logger
22

3-
from loguru import logger
4-
import ida_kernwin
3+
from libbs.decompilers.ida.compat import execute_ui
4+
55
from revengai.models.get_ai_decompilation_task import GetAiDecompilationTask
66

77
from reai_toolkit.app.app import App
@@ -38,6 +38,7 @@ def disable_function_tracking(self) -> None:
3838
self._decomp_hooks.unhook()
3939
self._decomp_hooks = None
4040

41+
@execute_ui
4142
def run_dialog(self) -> None:
4243
self._decomp_view = self.factory.ai_decomp(on_closed=self._on_pane_closed)
4344
self._decomp_view.Create(self._decomp_view.TITLE)
@@ -58,7 +59,7 @@ def _on_decomp_complete(
5859
) -> None:
5960
if response.success is False:
6061
if response.error_message:
61-
self.safe_error(message=response.error_message)
62+
self.show_error_dialog(message=response.error_message)
6263

6364
if self._decomp_view:
6465
self._decomp_view.update_view_content(
@@ -72,7 +73,7 @@ def _on_decomp_complete(
7273

7374
# Open a dialog to show the decompilation result
7475
if self._decomp_view is None:
75-
ida_kernwin.execute_sync(self.run_dialog, ida_kernwin.MFF_FAST)
76+
self.run_dialog()
7677

7778
if response.data is None or response.data.decompilation is None:
7879
return
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import TYPE_CHECKING
22

33
from reai_toolkit.app.coordinators.base_coordinator import BaseCoordinator
4+
from libbs.decompilers.ida.compat import execute_ui
45

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

15+
@execute_ui
1416
def run_dialog(self) -> None:
15-
self.safe_ui_exec(lambda: self.factory.auth().open_modal())
16-
# After dialog closes, update auth status
17-
self.safe_refresh()
17+
self.factory.auth().open_modal()
18+
self.refresh_disassembly_view()
1819

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

reai_toolkit/app/coordinators/auto_unstrip_coordinator.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,23 @@ def __init__(
3535

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

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

47-
pass
48-
4947
def _open_auto_unstrip_dialog(self) -> None:
5048
self.factory.auto_unstrip(response=self.last_response.data).open_modal()
5149

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

5553
if not response.success:
56-
self.safe_error(message=response.error_message)
54+
self.show_error_dialog(message=response.error_message)
5755
return
5856

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

7775
ida_kernwin.execute_ui_requests([self._open_auto_unstrip_dialog])
7876

79-
self.safe_refresh()
77+
self.refresh_disassembly_view()

reai_toolkit/app/coordinators/base_coordinator.py

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from abc import ABC, abstractmethod
22
from logging import Logger
3-
from typing import TYPE_CHECKING, Any, Callable
3+
from typing import TYPE_CHECKING
44

5+
from libbs.decompilers.ida.compat import execute_ui
56
import ida_kernwin
67

78
if TYPE_CHECKING:
@@ -13,7 +14,6 @@ class BaseCoordinator(ABC):
1314
"""
1415
Base class providing:
1516
- Common references (app, factory, log)
16-
- Thread-safe UI helpers (safe_* methods)
1717
- Authentication helpers (require_auth)
1818
"""
1919

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

25-
# ======================================================
26-
# IDA-safe helpers — executed on the UI thread safely
27-
# ======================================================
28-
29-
def safe_ui_exec(self, fn: Callable[[], Any], fast: bool = True) -> Any:
30-
"""Run a function safely on IDA’s main UI thread."""
31-
try:
32-
flags = ida_kernwin.MFF_FAST if fast else ida_kernwin.MFF_NOWAIT
33-
return ida_kernwin.execute_sync(fn, flags)
34-
except Exception as e:
35-
print(f"[Coordinator] safe_ui_exec failed: {e}")
36-
self.log.error(f"[Coordinator] safe_ui_exec failed: {e}")
37-
return None
38-
39-
def safe_info(self, msg: str) -> None:
25+
@execute_ui
26+
def show_info_dialog(self, msg: str) -> None:
4027
"""Display an info dialog safely."""
28+
try:
29+
ida_kernwin.info(msg)
30+
except Exception:
31+
self.log.warning(f"Failed to show info: {msg}")
4132

42-
def _do():
43-
try:
44-
ida_kernwin.info(msg)
45-
except Exception:
46-
self.log.warning(f"Failed to show info: {msg}")
47-
48-
self.safe_ui_exec(_do)
49-
50-
def safe_refresh(self) -> None:
51-
"""Safely refresh the disassembly view."""
52-
53-
def _do():
54-
try:
55-
ida_kernwin.refresh_idaview_anyway()
56-
except Exception:
57-
self.log.warning("Failed to refresh IDA view.")
5833

59-
self.safe_ui_exec(_do)
34+
@execute_ui
35+
def refresh_disassembly_view(self) -> None:
36+
try:
37+
ida_kernwin.refresh_idaview_anyway()
38+
except Exception:
39+
self.log.warning("Failed to refresh IDA view.")
6040

61-
def safe_error(self, message: str) -> None:
41+
@execute_ui
42+
def show_error_dialog(self, message: str) -> None:
6243
"""Show an error dialog safely."""
44+
try:
45+
self.factory.error_dialog(message=message).open_modal()
46+
except Exception:
47+
self.log.warning(f"Failed to show error dialog: {message}")
6348

64-
def _do():
65-
try:
66-
self.factory.error_dialog(message=message).open_modal()
67-
except Exception:
68-
self.log.warning(f"Failed to show error dialog: {message}")
69-
70-
self.safe_ui_exec(_do)
7149

7250
# ======================================================
7351
# Authentication helper

reai_toolkit/app/coordinators/create_analysis_coordinator.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import TYPE_CHECKING
22

33
from revengai import AnalysisCreateResponse
4+
from libbs.decompilers.ida.compat import execute_ui
45

56
from reai_toolkit.app.components.dialogs.analyse_dialog import AnalyseDialog
67
from reai_toolkit.app.coordinators.base_coordinator import BaseCoordinator
@@ -29,26 +30,28 @@ def __init__(
2930

3031
self.analysis_status_coord = analysis_status_coord
3132

33+
@execute_ui
3234
def run_dialog(self) -> None:
3335
dialog: AnalyseDialog = self.factory.create_analysis(service_callback=self._on_complete)
34-
# only call open_modal safely on the UI thread
35-
self.safe_ui_exec(lambda: dialog.open_modal())
36-
self.safe_refresh()
36+
dialog.open_modal()
37+
self.refresh_disassembly_view()
3738

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

4142
def _on_complete(self, service_response: GenericApiReturn) -> None:
4243
"""Handle completion of analysis creation."""
4344
if service_response.success and isinstance(service_response.data, AnalysisCreateResponse):
44-
self.safe_info(
45+
self.show_info_dialog(
4546
msg="Analysis created successfully, please wait while it is processed."
4647
)
48+
data: AnalysisCreateResponse = service_response.data
49+
4750
# Should have analysis id - refresh to update menu options
48-
self.safe_refresh()
51+
self.refresh_disassembly_view()
4952

5053
# Call Sync Task to poll status
51-
self.analysis_status_coord.poll_status(analysis_id=service_response.data.analysis_id)
54+
self.analysis_status_coord.poll_status(analysis_id=data.analysis_id)
5255
else:
5356
error_message: str = service_response.error_message or "Unknown error"
54-
self.safe_error(error_message)
57+
self.show_error_dialog(message=error_message)

reai_toolkit/app/coordinators/detach_coordinator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def _detach_analysis(self) -> None:
2929

3030
self.app.netstore_service.clear_all_ns()
3131

32-
self.safe_info(msg="Analysis detached successfully.")
33-
self.safe_refresh()
32+
self.show_info_dialog(msg="Analysis detached successfully.")
33+
self.refresh_disassembly_view()
3434

3535
def run_dialog(self) -> None:
3636
msg = QtWidgets.QMessageBox()

0 commit comments

Comments
 (0)