1
+ #!/usr/bin/env python3
2
+
1
3
import socket
2
4
import json
3
5
4
6
HOST = "127.0.0.1"
5
7
PORT = 5678
6
- DEPTH_LIMIT = 3 # 2 # How many levels deep to fetch variables
8
+ DEPTH_LIMIT = 3 # How many levels deep to fetch variables
7
9
8
10
9
11
def read_line (sock ):
@@ -36,7 +38,6 @@ def read_dap_message(sock):
36
38
Reads and returns one DAP message from the socket as a Python dict.
37
39
Raises ConnectionError if the socket is closed or data is invalid.
38
40
"""
39
- # Read headers until blank line
40
41
headers = {}
41
42
while True :
42
43
line = read_line (sock )
@@ -85,7 +86,6 @@ def fetch_variables(sock, seq, var_ref):
85
86
if msg .get ("type" ) == "response" and msg .get ("command" ) == "variables" :
86
87
variables_response = msg
87
88
else :
88
- # Just log and continue
89
89
print ("Got message (waiting for variables):" , msg )
90
90
91
91
vars_body = variables_response .get ("body" , {})
@@ -95,71 +95,83 @@ def fetch_variables(sock, seq, var_ref):
95
95
96
96
def fetch_variable_tree (sock , seq , var_ref , depth = DEPTH_LIMIT , visited = None ):
97
97
"""
98
- Recursively fetches a tree of variables up to a certain depth.
99
- - var_ref: The DAP variablesReference to expand.
100
- - depth: How many levels deep to recurse.
101
- - visited: A set of references we've already expanded, to avoid cycles.
102
-
103
- Returns (updated_seq, list_of_trees).
98
+ Recursively fetches a tree of variables up to 'depth' levels.
104
99
105
- Each item in list_of_trees is a dict:
100
+ Each returned item is a dict:
106
101
{
107
102
"name": str,
108
103
"value": str,
109
104
"type": str,
110
105
"evaluateName": str or None,
111
106
"variablesReference": int,
112
- "children": [ ... nested items ... ]
107
+ "children": [...]
113
108
}
114
109
"""
115
110
if visited is None :
116
111
visited = set ()
117
112
118
- # If we've already visited this reference, skip to avoid infinite loop
113
+ # Prevent infinite recursion on cyclical references
119
114
if var_ref in visited :
120
115
return seq , [
121
- {"name" : "<recursive>" , "value" : "..." , "type" : "recursive" , "children" : []}
116
+ {
117
+ "name" : "<recursive>" ,
118
+ "value" : "..." ,
119
+ "type" : "recursive" ,
120
+ "evaluateName" : None ,
121
+ "variablesReference" : 0 ,
122
+ "children" : [],
123
+ }
122
124
]
123
125
124
126
visited .add (var_ref )
125
127
126
- # Always do a single-level " variables" fetch
128
+ # 1) Fetch immediate child variables at this level
127
129
seq , vars_list = fetch_variables (sock , seq , var_ref )
128
130
129
131
result = []
130
132
for v in vars_list :
131
- child = {
133
+ child_ref = v .get ("variablesReference" , 0 )
134
+ item = {
132
135
"name" : v ["name" ],
133
136
"value" : v .get ("value" , "" ),
134
137
"type" : v .get ("type" , "" ),
135
138
"evaluateName" : v .get ("evaluateName" ),
136
- "variablesReference" : v . get ( "variablesReference" , 0 ) ,
139
+ "variablesReference" : child_ref ,
137
140
"children" : [],
138
141
}
139
142
140
- # If this variable has nested children, and we still have depth left
141
- child_ref = child ["variablesReference" ]
142
- if child_ref and child_ref > 0 and depth > 0 :
143
- # Recursively fetch child variables
143
+ # If this variable itself has children, recurse (within depth)
144
+ if child_ref > 0 and depth > 0 :
144
145
seq , child_vars = fetch_variable_tree (
145
146
sock , seq , child_ref , depth = depth - 1 , visited = visited
146
147
)
147
- child ["children" ] = child_vars
148
+ item ["children" ] = child_vars
148
149
149
- result .append (child )
150
+ result .append (item )
150
151
151
152
return seq , result
152
153
153
154
154
155
def dap_client ():
155
- """Example DAP client that pauses the main thread and fetches local/global variables with children."""
156
+ """
157
+ Example DAP client that:
158
+ 1. Connects to debugpy,
159
+ 2. Attaches to a running Python script,
160
+ 3. Sends configurationDone,
161
+ 4. Pauses the first thread,
162
+ 5. Reads the stack trace,
163
+ 6. For each frame, fetches all scopes (locals, globals, closures, etc.),
164
+ 7. Recursively expands variables up to DEPTH_LIMIT,
165
+ 8. Returns a structure with all frames and scopes.
166
+ """
167
+
156
168
print (f"Connecting to { HOST } :{ PORT } ..." )
157
169
sock = socket .create_connection ((HOST , PORT ))
158
170
sock .settimeout (10.0 )
159
171
160
172
seq = 1
161
173
162
- # 1) initialize
174
+ # 1) " initialize"
163
175
seq = send_dap_request (
164
176
sock ,
165
177
seq ,
@@ -185,11 +197,11 @@ def dap_client():
185
197
else :
186
198
print ("Got message (before initialize response):" , msg )
187
199
188
- # 2) attach
200
+ # 2) " attach"
189
201
seq = send_dap_request (sock , seq , "attach" , {"subProcess" : False })
190
202
print ("Sent 'attach' request." )
191
203
192
- # 3) configurationDone
204
+ # 3) " configurationDone"
193
205
print ("Sending 'configurationDone' request..." )
194
206
seq = send_dap_request (sock , seq , "configurationDone" )
195
207
print ("Sent 'configurationDone' request." )
@@ -203,7 +215,7 @@ def dap_client():
203
215
else :
204
216
print ("Got message (waiting for configurationDone):" , msg )
205
217
206
- # 4) threads
218
+ # 4) " threads"
207
219
seq = send_dap_request (sock , seq , "threads" )
208
220
print ("Sent 'threads' request." )
209
221
@@ -222,9 +234,9 @@ def dap_client():
222
234
if not threads_list :
223
235
print ("No threads. Exiting." )
224
236
sock .close ()
225
- return {}
237
+ return {"frames" : [] }
226
238
227
- # Pause the first thread so we can inspect variables
239
+ # Pause the first thread to ensure we can see meaningful variable data
228
240
thread_id = threads_list [0 ]["id" ]
229
241
print (f"Pausing thread { thread_id } ..." )
230
242
@@ -241,9 +253,16 @@ def dap_client():
241
253
else :
242
254
print ("Got message while waiting to pause:" , msg )
243
255
244
- # Now that thread is paused, ask for "stackTrace"
256
+ # 5) "stackTrace"
245
257
seq = send_dap_request (
246
- sock , seq , "stackTrace" , {"threadId" : thread_id , "startFrame" : 0 , "levels" : 20 }
258
+ sock ,
259
+ seq ,
260
+ "stackTrace" ,
261
+ {
262
+ "threadId" : thread_id ,
263
+ "startFrame" : 0 ,
264
+ "levels" : 50 , # raise if you suspect more frames
265
+ },
247
266
)
248
267
stack_trace_response = None
249
268
while not stack_trace_response :
@@ -253,20 +272,17 @@ def dap_client():
253
272
else :
254
273
print ("Got message (waiting for stackTrace):" , msg )
255
274
275
+ frames_data = []
256
276
frames = stack_trace_response ["body" ].get ("stackFrames" , [])
257
- print (f"Found { len (frames )} frames." )
277
+ print (f"Found { len (frames )} frames in stackTrace ." )
258
278
259
- globals_result = []
260
- locals_result = []
279
+ for frame_info in frames :
280
+ frame_id = frame_info ["id" ]
281
+ fn_name = frame_info ["name" ]
282
+ source_path = frame_info .get ("source" , {}).get ("path" , "no_source" )
283
+ print (f"Frame { frame_id } : { fn_name } @ { source_path } " )
261
284
262
- # Inspect the top frame's scopes, or all frames if you like
263
- for f in frames :
264
- frame_id = f ["id" ]
265
- print (
266
- f"Frame ID { frame_id } : { f ['name' ]} @ { f .get ('source' ,{}).get ('path' ,'no_source' )} "
267
- )
268
-
269
- # 1) get scopes
285
+ # 6) "scopes" for each frame
270
286
seq = send_dap_request (sock , seq , "scopes" , {"frameId" : frame_id })
271
287
scopes_response = None
272
288
while not scopes_response :
@@ -277,31 +293,37 @@ def dap_client():
277
293
print ("Got message (waiting for scopes):" , msg )
278
294
279
295
scope_list = scopes_response ["body" ].get ("scopes" , [])
296
+
297
+ # We'll store all scopes in a dict keyed by scope name
298
+ scope_dict = {}
280
299
for scope_info in scope_list :
281
- scope_name = scope_info ["name" ]
300
+ scope_name_original = scope_info ["name" ]
301
+ scope_name_lower = scope_name_original .lower ()
282
302
scope_ref = scope_info ["variablesReference" ]
283
- print (f" Scope: { scope_name } (ref={ scope_ref } )" )
284
-
285
- # 2) Recursively expand the variables in this scope
286
- seq , var_tree = fetch_variable_tree (sock , seq , scope_ref , depth = 2 )
287
-
288
- if scope_name .lower () == "locals" :
289
- locals_result .extend (var_tree )
290
- elif scope_name .lower () == "globals" :
291
- globals_result .extend (var_tree )
292
- else :
293
- print (f" (Scope '{ scope_name } ' not recognized as locals/globals)" )
303
+ print (f" Scope: { scope_name_original } (ref={ scope_ref } )" )
304
+
305
+ # Recursively expand variables in this scope
306
+ seq , var_tree = fetch_variable_tree (sock , seq , scope_ref , depth = DEPTH_LIMIT )
307
+ # Store them under the scope name (lowercased or original, your choice)
308
+ scope_dict [scope_name_lower ] = var_tree
309
+
310
+ frames_data .append (
311
+ {
312
+ "id" : frame_id ,
313
+ "functionName" : fn_name ,
314
+ "sourcePath" : source_path ,
315
+ "scopes" : scope_dict ,
316
+ }
317
+ )
294
318
295
319
print ("Done collecting variables. Closing socket." )
296
320
sock .close ()
297
321
298
- return {
299
- "globals" : globals_result ,
300
- "locals" : locals_result ,
301
- }
322
+ # Return everything
323
+ return {"frames" : frames_data }
302
324
303
325
304
326
if __name__ == "__main__" :
305
327
result = dap_client ()
306
- print ("\n === Final Expanded Variables ===\n " )
328
+ print ("\n === Final Expanded Frames ===\n " )
307
329
print (json .dumps (result , indent = 2 ))
0 commit comments