declib 3.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. declib/__init__.py +9 -0
  2. declib/__main__.py +190 -0
  3. declib/api/__init__.py +13 -0
  4. declib/api/artifact_dict.py +153 -0
  5. declib/api/artifact_lifter.py +161 -0
  6. declib/api/decompiler_client.py +1219 -0
  7. declib/api/decompiler_interface.py +1261 -0
  8. declib/api/decompiler_server.py +782 -0
  9. declib/api/server_registry.py +171 -0
  10. declib/api/type_definition_parser.py +201 -0
  11. declib/api/type_parser.py +409 -0
  12. declib/api/utils.py +31 -0
  13. declib/artifacts/__init__.py +93 -0
  14. declib/artifacts/artifact.py +311 -0
  15. declib/artifacts/comment.py +49 -0
  16. declib/artifacts/context.py +61 -0
  17. declib/artifacts/decompilation.py +35 -0
  18. declib/artifacts/enum.py +53 -0
  19. declib/artifacts/formatting.py +27 -0
  20. declib/artifacts/func.py +433 -0
  21. declib/artifacts/global_variable.py +31 -0
  22. declib/artifacts/patch.py +49 -0
  23. declib/artifacts/segment.py +37 -0
  24. declib/artifacts/stack_variable.py +50 -0
  25. declib/artifacts/struct.py +184 -0
  26. declib/artifacts/typedef.py +59 -0
  27. declib/cli/__init__.py +3 -0
  28. declib/cli/decompiler_cli.py +1487 -0
  29. declib/configuration.py +184 -0
  30. declib/decompiler_stubs/__init__.py +0 -0
  31. declib/decompiler_stubs/angr_declib/__init__.py +4 -0
  32. declib/decompiler_stubs/binja_declib/__init__.py +4 -0
  33. declib/decompiler_stubs/ida_declib.py +8 -0
  34. declib/decompilers/__init__.py +8 -0
  35. declib/decompilers/angr/__init__.py +11 -0
  36. declib/decompilers/angr/artifact_lifter.py +46 -0
  37. declib/decompilers/angr/compat.py +262 -0
  38. declib/decompilers/angr/interface.py +949 -0
  39. declib/decompilers/binja/__init__.py +0 -0
  40. declib/decompilers/binja/artifact_lifter.py +32 -0
  41. declib/decompilers/binja/hooks.py +201 -0
  42. declib/decompilers/binja/interface.py +795 -0
  43. declib/decompilers/ghidra/__init__.py +0 -0
  44. declib/decompilers/ghidra/artifact_lifter.py +60 -0
  45. declib/decompilers/ghidra/compat/__init__.py +0 -0
  46. declib/decompilers/ghidra/compat/headless.py +156 -0
  47. declib/decompilers/ghidra/compat/imports.py +78 -0
  48. declib/decompilers/ghidra/compat/state.py +54 -0
  49. declib/decompilers/ghidra/compat/transaction.py +30 -0
  50. declib/decompilers/ghidra/hooks.py +242 -0
  51. declib/decompilers/ghidra/interface.py +1433 -0
  52. declib/decompilers/ida/__init__.py +0 -0
  53. declib/decompilers/ida/artifact_lifter.py +51 -0
  54. declib/decompilers/ida/compat.py +2054 -0
  55. declib/decompilers/ida/hooks.py +700 -0
  56. declib/decompilers/ida/ida_ui.py +80 -0
  57. declib/decompilers/ida/interface.py +659 -0
  58. declib/logger.py +101 -0
  59. declib/plugin_installer.py +259 -0
  60. declib/skills/__init__.py +24 -0
  61. declib/skills/decompiler/SKILL.md +316 -0
  62. declib/ui/__init__.py +33 -0
  63. declib/ui/qt_objects.py +146 -0
  64. declib/ui/utils.py +115 -0
  65. declib/ui/version.py +14 -0
  66. declib-3.8.0.dist-info/METADATA +138 -0
  67. declib-3.8.0.dist-info/RECORD +71 -0
  68. declib-3.8.0.dist-info/WHEEL +5 -0
  69. declib-3.8.0.dist-info/entry_points.txt +3 -0
  70. declib-3.8.0.dist-info/licenses/LICENSE +24 -0
  71. declib-3.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1219 @@
