1+ #!/usr/bin/env python3
2+
13import socket
24import json
35
46HOST = "127.0.0.1"
57PORT = 5678
6- DEPTH_LIMIT = 3 # 2 # How many levels deep to fetch variables
8+ DEPTH_LIMIT = 3 # How many levels deep to fetch variables
79
810
911def read_line (sock ):
@@ -36,7 +38,6 @@ def read_dap_message(sock):
3638 Reads and returns one DAP message from the socket as a Python dict.
3739 Raises ConnectionError if the socket is closed or data is invalid.
3840 """
39- # Read headers until blank line
4041 headers = {}
4142 while True :
4243 line = read_line (sock )
@@ -85,7 +86,6 @@ def fetch_variables(sock, seq, var_ref):
8586 if msg .get ("type" ) == "response" and msg .get ("command" ) == "variables" :
8687 variables_response = msg
8788 else :
88- # Just log and continue
8989 print ("Got message (waiting for variables):" , msg )
9090
9191 vars_body = variables_response .get ("body" , {})
@@ -95,71 +95,83 @@ def fetch_variables(sock, seq, var_ref):
9595
9696def fetch_variable_tree (sock , seq , var_ref , depth = DEPTH_LIMIT , visited = None ):
9797 """
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.
10499
105- Each item in list_of_trees is a dict:
100+ Each returned item is a dict:
106101 {
107102 "name": str,
108103 "value": str,
109104 "type": str,
110105 "evaluateName": str or None,
111106 "variablesReference": int,
112- "children": [ ... nested items ... ]
107+ "children": [...]
113108 }
114109 """
115110 if visited is None :
116111 visited = set ()
117112
118- # If we've already visited this reference, skip to avoid infinite loop
113+ # Prevent infinite recursion on cyclical references
119114 if var_ref in visited :
120115 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+ }
122124 ]
123125
124126 visited .add (var_ref )
125127
126- # Always do a single-level " variables" fetch
128+ # 1) Fetch immediate child variables at this level
127129 seq , vars_list = fetch_variables (sock , seq , var_ref )
128130
129131 result = []
130132 for v in vars_list :
131- child = {
133+ child_ref = v .get ("variablesReference" , 0 )
134+ item = {
132135 "name" : v ["name" ],
133136 "value" : v .get ("value" , "" ),
134137 "type" : v .get ("type" , "" ),
135138 "evaluateName" : v .get ("evaluateName" ),
136- "variablesReference" : v . get ( "variablesReference" , 0 ) ,
139+ "variablesReference" : child_ref ,
137140 "children" : [],
138141 }
139142
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 :
144145 seq , child_vars = fetch_variable_tree (
145146 sock , seq , child_ref , depth = depth - 1 , visited = visited
146147 )
147- child ["children" ] = child_vars
148+ item ["children" ] = child_vars
148149
149- result .append (child )
150+ result .append (item )
150151
151152 return seq , result
152153
153154
154155def 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+
156168 print (f"Connecting to { HOST } :{ PORT } ..." )
157169 sock = socket .create_connection ((HOST , PORT ))
158170 sock .settimeout (10.0 )
159171
160172 seq = 1
161173
162- # 1) initialize
174+ # 1) " initialize"
163175 seq = send_dap_request (
164176 sock ,
165177 seq ,
@@ -185,11 +197,11 @@ def dap_client():
185197 else :
186198 print ("Got message (before initialize response):" , msg )
187199
188- # 2) attach
200+ # 2) " attach"
189201 seq = send_dap_request (sock , seq , "attach" , {"subProcess" : False })
190202 print ("Sent 'attach' request." )
191203
192- # 3) configurationDone
204+ # 3) " configurationDone"
193205 print ("Sending 'configurationDone' request..." )
194206 seq = send_dap_request (sock , seq , "configurationDone" )
195207 print ("Sent 'configurationDone' request." )
@@ -203,7 +215,7 @@ def dap_client():
203215 else :
204216 print ("Got message (waiting for configurationDone):" , msg )
205217
206- # 4) threads
218+ # 4) " threads"
207219 seq = send_dap_request (sock , seq , "threads" )
208220 print ("Sent 'threads' request." )
209221
@@ -222,9 +234,9 @@ def dap_client():
222234 if not threads_list :
223235 print ("No threads. Exiting." )
224236 sock .close ()
225- return {}
237+ return {"frames" : [] }
226238
227- # Pause the first thread so we can inspect variables
239+ # Pause the first thread to ensure we can see meaningful variable data
228240 thread_id = threads_list [0 ]["id" ]
229241 print (f"Pausing thread { thread_id } ..." )
230242
@@ -241,9 +253,16 @@ def dap_client():
241253 else :
242254 print ("Got message while waiting to pause:" , msg )
243255
244- # Now that thread is paused, ask for "stackTrace"
256+ # 5) "stackTrace"
245257 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+ },
247266 )
248267 stack_trace_response = None
249268 while not stack_trace_response :
@@ -253,20 +272,17 @@ def dap_client():
253272 else :
254273 print ("Got message (waiting for stackTrace):" , msg )
255274
275+ frames_data = []
256276 frames = stack_trace_response ["body" ].get ("stackFrames" , [])
257- print (f"Found { len (frames )} frames." )
277+ print (f"Found { len (frames )} frames in stackTrace ." )
258278
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 } " )
261284
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
270286 seq = send_dap_request (sock , seq , "scopes" , {"frameId" : frame_id })
271287 scopes_response = None
272288 while not scopes_response :
@@ -277,31 +293,37 @@ def dap_client():
277293 print ("Got message (waiting for scopes):" , msg )
278294
279295 scope_list = scopes_response ["body" ].get ("scopes" , [])
296+
297+ # We'll store all scopes in a dict keyed by scope name
298+ scope_dict = {}
280299 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 ()
282302 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+ )
294318
295319 print ("Done collecting variables. Closing socket." )
296320 sock .close ()
297321
298- return {
299- "globals" : globals_result ,
300- "locals" : locals_result ,
301- }
322+ # Return everything
323+ return {"frames" : frames_data }
302324
303325
304326if __name__ == "__main__" :
305327 result = dap_client ()
306- print ("\n === Final Expanded Variables ===\n " )
328+ print ("\n === Final Expanded Frames ===\n " )
307329 print (json .dumps (result , indent = 2 ))
0 commit comments