Skip to content

Commit 370f732

Browse files
committed
Scan non-blockingly
1 parent aabeec5 commit 370f732

File tree

5 files changed

+119
-21
lines changed

5 files changed

+119
-21
lines changed

core/concurrency/subprocess.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from concurrent.futures import ProcessPoolExecutor, Future
2+
from functools import partial
3+
from typing import Callable, Optional, Any
4+
5+
6+
# Used for heavy calculations where a thread is not enough and blocking the UI is not feasible.
7+
class Subprocess:
8+
def __init__(self):
9+
self.executor = ProcessPoolExecutor()
10+
self.future: Optional[Future] = None
11+
12+
def submit(
13+
self,
14+
func: Callable,
15+
*args,
16+
on_done: Optional[Callable[[Any], None]] = None,
17+
on_error: Optional[Callable[[Exception], None]] = None,
18+
**kwargs
19+
):
20+
if self.future and not self.future.done():
21+
self.future.cancel()
22+
23+
self.future = self.executor.submit(func, *args, **kwargs)
24+
self.future.add_done_callback(
25+
partial(self._handle_future, on_done=on_done, on_error=on_error)
26+
)
27+
28+
def _handle_future(self, future: Future, on_done: Optional[Callable], on_error: Optional[Callable]):
29+
self._process_future(future, on_done, on_error)
30+
31+
def _process_future(self, future: Future, on_done: Optional[Callable], on_error: Optional[Callable]):
32+
try:
33+
result = future.result()
34+
except Exception as e:
35+
if on_error:
36+
on_error(e)
37+
else:
38+
if on_done:
39+
on_done(result)
40+
41+
def shutdown(self, wait: bool = False, cancel_futures: bool = True):
42+
if self.future and not self.future.done():
43+
self.future.cancel()
44+
self.executor.shutdown(wait=wait, cancel_futures=cancel_futures)

core/concurrency/worker.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import sys
2+
import traceback
3+
4+
from PySide6.QtCore import QObject, Signal, QRunnable, Slot
5+
6+
7+
# Used for lighter calculations where a thread is enough.
8+
# Briefly blocking the UI is acceptable.
9+
class WorkerSignals(QObject):
10+
finished = Signal()
11+
error = Signal(tuple)
12+
result = Signal(object)
13+
14+
15+
class Worker(QRunnable):
16+
def __init__(self, fn, *args, **kwargs):
17+
super().__init__()
18+
self.fn = fn
19+
self.args = args
20+
self.kwargs = kwargs
21+
self.signals = WorkerSignals()
22+
23+
@Slot()
24+
def run(self):
25+
try:
26+
result = self.fn(*self.args, **self.kwargs)
27+
except Exception:
28+
traceback.print_exc()
29+
exctype, value = sys.exc_info()[:2]
30+
self.signals.error.emit((exctype, value, traceback.format_exc()))
31+
else:
32+
self.signals.result.emit(result)
33+
finally:
34+
self.signals.finished.emit()

