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.
- declib/__init__.py +9 -0
- declib/__main__.py +190 -0
- declib/api/__init__.py +13 -0
- declib/api/artifact_dict.py +153 -0
- declib/api/artifact_lifter.py +161 -0
- declib/api/decompiler_client.py +1219 -0
- declib/api/decompiler_interface.py +1261 -0
- declib/api/decompiler_server.py +782 -0
- declib/api/server_registry.py +171 -0
- declib/api/type_definition_parser.py +201 -0
- declib/api/type_parser.py +409 -0
- declib/api/utils.py +31 -0
- declib/artifacts/__init__.py +93 -0
- declib/artifacts/artifact.py +311 -0
- declib/artifacts/comment.py +49 -0
- declib/artifacts/context.py +61 -0
- declib/artifacts/decompilation.py +35 -0
- declib/artifacts/enum.py +53 -0
- declib/artifacts/formatting.py +27 -0
- declib/artifacts/func.py +433 -0
- declib/artifacts/global_variable.py +31 -0
- declib/artifacts/patch.py +49 -0
- declib/artifacts/segment.py +37 -0
- declib/artifacts/stack_variable.py +50 -0
- declib/artifacts/struct.py +184 -0
- declib/artifacts/typedef.py +59 -0
- declib/cli/__init__.py +3 -0
- declib/cli/decompiler_cli.py +1487 -0
- declib/configuration.py +184 -0
- declib/decompiler_stubs/__init__.py +0 -0
- declib/decompiler_stubs/angr_declib/__init__.py +4 -0
- declib/decompiler_stubs/binja_declib/__init__.py +4 -0
- declib/decompiler_stubs/ida_declib.py +8 -0
- declib/decompilers/__init__.py +8 -0
- declib/decompilers/angr/__init__.py +11 -0
- declib/decompilers/angr/artifact_lifter.py +46 -0
- declib/decompilers/angr/compat.py +262 -0
- declib/decompilers/angr/interface.py +949 -0
- declib/decompilers/binja/__init__.py +0 -0
- declib/decompilers/binja/artifact_lifter.py +32 -0
- declib/decompilers/binja/hooks.py +201 -0
- declib/decompilers/binja/interface.py +795 -0
- declib/decompilers/ghidra/__init__.py +0 -0
- declib/decompilers/ghidra/artifact_lifter.py +60 -0
- declib/decompilers/ghidra/compat/__init__.py +0 -0
- declib/decompilers/ghidra/compat/headless.py +156 -0
- declib/decompilers/ghidra/compat/imports.py +78 -0
- declib/decompilers/ghidra/compat/state.py +54 -0
- declib/decompilers/ghidra/compat/transaction.py +30 -0
- declib/decompilers/ghidra/hooks.py +242 -0
- declib/decompilers/ghidra/interface.py +1433 -0
- declib/decompilers/ida/__init__.py +0 -0
- declib/decompilers/ida/artifact_lifter.py +51 -0
- declib/decompilers/ida/compat.py +2054 -0
- declib/decompilers/ida/hooks.py +700 -0
- declib/decompilers/ida/ida_ui.py +80 -0
- declib/decompilers/ida/interface.py +659 -0
- declib/logger.py +101 -0
- declib/plugin_installer.py +259 -0
- declib/skills/__init__.py +24 -0
- declib/skills/decompiler/SKILL.md +316 -0
- declib/ui/__init__.py +33 -0
- declib/ui/qt_objects.py +146 -0
- declib/ui/utils.py +115 -0
- declib/ui/version.py +14 -0
- declib-3.8.0.dist-info/METADATA +138 -0
- declib-3.8.0.dist-info/RECORD +71 -0
- declib-3.8.0.dist-info/WHEEL +5 -0
- declib-3.8.0.dist-info/entry_points.txt +3 -0
- declib-3.8.0.dist-info/licenses/LICENSE +24 -0
- 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
|