1
+ # Note to reader: most of this code was generated by Claude 4.5. It may contain errors and was designed
2
+ # in tandem with decompiler_server.py and the tests/test_client_server.py file. This comment will be
3
+ # removed when the majority of the file is owned by a human author.
4
+
5
+ import logging
6
+ import socket
7
+ import time
8
+ import os
9
+ import glob
10
+ import tempfile
11
+ from typing import Dict, Any, Optional, List, Union, Callable
12
+ from collections import defaultdict
13
+ import threading
14
+
15
+ from declib.artifacts import (
16
+ Artifact, Function, Comment, Patch, GlobalVariable, Segment,
17
+ Struct, Enum, Typedef, Context, Decompilation
18
+ )
19
+ from declib.artifacts.formatting import ArtifactFormat
20
+ from declib.api.decompiler_server import SocketProtocol
21
+ from declib.api.type_parser import CTypeParser
22
+ from declib.configuration import DecLibConfig
23
+ from declib.api import server_registry
24
+
25
+ _l = logging.getLogger(__name__)
26
+
27
+ # Must match decompiler_server._WIRE_FMT; JSON avoids the `toml` package's
28
+ # buggy handling of raw `\x` escapes inside decompilation text.
29
+ _WIRE_FMT = ArtifactFormat.JSON
30
+
31
+
32
+ class ArtLifterProxy:
33
+ """
34
+ A proxy for the ArtifactLifter that delegates all operations to the remote server.
35
+ This maintains API compatibility with the local ArtifactLifter while sending
36
+ requests to the remote decompiler server.
37
+ """
38
+ SCOPE_DELIMITER = "::"
39
+
40
+ def __init__(self, client: 'DecompilerClient'):
41
+ self.client = client
42
+
43
+ def lift(self, artifact: Artifact):
44
+ """Lift an artifact using the remote decompiler"""
45
+ return self.client._send_request({
46
+ "type": "method_call",
47
+ "method_name": "art_lifter.lift",
48
+ "args": [artifact]
49
+ })
50
+
51
+ def lower(self, artifact: Artifact):
52
+ """Lower an artifact using the remote decompiler"""
53
+ return self.client._send_request({
54
+ "type": "method_call",
55
+ "method_name": "art_lifter.lower",
56
+ "args": [artifact]
57
+ })
58
+
59
+ def lift_addr(self, addr: int) -> int:
60
+ """Lift an address using the remote decompiler"""
61
+ return self.client._send_request({
62
+ "type": "method_call",
63
+ "method_name": "art_lifter.lift_addr",
64
+ "args": [addr]
65
+ })
66
+
67
+ def lower_addr(self, addr: int) -> int:
68
+ """Lower an address using the remote decompiler"""
69
+ return self.client._send_request({
70
+ "type": "method_call",
71
+ "method_name": "art_lifter.lower_addr",
72
+ "args": [addr]
73
+ })
74
+
75
+ def lift_type(self, type_str: str) -> str:
76
+ """Lift a type string using the remote decompiler"""
77
+ return self.client._send_request({
78
+ "type": "method_call",
79
+ "method_name": "art_lifter.lift_type",
80
+ "args": [type_str]
81
+ })
82
+
83
+ def lower_type(self, type_str: str) -> str:
84
+ """Lower a type string using the remote decompiler"""
85
+ return self.client._send_request({
86
+ "type": "method_call",
87
+ "method_name": "art_lifter.lower_type",
88
+ "args": [type_str]
89
+ })
90
+
91
+ def lift_stack_offset(self, offset: int, func_addr: int) -> int:
92
+ """Lift a stack offset using the remote decompiler"""
93
+ return self.client._send_request({
94
+ "type": "method_call",
95
+ "method_name": "art_lifter.lift_stack_offset",
96
+ "args": [offset, func_addr]
97
+ })
98
+
99
+ def lower_stack_offset(self, offset: int, func_addr: int) -> int:
100
+ """Lower a stack offset using the remote decompiler"""
101
+ return self.client._send_request({
102
+ "type": "method_call",
103
+ "method_name": "art_lifter.lower_stack_offset",
104
+ "args": [offset, func_addr]
105
+ })
106
+
107
+ @staticmethod
108
+ def parse_scoped_type(type_str: str) -> tuple[str, str | None]:
109
+ """
110
+ Parse a scoped type string into its base type and scope.
111
+ This is a static method that doesn't need remote decompiler access.
112
+ """
113
+ if not type_str:
114
+ return "", None
115
+
116
+ # check if the type is scoped
117
+ scope = None
118
+ deli = ArtLifterProxy.SCOPE_DELIMITER
119
+ if deli in type_str:
120
+ scope_parts = type_str.split(deli)
121
+ base_type = scope_parts[-1]
122
+ scope = deli.join(scope_parts[:-1])
123
+ else:
124
+ base_type = type_str
125
+
126
+ return base_type, scope
127
+
128
+ @staticmethod
129
+ def scoped_type_to_str(name: str, scope: str | None = None) -> str:
130
+ """
131
+ Convert a name and scope into a scoped type string.
132
+ This is a static method that doesn't need remote decompiler access.
133
+ """
134
+ return name if not scope else f"{scope}::{name}"
135
+
136
+
137
+ class FastClientArtifactDict(dict):
138
+ """
139
+ A fast client-side proxy for ArtifactDict that communicates with DecompilerServer via AF_UNIX sockets.
140
+
141
+ This class mimics the behavior of ArtifactDict but uses sockets for bulk operations
142
+ and maintains the same performance characteristics as the local version by using
143
+ the _lifted_art_lister pattern.
144
+ """
145
+
146
+ def __init__(self, collection_name: str, artifact_class, client: 'DecompilerClient'):
147
+ super().__init__()
148
+ self.collection_name = collection_name
149
+ self.artifact_class = artifact_class
150
+ self.client = client
151
+ self._light_cache = {}
152
+ self._light_cache_timestamp = 0
153
+ self._cache_ttl = 10.0 # Cache for 10 seconds
154
+
155
+ def _get_light_artifacts(self) -> Dict:
156
+ """Get all light artifacts using the server's fast bulk operation"""
157
+ current_time = time.time()
158
+ if current_time - self._light_cache_timestamp > self._cache_ttl:
159
+ # Cache expired, fetch from server using bulk endpoint
160
+ try:
161
+ _l.debug(f"Fetching light artifacts for {self.collection_name}")
162
+ request = {
163
+ "type": "get_light_artifacts",
164
+ "collection_name": self.collection_name
165
+ }
166
+ serialized_artifacts = self.client._send_request(request)
167
+
168
+ # Reconstruct artifacts from serialized format
169
+ reconstructed_artifacts = {}
170
+ for addr, artifact_info in serialized_artifacts.items():
171
+ try:
172
+ # Import the artifact class dynamically
173
+ module_name = artifact_info['module']
174
+ class_name = artifact_info['type']
175
+ serialized_data = artifact_info['data']
176
+
177
+ # Import the module and get the class
178
+ module = __import__(module_name, fromlist=[class_name])
179
+ artifact_class = getattr(module, class_name)
180
+
181
+ # Reconstruct the artifact using its loads method
182
+ artifact = artifact_class.loads(serialized_data, fmt=_WIRE_FMT)
183
+ reconstructed_artifacts[addr] = artifact
184
+
185
+ except Exception as e:
186
+ _l.warning(f"Failed to reconstruct artifact at 0x{addr:x}: {e}")
187
+ # Skip problematic artifacts rather than failing completely
188
+ continue
189
+
190
+ self._light_cache = reconstructed_artifacts
191
+ self._light_cache_timestamp = current_time
192
+ except Exception as e:
193
+ _l.warning(f"Failed to fetch light artifacts for {self.collection_name}: {e}")
194
+
195
+ return self._light_cache
196
+
197
+ def _invalidate_cache(self):
198
+ """Invalidate the light artifact cache"""
199
+ self._light_cache.clear()
200
+ self._light_cache_timestamp = 0
201
+
202
+ def __len__(self):
203
+ """Return the number of items in the collection"""
204
+ light_items = self._get_light_artifacts()
205
+ return len(light_items)
206
+
207
+ def __iter__(self):
208
+ """Iterate over keys in the collection"""
209
+ light_items = self._get_light_artifacts()
210
+ return iter(light_items.keys())
211
+
212
+ def keys(self):
213
+ """Return an iterator over the keys (fast bulk operation)"""
214
+ light_items = self._get_light_artifacts()
215
+ return light_items.keys()
216
+
217
+ def values(self):
218
+ """Return an iterator over the values (light artifacts, fast bulk operation)"""
219
+ light_items = self._get_light_artifacts()
220
+ return light_items.values()
221
+
222
+ def items(self):
223
+ """Return an iterator over (key, value) pairs (fast bulk operation)"""
224
+ light_items = self._get_light_artifacts()
225
+ return light_items.items()
226
+
227
+ def __getitem__(self, key):
228
+ """Get a full artifact by key (same behavior as local ArtifactDict)"""
229
+ # First, check if the key exists by looking at light artifacts
230
+ light_items = self._get_light_artifacts()
231
+ if key not in light_items:
232
+ raise KeyError(f"Key {key} not found in {self.collection_name}")
233
+
234
+ # Key exists, get the full artifact from server
235
+ try:
236
+ request = {
237
+ "type": "get_full_artifact",
238
+ "collection_name": self.collection_name,
239
+ "key": key
240
+ }
241
+ return self.client._send_request(request)
242
+ except Exception as e:
243
+ if "not found" in str(e).lower():
244
+ raise KeyError(f"Key {key} not found in {self.collection_name}")
245
+ else:
246
+ raise
247
+
248
+ def get_light(self, key):
249
+ """Get a light artifact by key (fast, cached access)"""
250
+ light_items = self._get_light_artifacts()
251
+ if key not in light_items:
252
+ raise KeyError(f"Key {key} not found in {self.collection_name}")
253
+ return light_items[key]
254
+
255
+ def get_full(self, key):
256
+ """Explicitly get a full artifact (with expensive operations like decompilation)"""
257
+ try:
258
+ request = {
259
+ "type": "get_full_artifact",
260
+ "collection_name": self.collection_name,
261
+ "key": key
262
+ }
263
+ return self.client._send_request(request)
264
+ except Exception as e:
265
+ if "not found" in str(e).lower():
266
+ raise KeyError(f"Key {key} not found in {self.collection_name}")
267
+ else:
268
+ raise
269
+
270
+ def __setitem__(self, key, value):
271
+ """Set an artifact by key on the server"""
272
+ if not isinstance(value, self.artifact_class):
273
+ raise ValueError(f"Expected {self.artifact_class.__name__}, got {type(value).__name__}")
274
+
275
+ # Use the direct decompiler interface for setting artifacts
276
+ success = self.client.set_artifact(value)
277
+
278
+ # Invalidate cache since we modified the collection
279
+ self._invalidate_cache()
280
+
281
+ if not success:
282
+ raise RuntimeError(f"Failed to set artifact")
283
+
284
+ def __delitem__(self, key):
285
+ """Delete an artifact by key (not implemented in decompiler interfaces)"""
286
+ raise NotImplementedError("Deletion not supported by DecompilerInterface")
287
+
288
+ def __contains__(self, key):
289
+ """Check if a key exists in the collection"""
290
+ light_items = self._get_light_artifacts()
291
+ return key in light_items
292
+
293
+ def get(self, key, default=None):
294
+ """Get a full artifact with a default value"""
295
+ try:
296
+ return self[key] # Use __getitem__ which returns full artifact
297
+ except KeyError:
298
+ return default
299
+
300
+
301
+ class DecompilerClient:
302
+ """
303
+ A client that connects to DecompilerServer via AF_UNIX sockets and provides the same interface as DecompilerInterface.
304
+
305
+ This class acts as a transparent proxy to a remote DecompilerInterface, allowing users to
306
+ write code that works identically whether using a local or remote decompiler.
307
+ """
308
+
309
+ def __init__(self,
310
+ socket_path: str,
311
+ timeout: float = 30.0):
312
+ """
313
+ Initialize the DecompilerClient.
314
+
315
+ Args:
316
+ socket_path: Path to the AF_UNIX socket
317
+ timeout: Connection timeout in seconds
318
+ """
319
+ self.socket_path = socket_path
320
+ self.timeout = timeout
321
+
322
+ # Connection state
323
+ self._socket = None
324
+ self._connected = False
325
+ self._server_info = None
326
+ self._socket_lock = threading.Lock()
327
+
328
+ # Try to connect
329
+ self._connect()
330
+
331
+ # Initialize fast artifact collections
332
+ self.functions = FastClientArtifactDict("functions", Function, self)
333
+ self.comments = FastClientArtifactDict("comments", Comment, self)
334
+ self.patches = FastClientArtifactDict("patches", Patch, self)
335
+ self.global_vars = FastClientArtifactDict("global_vars", GlobalVariable, self)
336
+ self.segments = FastClientArtifactDict("segments", Segment, self)
337
+ self.structs = FastClientArtifactDict("structs", Struct, self)
338
+ self.enums = FastClientArtifactDict("enums", Enum, self)
339
+ self.typedefs = FastClientArtifactDict("typedefs", Typedef, self)
340
+
341
+ # Initialize callback attributes to match DecompilerInterface
342
+ self.artifact_change_callbacks = defaultdict(list)
343
+ self.decompiler_closed_callbacks = []
344
+ self.decompiler_opened_callbacks = []
345
+ self.undo_event_callbacks = []
346
+ self._thread_artifact_callbacks = True
347
+
348
+ # Create a proxy art_lifter that delegates to server
349
+ # art_lifter is typically used for address lifting operations
350
+ self.art_lifter = ArtLifterProxy(self)
351
+
352
+ # Additional public attributes to match DecompilerInterface
353
+ self.type_parser = CTypeParser() # Local type parser
354
+ self.artifact_write_lock = threading.Lock() # Thread safety lock
355
+ self.config = DecLibConfig.update_or_make() # Configuration object
356
+ self.gui_plugin = None # GUI plugin reference
357
+ self.artifact_watchers_started = False # Watcher state
358
+
359
+ # Event listener state for receiving callbacks from server
360
+ self._event_listener_running = False
361
+ self._subscribed_to_events = False
362
+ self._event_listener_thread = None
363
+ self._event_socket = None
364
+ self._event_socket_lock = threading.Lock()
365
+
366
+ # These attributes will be fetched from server on first access
367
+ self._supports_undo = None
368
+ self._supports_type_scopes = None
369
+ self._qt_version = None
370
+ self._default_func_prefix = None
371
+ self._headless = None
372
+ self._force_click_recording = None
373
+ self._track_mouse_moves = None
374
+
375
+ _l.info(f"DecompilerClient connected to {socket_path}")
376
+
377
+ def _create_and_connect_socket(self) -> socket.socket:
378
+ """Create and connect a socket handling both AF_UNIX and AF_INET fallbacks."""
379
+ if hasattr(socket, "AF_UNIX"):
380
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
381
+ sock.settimeout(self.timeout)
382
+ sock.connect(self.socket_path)
383
+ else:
384
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
385
+ sock.settimeout(self.timeout)
386
+ with open(self.socket_path, 'r') as f:
387
+ port = int(f.read().strip())
388
+ sock.connect(('127.0.0.1', port))
389
+ return sock
390
+
391
+ def _connect(self):
392
+ """Establish connection to the server"""
393
+ try:
394
+ _l.debug(f"Attempting to connect to server at {self.socket_path}")
395
+
396
+ self._socket = self._create_and_connect_socket()
397
+
398
+ _l.debug("Socket connection established")
399
+
400
+ # Test the connection by getting server info first
401
+ self._server_info = self._send_request({"type": "server_info"})
402
+ _l.debug(f"Got server info: {self._server_info}")
403
+
404
+ self._connected = True
405
+
406
+ _l.info(f"Connected to {self._server_info.get('name', 'DecompilerServer')} "
407
+ f"using {self._server_info.get('decompiler', 'unknown')} decompiler")
408
+ except Exception as e:
409
+ _l.error(f"Failed to connect to DecompilerServer at {self.socket_path}: {e}")
410
+
411
+ # Provide helpful error messages for common issues
412
+ if "No such file or directory" in str(e):
413
+ raise ConnectionError(f"Cannot connect to DecompilerServer at {self.socket_path}. "
414
+ f"Make sure the server is running with: declib --server")
415
+ elif "Connection refused" in str(e):
416
+ raise ConnectionError(f"Cannot connect to DecompilerServer at {self.socket_path}. "
417
+ f"Make sure the server is running.")
418
+ else:
419
+ raise ConnectionError(f"Cannot connect to DecompilerServer: {e}")
420
+
421
+ def _send_request(self, request: Dict[str, Any]) -> Any:
422
+ """Send a request to the server and return the response"""
423
+ with self._socket_lock:
424
+ try:
425
+ SocketProtocol.send_message(self._socket, request)
426
+ response = SocketProtocol.recv_message(self._socket)
427
+
428
+ # Check if response is an error
429
+ if isinstance(response, dict) and "error" in response:
430
+ error_type = response.get("type", "Exception")
431
+ error_msg = response.get("error", "Unknown error")
432
+
433
+ # Try to reconstruct the original exception type
434
+ if error_type == "KeyError":
435
+ raise KeyError(error_msg)
436
+ elif error_type == "ValueError":
437
+ raise ValueError(error_msg)
438
+ elif error_type == "AttributeError":
439
+ raise AttributeError(error_msg)
440
+ else:
441
+ raise RuntimeError(f"{error_type}: {error_msg}")
442
+
443
+ # Check if response is a serialized artifact
444
+ if isinstance(response, dict) and response.get("is_artifact"):
445
+ try:
446
+ # Reconstruct the artifact
447
+ module_name = response['module']
448
+ class_name = response['type']
449
+ serialized_data = response['data']
450
+
451
+ # Import the module and get the class
452
+ module = __import__(module_name, fromlist=[class_name])
453
+ artifact_class = getattr(module, class_name)
454
+
455
+ # Reconstruct the artifact using its loads method
456
+ artifact = artifact_class.loads(serialized_data, fmt=_WIRE_FMT)
457
+ return artifact
458
+
459
+ except Exception as e:
460
+ _l.warning(f"Failed to reconstruct artifact response: {e}")
461
+ # Fall back to returning the raw response
462
+ return response
463
+
464
+ return response
465
+ except Exception as e:
466
+ _l.error(f"Request failed: {e} for {request}")
467
+ raise
468
+
469
+ # Properties - mirror DecompilerInterface properties
470
+ @property
471
+ def name(self) -> str:
472
+ """Name of the decompiler"""
473
+ return self._server_info.get('decompiler', 'remote')
474
+
475
+ @property
476
+ def binary_base_addr(self) -> int:
477
+ """Base address of the binary"""
478
+ return self._send_request({"type": "property_get", "property_name": "binary_base_addr"})
479
+
480
+ @property
481
+ def binary_hash(self) -> str:
482
+ """Hash of the binary"""
483
+ return self._send_request({"type": "property_get", "property_name": "binary_hash"})
484
+
485
+ @property
486
+ def binary_path(self) -> Optional[str]:
487
+ """Path to the binary"""
488
+ return self._send_request({"type": "property_get", "property_name": "binary_path"})
489
+
490
+ @property
491
+ def decompiler_available(self) -> bool:
492
+ """Whether decompiler is available"""
493
+ return self._send_request({"type": "property_get", "property_name": "decompiler_available"})
494
+
495
+ @property
496
+ def default_pointer_size(self) -> int:
497
+ """Default pointer size"""
498
+ return self._send_request({"type": "property_get", "property_name": "default_pointer_size"})
499
+
500
+ # GUI API methods - delegate to remote decompiler
501
+ def gui_active_context(self) -> Optional[Context]:
502
+ """Get the active context from the GUI"""
503
+ return self._send_request({"type": "method_call", "method_name": "gui_active_context"})
504
+
505
+ def gui_goto(self, func_addr) -> None:
506
+ """Go to an address in the GUI"""
507
+ return self._send_request({"type": "method_call", "method_name": "gui_goto", "args": [func_addr]})
508
+
509
+ def gui_show_type(self, type_name: str) -> None:
510
+ """Show a type in the GUI"""
511
+ return self._send_request({"type": "method_call", "method_name": "gui_show_type", "args": [type_name]})
512
+
513
+ def gui_ask_for_string(self, question: str, title: str = "Plugin Question", default: str = "") -> str:
514
+ """Ask for a string input"""
515
+ return self._send_request({"type": "method_call", "method_name": "gui_ask_for_string", "args": [question, title, default]})
516
+
517
+ def gui_ask_for_choice(self, question: str, choices: list, title: str = "Plugin Question") -> str:
518
+ """Ask for a choice from a list"""
519
+ return self._send_request({"type": "method_call", "method_name": "gui_ask_for_choice", "args": [question, choices, title]})
520
+
521
+ def gui_popup_text(self, text: str, title: str = "Plugin Message") -> bool:
522
+ """Show a popup message"""
523
+ return self._send_request({"type": "method_call", "method_name": "gui_popup_text", "args": [text, title]})
524
+
525
+ # Core decompiler API methods - delegate to remote decompiler
526
+ def fast_get_function(self, func_addr) -> Optional[Function]:
527
+ """Get a light version of a function"""
528
+ return self._send_request({"type": "method_call", "method_name": "fast_get_function", "args": [func_addr]})
529
+
530
+ def get_func_size(self, func_addr) -> int:
531
+ """Get the size of a function"""
532
+ return self._send_request({"type": "method_call", "method_name": "get_func_size", "args": [func_addr]})
533
+
534
+ def decompile(self, addr: int, map_lines=False, **kwargs) -> Optional[Decompilation]:
535
+ """Decompile a function"""
536
+ return self._send_request({"type": "method_call", "method_name": "decompile", "args": [addr], "kwargs": {"map_lines": map_lines, **kwargs}})
537
+
538
+ def xrefs_to(self, artifact: Artifact, decompile=False, only_code=False) -> List[Artifact]:
539
+ """Get cross-references to an artifact"""
540
+ return self._send_request({"type": "method_call", "method_name": "xrefs_to", "args": [artifact], "kwargs": {"decompile": decompile, "only_code": only_code}})
541
+
542
+ def xrefs_to_addr(self, addr: int, only_code: bool = False) -> List[Artifact]:
543
+ """Get references to a raw address (e.g. a string constant)"""
544
+ return self._send_request({"type": "method_call", "method_name": "xrefs_to_addr", "args": [addr], "kwargs": {"only_code": only_code}})
545
+
546
+ def xrefs_from(self, func_addr: int) -> List[Function]:
547
+ """Get the callees of a function (what the function calls)."""
548
+ return self._send_request({"type": "method_call", "method_name": "xrefs_from", "args": [func_addr]})
549
+
550
+ def get_callers(self, target) -> List[Function]:
551
+ """Get callers of a function (by target Function, address, or symbol name)"""
552
+ return self._send_request({"type": "method_call", "method_name": "get_callers", "args": [target]})
553
+
554
+ def list_strings(self, filter: Optional[str] = None) -> List:
555
+ """List strings in the binary with an optional regex filter"""
556
+ return self._send_request({"type": "method_call", "method_name": "list_strings", "kwargs": {"filter": filter}})
557
+
558
+ def disassemble(self, addr: int, **kwargs) -> Optional[str]:
559
+ """Disassemble a function"""
560
+ return self._send_request({"type": "method_call", "method_name": "disassemble", "args": [addr], "kwargs": kwargs})
561
+
562
+ def read_memory(self, addr: int, size: int) -> Optional[bytes]:
563
+ """Read raw bytes from the loaded program."""
564
+ return self._send_request({"type": "method_call", "method_name": "read_memory", "args": [addr, size]})
565
+
566
+ def get_callgraph(self, only_names=False):
567
+ """Get the call graph"""
568
+ return self._send_request({"type": "method_call", "method_name": "get_callgraph", "kwargs": {"only_names": only_names}})
569
+
570
+ def get_dependencies(self, artifact: Artifact, decompile=True, max_resolves=50, **kwargs) -> List[Artifact]:
571
+ """Get dependencies for an artifact"""
572
+ return self._send_request({"type": "method_call", "method_name": "get_dependencies", "args": [artifact], "kwargs": {"decompile": decompile, "max_resolves": max_resolves, **kwargs}})
573
+
574
+ def get_func_containing(self, addr: int) -> Optional[Function]:
575
+ """Get the function containing an address"""
576
+ return self._send_request({"type": "method_call", "method_name": "get_func_containing", "args": [addr]})
577
+
578
+ def get_decompilation_object(self, function: Function, **kwargs):
579
+ """Get the decompilation object for a function"""
580
+ return self._send_request({"type": "method_call", "method_name": "get_decompilation_object", "args": [function], "kwargs": kwargs})
581
+
582
+ def set_artifact(self, artifact: Artifact, lower=True, **kwargs) -> bool:
583
+ """Set an artifact in the decompiler"""
584
+ return self._send_request({"type": "method_call", "method_name": "set_artifact", "args": [artifact], "kwargs": {"lower": lower, **kwargs}})
585
+
586
+ def get_defined_type(self, type_str: str):
587
+ """Get a defined type by string"""
588
+ return self._send_request({"type": "method_call", "method_name": "get_defined_type", "args": [type_str]})
589
+
590
+ # Optional API methods - delegate to remote decompiler
591
+ def undo(self) -> None:
592
+ """Undo the last operation"""
593
+ return self._send_request({"type": "method_call", "method_name": "undo"})
594
+
595
+ def local_variable_names(self, func: Function) -> List[str]:
596
+ """Get local variable names for a function"""
597
+ return self._send_request({"type": "method_call", "method_name": "local_variable_names", "args": [func]})
598
+
599
+ def rename_local_variables_by_names(self, func: Function, name_map: Dict[str, str], **kwargs) -> bool:
600
+ """Rename local variables by name map"""
601
+ return self._send_request({"type": "method_call", "method_name": "rename_local_variables_by_names", "args": [func, name_map], "kwargs": kwargs})
602
+
603
+ # Logging methods - delegate to remote decompiler
604
+ def print(self, msg: str, **kwargs) -> None:
605
+ """Print a message"""
606
+ return self._send_request({"type": "method_call", "method_name": "print", "args": [msg], "kwargs": kwargs})
607
+
608
+ def info(self, msg: str, **kwargs) -> None:
609
+ """Log an info message"""
610
+ return self._send_request({"type": "method_call", "method_name": "info", "args": [msg], "kwargs": kwargs})
611
+
612
+ def debug(self, msg: str, **kwargs) -> None:
613
+ """Log a debug message"""
614
+ return self._send_request({"type": "method_call", "method_name": "debug", "args": [msg], "kwargs": kwargs})
615
+
616
+ def warning(self, msg: str, **kwargs) -> None:
617
+ """Log a warning message"""
618
+ return self._send_request({"type": "method_call", "method_name": "warning", "args": [msg], "kwargs": kwargs})
619
+
620
+ def error(self, msg: str, **kwargs) -> None:
621
+ """Log an error message"""
622
+ return self._send_request({"type": "method_call", "method_name": "error", "args": [msg], "kwargs": kwargs})
623
+
624
+ def _start_event_listener(self) -> None:
625
+ """Start the event listener thread to receive callbacks from server"""
626
+ if self._event_listener_running:
627
+ _l.debug("Event listener already running")
628
+ return
629
+
630
+ _l.debug("Starting event listener")
631
+
632
+ # Create a separate socket connection for receiving events
633
+ try:
634
+ self._event_socket = self._create_and_connect_socket()
635
+
636
+ # Send subscription request to server
637
+ SocketProtocol.send_message(self._event_socket, {"type": "subscribe_events"})
638
+ response = SocketProtocol.recv_message(self._event_socket)
639
+
640
+ if response.get("status") == "subscribed":
641
+ self._subscribed_to_events = True
642
+ _l.debug("Successfully subscribed to events")
643
+
644
+ # Start event listener thread
645
+ self._event_listener_running = True
646
+ self._event_listener_thread = threading.Thread(
647
+ target=self._event_listener_loop,
648
+ daemon=True
649
+ )
650
+ self._event_listener_thread.start()
651
+ _l.info("Event listener started")
652
+ else:
653
+ _l.error(f"Failed to subscribe to events: {response}")
654
+ self._event_socket.close()
655
+ self._event_socket = None
656
+
657
+ except Exception as e:
658
+ _l.error(f"Failed to start event listener: {e}")
659
+ if self._event_socket:
660
+ self._event_socket.close()
661
+ self._event_socket = None
662
+
663
+ def _stop_event_listener(self) -> None:
664
+ """Stop the event listener thread"""
665
+ if not self._event_listener_running:
666
+ _l.debug("Event listener not running")
667
+ return
668
+
669
+ _l.debug("Stopping event listener")
670
+ self._event_listener_running = False
671
+
672
+ # Send unsubscribe request
673
+ if self._event_socket and self._subscribed_to_events:
674
+ try:
675
+ SocketProtocol.send_message(self._event_socket, {"type": "unsubscribe_events"})
676
+ except:
677
+ pass
678
+
679
+ # Close event socket
680
+ if self._event_socket:
681
+ try:
682
+ self._event_socket.close()
683
+ except:
684
+ pass
685
+ self._event_socket = None
686
+
687
+ # Wait for thread to finish
688
+ if self._event_listener_thread and self._event_listener_thread.is_alive():
689
+ self._event_listener_thread.join(timeout=2.0)
690
+
691
+ self._subscribed_to_events = False
692
+ _l.info("Event listener stopped")
693
+
694
+ def _event_listener_loop(self) -> None:
695
+ """Event listener thread loop that receives events from server"""
696
+ _l.debug("Event listener loop started")
697
+
698
+ try:
699
+ while self._event_listener_running:
700
+ try:
701
+ # Set a timeout so we can periodically check if we should stop
702
+ self._event_socket.settimeout(1.0)
703
+ event = SocketProtocol.recv_message(self._event_socket)
704
+
705
+ # Process the event
706
+ self._process_event(event)
707
+
708
+ except socket.timeout:
709
+ # Normal timeout, continue loop
710
+ continue
711
+ except ConnectionError as e:
712
+ _l.warning(f"Event listener connection error: {e}")
713
+ break
714
+ except Exception as e:
715
+ _l.error(f"Error in event listener loop: {e}")
716
+ break
717
+
718
+ except Exception as e:
719
+ _l.error(f"Fatal error in event listener loop: {e}")
720
+ finally:
721
+ _l.debug("Event listener loop ended")
722
+ self._event_listener_running = False
723
+
724
+ def _process_event(self, event: Dict[str, Any]) -> None:
725
+ """Process an event received from the server"""
726
+ try:
727
+ event_type = event.get("event_type")
728
+ artifact_data = event.get("artifact")
729
+
730
+ if not event_type or not artifact_data:
731
+ _l.warning(f"Invalid event received: {event}")
732
+ return
733
+
734
+ # Reconstruct the artifact from serialized data
735
+ if isinstance(artifact_data, dict) and artifact_data.get("is_artifact"):
736
+ module_name = artifact_data['module']
737
+ class_name = artifact_data['type']
738
+ serialized_data = artifact_data['data']
739
+
740
+ # Import the module and get the class
741
+ module = __import__(module_name, fromlist=[class_name])
742
+ artifact_class = getattr(module, class_name)
743
+
744
+ # Reconstruct the artifact
745
+ artifact = artifact_class.loads(serialized_data, fmt=_WIRE_FMT)
746
+
747
+ # Extract additional kwargs
748
+ kwargs = event.get("kwargs", {})
749
+
750
+ # Dispatch to appropriate handler based on event type
751
+ if event_type == "comment_changed":
752
+ self.comment_changed(artifact, **kwargs)
753
+ elif event_type == "function_header_changed":
754
+ self.function_header_changed(artifact, **kwargs)
755
+ elif event_type == "stack_variable_changed":
756
+ self.stack_variable_changed(artifact, **kwargs)
757
+ elif event_type == "struct_changed":
758
+ self.struct_changed(artifact, **kwargs)
759
+ elif event_type == "enum_changed":
760
+ self.enum_changed(artifact, **kwargs)
761
+ elif event_type == "typedef_changed":
762
+ self.typedef_changed(artifact, **kwargs)
763
+ elif event_type == "global_variable_changed":
764
+ self.global_variable_changed(artifact, **kwargs)
765
+ else:
766
+ _l.warning(f"Unknown event type: {event_type}")
767
+
768
+ except Exception as e:
769
+ _l.error(f"Error processing event: {e}")
770
+
771
+ # Lifecycle methods
772
+ def shutdown(self) -> None:
773
+ """Disconnect this client from the server.
774
+
775
+ This only tears down the *local* client; the server (and its loaded
776
+ decompiler project) keeps running so other clients can still connect.
777
+ To actually stop the server, use ``shutdown_server()`` or the
778
+ ``decompiler stop`` CLI.
779
+ """
780
+ _l.info("DecompilerClient shutting down")
781
+
782
+ # Stop event listener first
783
+ if self._event_listener_running:
784
+ self._stop_event_listener()
785
+
786
+ if self._socket:
787
+ try:
788
+ self._socket.close()
789
+ except Exception:
790
+ pass
791
+ self._connected = False
792
+ _l.info("DecompilerClient shut down complete")
793
+
794
+ def shutdown_server(self) -> None:
795
+ """Ask the server to tear down its decompiler interface, then disconnect.
796
+
797
+ Used by CLI commands like ``decompiler stop``. Regular usage should
798
+ prefer :meth:`shutdown`, which leaves the server running.
799
+ """
800
+ if self._socket:
801
+ try:
802
+ self._send_request({"type": "shutdown_deci"})
803
+ except Exception:
804
+ pass
805
+ self.shutdown()
806
+
807
+ def is_connected(self) -> bool:
808
+ """Check if connected to the server"""
809
+ return self._connected and self._socket
810
+
811
+ def reconnect(self) -> None:
812
+ """Reconnect to the server"""
813
+ if self._socket:
814
+ self._socket.close()
815
+ self._connect()
816
+
817
+ def ping(self) -> bool:
818
+ """Ping the server to check connectivity"""
819
+ try:
820
+ self._send_request({"type": "server_info"})
821
+ return True
822
+ except Exception:
823
+ return False
824
+
825
+ # Context manager support
826
+ def __enter__(self):
827
+ return self
828
+
829
+ def __exit__(self, exc_type, exc_val, exc_tb):
830
+ self.shutdown()
831
+
832
+ @staticmethod
833
+ def discover_from_registry(
834
+ server_id: Optional[str] = None,
835
+ binary_path: Optional[str] = None,
836
+ binary_hash: Optional[str] = None,
837
+ backend: Optional[str] = None,
838
+ **kwargs,
839
+ ) -> 'DecompilerClient':
840
+ """
841
+ Find a running server via the shared registry and connect to it.
842
+
843
+ Filters narrow the pool in the order: server_id, binary_path, binary_hash, backend.
844
+ If no server matches, a ConnectionError is raised.
845
+ """
846
+ record = server_registry.find_server(
847
+ server_id=server_id,
848
+ binary_path=binary_path,
849
+ binary_hash=binary_hash,
850
+ backend=backend,
851
+ )
852
+ if not record:
853
+ filters = {
854
+ "server_id": server_id,
855
+ "binary_path": binary_path,
856
+ "binary_hash": binary_hash,
857
+ "backend": backend,
858
+ }
859
+ active = {k: v for k, v in filters.items() if v}
860
+ raise ConnectionError(
861
+ f"No matching DecompilerServer in registry. Filters: {active or 'none'}."
862
+ )
863
+ return DecompilerClient(socket_path=record["socket_path"], **kwargs)
864
+
865
+ # Static methods for compatibility
866
+ @staticmethod
867
+ def discover(server_url: str = None, binary_hash: str = None, **kwargs) -> 'DecompilerClient':
868
+ """
869
+ Discover and connect to a DecompilerServer.
870
+
871
+ This method provides a similar interface to DecompilerInterface.discover()
872
+ but connects to a remote server instead. It intelligently handles:
873
+ - Stale socket files from previous server instances
874
+ - Multiple running servers
875
+ - Binary hash matching to connect to the correct server
876
+
877
+ Args:
878
+ server_url: URL of the server (e.g., "unix:///tmp/declib_server_abc123/decompiler.sock")
879
+ binary_hash: Optional binary hash to match against server's binary_hash
880
+ **kwargs: Additional arguments for DecompilerClient constructor
881
+
882
+ Returns:
883
+ Connected DecompilerClient instance
884
+
885
+ Raises:
886
+ ConnectionError: If no suitable server is found or connection fails
887
+ """
888
+ if server_url:
889
+ # Parse server URL
890
+ if "://" in server_url:
891
+ protocol, path = server_url.split("://", 1)
892
+ if protocol != "unix":
893
+ _l.warning(f"Expected unix:// protocol, got {protocol}://")
894
+ socket_path = path
895
+ else:
896
+ # Assume it's a direct path
897
+ socket_path = server_url
898
+
899
+ # If binary_hash is provided, validate it matches
900
+ if binary_hash:
901
+ try:
902
+ client = DecompilerClient(socket_path=socket_path, **kwargs)
903
+ server_hash = client.binary_hash
904
+ if server_hash != binary_hash:
905
+ client.shutdown()
906
+ raise ConnectionError(
907
+ f"Server at {socket_path} has binary_hash={server_hash}, "
908
+ f"but expected {binary_hash}"
909
+ )
910
+ return client
911
+ except Exception as e:
912
+ raise ConnectionError(f"Failed to connect to server at {socket_path}: {e}")
913
+ else:
914
+ return DecompilerClient(socket_path=socket_path, **kwargs)
915
+ else:
916
+ # Auto-discovery: find all socket files and try to connect to each
917
+ temp_dir = tempfile.gettempdir()
918
+ pattern = os.path.join(temp_dir, "declib_server_*/decompiler.sock")
919
+ matches = glob.glob(pattern)
920
+
921
+ if not matches:
922
+ raise ConnectionError("No DecompilerServer found. Start one with: declib --server")
923
+
924
+ # Sort by modification time (newest first) to prefer recently started servers
925
+ matches.sort(key=lambda p: os.path.getmtime(p), reverse=True)
926
+
927
+ _l.debug(f"Found {len(matches)} potential server socket(s)")
928
+
929
+ # Try each socket, filtering by binary_hash if provided
930
+ successful_connections = []
931
+ for socket_path in matches:
932
+ try:
933
+ _l.debug(f"Attempting connection to {socket_path}")
934
+ test_client = DecompilerClient(socket_path=socket_path, **kwargs)
935
+
936
+ # Successfully connected, now check binary_hash if needed
937
+ if binary_hash:
938
+ try:
939
+ server_hash = test_client.binary_hash
940
+ if server_hash == binary_hash:
941
+ _l.info(f"Auto-discovered server at {socket_path} with matching binary_hash")
942
+ return test_client
943
+ else:
944
+ _l.debug(f"Server at {socket_path} has binary_hash={server_hash}, skipping")
945
+ test_client.shutdown()
946
+ except Exception as e:
947
+ _l.debug(f"Failed to get binary_hash from {socket_path}: {e}")
948
+ test_client.shutdown()
949
+ else:
950
+ # No binary_hash filter, use the first working server
951
+ _l.info(f"Auto-discovered server at {socket_path}")
952
+ return test_client
953
+
954
+ except ConnectionError as e:
955
+ # This socket is defunct (server stopped), skip it
956
+ _l.debug(f"Failed to connect to {socket_path}: {e}")
957
+ continue
958
+ except Exception as e:
959
+ _l.debug(f"Unexpected error connecting to {socket_path}: {e}")
960
+ continue
961
+
962
+ # No suitable server found
963
+ if binary_hash:
964
+ raise ConnectionError(
965
+ f"No DecompilerServer found with binary_hash={binary_hash}. "
966
+ f"Found {len(matches)} socket(s) but none matched."
967
+ )
968
+ else:
969
+ raise ConnectionError(
970
+ f"No working DecompilerServer found. Found {len(matches)} socket(s) "
971
+ f"but all connections failed. Start a new server with: declib --server"
972
+ )
973
+
974
+ # Properties that fetch values from server on first access
975
+ @property
976
+ def supports_undo(self) -> bool:
977
+ """Check if the decompiler supports undo operations"""
978
+ if self._supports_undo is None:
979
+ self._supports_undo = self._send_request({"type": "property_get", "property_name": "supports_undo"})
980
+ return self._supports_undo
981
+
982
+ @property
983
+ def supports_type_scopes(self) -> bool:
984
+ """Check if the decompiler supports type scopes"""
985
+ if self._supports_type_scopes is None:
986
+ self._supports_type_scopes = self._send_request({"type": "property_get", "property_name": "supports_type_scopes"})
987
+ return self._supports_type_scopes
988
+
989
+ @property
990
+ def qt_version(self) -> str:
991
+ """Get the Qt version used by the decompiler"""
992
+ if self._qt_version is None:
993
+ self._qt_version = self._send_request({"type": "property_get", "property_name": "qt_version"})
994
+ return self._qt_version
995
+
996
+ @property
997
+ def default_func_prefix(self) -> str:
998
+ """Get the default function prefix used by the decompiler"""
999
+ if self._default_func_prefix is None:
1000
+ self._default_func_prefix = self._send_request({"type": "property_get", "property_name": "default_func_prefix"})
1001
+ return self._default_func_prefix
1002
+
1003
+ @property
1004
+ def headless(self) -> bool:
1005
+ """Check if the decompiler is running in headless mode"""
1006
+ if self._headless is None:
1007
+ self._headless = self._send_request({"type": "property_get", "property_name": "headless"})
1008
+ return self._headless
1009
+
1010
+ @property
1011
+ def force_click_recording(self) -> bool:
1012
+ """Check if click recording is forced"""
1013
+ if self._force_click_recording is None:
1014
+ self._force_click_recording = self._send_request({"type": "property_get", "property_name": "force_click_recording"})
1015
+ return self._force_click_recording
1016
+
1017
+ @property
1018
+ def track_mouse_moves(self) -> bool:
1019
+ """Check if mouse moves are tracked"""
1020
+ if self._track_mouse_moves is None:
1021
+ self._track_mouse_moves = self._send_request({"type": "property_get", "property_name": "track_mouse_moves"})
1022
+ return self._track_mouse_moves
1023
+
1024
+ @property
1025
+ def default_pointer_size(self) -> int:
1026
+ """Get default pointer size"""
1027
+ return self._send_request({"type": "property_get", "property_name": "default_pointer_size"})
1028
+
1029
+ # Artifact watcher methods
1030
+ def start_artifact_watchers(self) -> None:
1031
+ """Start artifact watchers on the remote decompiler"""
1032
+ result = self._send_request({"type": "method_call", "method_name": "start_artifact_watchers"})
1033
+ self.artifact_watchers_started = True
1034
+
1035
+ # Start event listener to receive callbacks from server
1036
+ self._start_event_listener()
1037
+
1038
+ return result
1039
+
1040
+ def stop_artifact_watchers(self) -> None:
1041
+ """Stop artifact watchers on the remote decompiler"""
1042
+ # Stop event listener first
1043
+ self._stop_event_listener()
1044
+
1045
+ result = self._send_request({"type": "method_call", "method_name": "stop_artifact_watchers"})
1046
+ self.artifact_watchers_started = False
1047
+ return result
1048
+
1049
+ def should_watch_artifacts(self) -> bool:
1050
+ """Check if artifacts should be watched"""
1051
+ return self._send_request({"type": "method_call", "method_name": "should_watch_artifacts"})
1052
+
1053
+ # GUI registration methods (stubs since we can't proxy GUI operations)
1054
+ def gui_register_ctx_menu(self, name: str, action_string: str, callback_func: Callable, category=None) -> bool:
1055
+ """Register a context menu item (not supported in remote mode)"""
1056
+ _l.warning("GUI context menu registration is not supported in remote decompiler mode")
1057
+ return False
1058
+
1059
+ def gui_register_ctx_menu_many(self, actions: dict) -> None:
1060
+ """Register multiple context menu items (not supported in remote mode)"""
1061
+ _l.warning("GUI context menu registration is not supported in remote decompiler mode")
1062
+
1063
+ def gui_run_on_main_thread(self, func: Callable, *args, **kwargs):
1064
+ """Run function on main thread (not supported in remote mode)"""
1065
+ _l.warning("GUI main thread operations are not supported in remote decompiler mode")
1066
+ raise NotImplementedError("GUI main thread operations not supported in remote mode")
1067
+
1068
+ def gui_attach_qt_window(self, qt_window, title: str, target_window=None, position=None, *args, **kwargs) -> bool:
1069
+ """Attach Qt window (not supported in remote mode)"""
1070
+ _l.warning("GUI window attachment is not supported in remote decompiler mode")
1071
+ return False
1072
+
1073
+ # Event callback methods (these trigger callbacks locally but don't send to server)
1074
+ def decompiler_opened_event(self, **kwargs):
1075
+ """Handle decompiler opened event"""
1076
+ for callback in self.decompiler_opened_callbacks:
1077
+ try:
1078
+ if self._thread_artifact_callbacks:
1079
+ import threading
1080
+ thread = threading.Thread(target=callback, kwargs=kwargs)
1081
+ thread.start()
1082
+ else:
1083
+ callback(**kwargs)
1084
+ except Exception as e:
1085
+ _l.error(f"Error in decompiler opened callback: {e}")
1086
+
1087
+ def decompiler_closed_event(self, **kwargs):
1088
+ """Handle decompiler closed event"""
1089
+ for callback in self.decompiler_closed_callbacks:
1090
+ try:
1091
+ if self._thread_artifact_callbacks:
1092
+ import threading
1093
+ thread = threading.Thread(target=callback, kwargs=kwargs)
1094
+ thread.start()
1095
+ else:
1096
+ callback(**kwargs)
1097
+ except Exception as e:
1098
+ _l.error(f"Error in decompiler closed callback: {e}")
1099
+
1100
+ def gui_undo_event(self, **kwargs):
1101
+ """Handle GUI undo event"""
1102
+ for callback in self.undo_event_callbacks:
1103
+ try:
1104
+ if self._thread_artifact_callbacks:
1105
+ import threading
1106
+ thread = threading.Thread(target=callback, kwargs=kwargs)
1107
+ thread.start()
1108
+ else:
1109
+ callback(**kwargs)
1110
+ except Exception as e:
1111
+ _l.error(f"Error in undo event callback: {e}")
1112
+
1113
+ def gui_context_changed(self, ctx: Context, **kwargs) -> Context:
1114
+ """Handle GUI context changed event"""
1115
+ # This would typically be handled by GUI callbacks locally
1116
+ return ctx
1117
+
1118
+ # Artifact change event methods (these handle local callbacks)
1119
+ def function_header_changed(self, fheader, **kwargs):
1120
+ """Handle function header changed event"""
1121
+ for callback in self.artifact_change_callbacks.get(type(fheader), []):
1122
+ try:
1123
+ if self._thread_artifact_callbacks:
1124
+ import threading
1125
+ thread = threading.Thread(target=callback, args=(fheader,), kwargs=kwargs)
1126
+ thread.start()
1127
+ else:
1128
+ callback(fheader, **kwargs)
1129
+ except Exception as e:
1130
+ _l.error(f"Error in function header change callback: {e}")
1131
+ return fheader
1132
+
1133
+ def stack_variable_changed(self, svar, **kwargs):
1134
+ """Handle stack variable changed event"""
1135
+ for callback in self.artifact_change_callbacks.get(type(svar), []):
1136
+ try:
1137
+ if self._thread_artifact_callbacks:
1138
+ import threading
1139
+ thread = threading.Thread(target=callback, args=(svar,), kwargs=kwargs)
1140
+ thread.start()
1141
+ else:
1142
+ callback(svar, **kwargs)
1143
+ except Exception as e:
1144
+ _l.error(f"Error in stack variable change callback: {e}")
1145
+ return svar
1146
+
1147
+ def comment_changed(self, comment: Comment, deleted=False, **kwargs) -> Comment:
1148
+ """Handle comment changed event"""
1149
+ kwargs["deleted"] = deleted
1150
+ for callback in self.artifact_change_callbacks.get(Comment, []):
1151
+ try:
1152
+ if self._thread_artifact_callbacks:
1153
+ import threading
1154
+ thread = threading.Thread(target=callback, args=(comment,), kwargs=kwargs)
1155
+ thread.start()
1156
+ else:
1157
+ callback(comment, **kwargs)
1158
+ except Exception as e:
1159
+ _l.error(f"Error in comment change callback: {e}")
1160
+ return comment
1161
+
1162
+ def struct_changed(self, struct: Struct, deleted=False, **kwargs) -> Struct:
1163
+ """Handle struct changed event"""
1164
+ kwargs["deleted"] = deleted
1165
+ for callback in self.artifact_change_callbacks.get(Struct, []):
1166
+ try:
1167
+ if self._thread_artifact_callbacks:
1168
+ import threading
1169
+ thread = threading.Thread(target=callback, args=(struct,), kwargs=kwargs)
1170
+ thread.start()
1171
+ else:
1172
+ callback(struct, **kwargs)
1173
+ except Exception as e:
1174
+ _l.error(f"Error in struct change callback: {e}")
1175
+ return struct
1176
+
1177
+ def enum_changed(self, enum: Enum, deleted=False, **kwargs) -> Enum:
1178
+ """Handle enum changed event"""
1179
+ kwargs["deleted"] = deleted
1180
+ for callback in self.artifact_change_callbacks.get(Enum, []):
1181
+ try:
1182
+ if self._thread_artifact_callbacks:
1183
+ import threading
1184
+ thread = threading.Thread(target=callback, args=(enum,), kwargs=kwargs)
1185
+ thread.start()
1186
+ else:
1187
+ callback(enum, **kwargs)
1188
+ except Exception as e:
1189
+ _l.error(f"Error in enum change callback: {e}")
1190
+ return enum
1191
+
1192
+ def typedef_changed(self, typedef: Typedef, deleted=False, **kwargs) -> Typedef:
1193
+ """Handle typedef changed event"""
1194
+ kwargs["deleted"] = deleted
1195
+ for callback in self.artifact_change_callbacks.get(Typedef, []):
1196
+ try:
1197
+ if self._thread_artifact_callbacks:
1198
+ import threading
1199
+ thread = threading.Thread(target=callback, args=(typedef,), kwargs=kwargs)
1200
+ thread.start()
1201
+ else:
1202
+ callback(typedef, **kwargs)
1203
+ except Exception as e:
1204
+ _l.error(f"Error in typedef change callback: {e}")
1205
+ return typedef
1206
+
1207
+ def global_variable_changed(self, gvar: GlobalVariable, **kwargs) -> GlobalVariable:
1208
+ """Handle global variable changed event"""
1209
+ for callback in self.artifact_change_callbacks.get(GlobalVariable, []):
1210
+ try:
1211
+ if self._thread_artifact_callbacks:
1212
+ import threading
1213
+ thread = threading.Thread(target=callback, args=(gvar,), kwargs=kwargs)
1214
+ thread.start()
1215
+ else:
1216
+ callback(gvar, **kwargs)
1217
+ except Exception as e:
1218
+ _l.error(f"Error in global variable change callback: {e}")
1219
+ return gvar