core/scan.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,7 @@ def disasm(self, sec_bytes: bytes, base_va: int, section_offset: int, syscall_ma
345345
for ins in result.get("context", [])[-12:]:
346346
mnemonic = str(ins['mnemonic'])
347347
arrow = ">>> " if mnemonic.startswith("svc") else " "
348-
info(
349-
f" {arrow}0x{ins['address']:x} {mnemonic} {ins['op_str']}", "debug")
348+
debug(f" {arrow}0x{ins['address']:x} {mnemonic} {ins['op_str']}")
350349
debug("")
351350
i += 4
352351
return results

img/spinner.gif

Loading

ui/tabs/scanner_tab.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
import json
2+
13
from PySide6.QtWidgets import (
2-
QWidget, QVBoxLayout, QPushButton, QLabel, QTreeWidget, QTreeWidgetItem, QFileDialog, QHBoxLayout
4+
QWidget, QVBoxLayout, QPushButton, QLabel,
5+
QTreeWidget, QTreeWidgetItem, QFileDialog, QHBoxLayout
36
)
47

8+
from core.concurrency.subprocess import Subprocess
59
from core.scan import MachOScanner
610
from core.utils import is_macho_file
711

812

13+
def run_scan_process(file_path: str, verbose: bool = True):
14+
scanner = MachOScanner()
15+
results = scanner.analyze(file_path, verbose=verbose, out_path=None)
16+
return json.dumps(results)
17+
18+
919
class ScannerTab(QWidget):
1020
def __init__(self):
1121
super().__init__()
@@ -25,6 +35,8 @@ def __init__(self):
2535
self.tree.setColumnWidth(0, 220)
2636
self.tree.setAlternatingRowColors(True)
2737

38+
# todo add spinner
39+
2840
self.scan_button = QPushButton("Run Scan")
2941
self.scan_button.clicked.connect(self._on_scan_clicked)
3042
self.scan_button.setEnabled(False)
@@ -36,7 +48,11 @@ def __init__(self):
3648
self.setAcceptDrops(True)
3749

3850
self.file_path = None
39-
self.scanner = MachOScanner()
51+
self.worker = Subprocess()
52+
53+
def _toggle_button_states(self, on):
54+
self.scan_button.setEnabled(on)
55+
self.open_button.setEnabled(on)
4056

4157
def dragEnterEvent(self, event):
4258
if event.mimeData().hasUrls():
@@ -49,6 +65,7 @@ def dragEnterEvent(self, event):
4965
event.ignore()
5066

5167
def dropEvent(self, event):
68+
# todo forbid drop when already scanning
5269
if event.mimeData().hasUrls():
5370
file_path = event.mimeData().urls()[0].toLocalFile()
5471
if file_path and is_macho_file(file_path):
@@ -80,22 +97,26 @@ def _run_scan(self):
8097
if not self.file_path:
8198
return
8299

83-
self.scan_button.setEnabled(False)
100+
self._toggle_button_states(False)
84101
self.tree.clear()
85102
status_root = QTreeWidgetItem(["Status", "Scanning..."])
86103
self.tree.addTopLevelItem(status_root)
87104

88-
# TODO: This is a blocking call, UI will freeze here for large files!
89-
results = self.scanner.analyze(self.file_path, verbose=True)
90-
91-
print(results)
105+
self.worker.submit(
106+
run_scan_process,
107+
self.file_path,
108+
True,
109+
on_done=self._display_results,
110+
on_error=self._display_error
111+
)
92112

113+
def _display_results(self, jsonResults):
93114
self.tree.clear()
94115
root = QTreeWidgetItem(["Scan Results", self.file_path])
95116
self.tree.addTopLevelItem(root)
96117

97-
for i, result in enumerate(results, 1):
98-
result_item = QTreeWidgetItem([f"Binary"])
118+
for i, result in enumerate(json.loads(jsonResults), 1):
119+
result_item = QTreeWidgetItem(["Binary"])
99120
root.addChild(result_item)
100121

101122
for category, data in result.items():
@@ -109,15 +130,6 @@ def _run_scan(self):
109130
f"section: {s.get('section', '')}, offset: {s.get('offset', '')}"
110131
])
111132
cat_item.addChild(syscall_item)
112-
113-
# context = s.get("context", [])
114-
# for ins in context[-12:]: # last 12 instructions
115-
# ctx_item = QTreeWidgetItem([
116-
# f"0x{ins['address']:x} {ins['mnemonic']}",
117-
# ins.get("op_str", "")
118-
# ])
119-
# syscall_item.addChild(ctx_item)
120-
121133
elif isinstance(data, dict):
122134
for k, v in data.items():
123135
sub_item = QTreeWidgetItem([str(k), str(v)])
@@ -131,4 +143,13 @@ def _run_scan(self):
131143

132144
root.setExpanded(True)
133145
self.tree.expandAll()
134-
self.scan_button.setEnabled(True)
146+
self._toggle_button_states(True)
147+
148+
def _display_error(self, error):
149+
self.tree.clear()
150+
self.tree.addTopLevelItem(QTreeWidgetItem(["Error", str(error)]))
151+
self._toggle_button_states(True)
152+
153+
def closeEvent(self, event):
154+
self.worker.shutdown(wait=False, cancel_futures=True)
155+
super().closeEvent(event)

0 commit comments

Comments
 (0)