|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing utilities functions for the debug client. |
|
8 """ |
|
9 |
|
10 import json |
|
11 import os |
|
12 import traceback |
|
13 import sys |
|
14 |
|
15 # |
|
16 # Taken from inspect.py of Python 3.4 |
|
17 # |
|
18 |
|
19 from collections import namedtuple |
|
20 from inspect import iscode, isframe |
|
21 |
|
22 # Create constants for the compiler flags in Include/code.h |
|
23 # We try to get them from dis to avoid duplication, but fall |
|
24 # back to hardcoding so the dependency is optional |
|
25 try: |
|
26 from dis import COMPILER_FLAG_NAMES |
|
27 except ImportError: |
|
28 CO_OPTIMIZED, CO_NEWLOCALS = 0x1, 0x2 |
|
29 CO_VARARGS, CO_VARKEYWORDS = 0x4, 0x8 |
|
30 CO_NESTED, CO_GENERATOR, CO_NOFREE = 0x10, 0x20, 0x40 |
|
31 else: |
|
32 mod_dict = globals() |
|
33 for k, v in COMPILER_FLAG_NAMES.items(): |
|
34 mod_dict["CO_" + v] = k |
|
35 |
|
36 ArgInfo = namedtuple("ArgInfo", "args varargs keywords locals") |
|
37 |
|
38 |
|
39 def getargvalues(frame): |
|
40 """ |
|
41 Function to get information about arguments passed into a |
|
42 particular frame. |
|
43 |
|
44 @param frame reference to a frame object to be processed |
|
45 @type frame |
|
46 @return tuple of four things, where 'args' is a list of the argument names, |
|
47 'varargs' and 'varkw' are the names of the * and ** arguments or None |
|
48 and 'locals' is the locals dictionary of the given frame. |
|
49 @exception TypeError raised if the input parameter is not a frame object |
|
50 """ |
|
51 if not isframe(frame): |
|
52 raise TypeError("{0!r} is not a frame object".format(frame)) |
|
53 |
|
54 args, varargs, kwonlyargs, varkw = _getfullargs(frame.f_code) |
|
55 return ArgInfo(args + kwonlyargs, varargs, varkw, frame.f_locals) |
|
56 |
|
57 |
|
58 def _getfullargs(co): |
|
59 """ |
|
60 Protected function to get information about the arguments accepted |
|
61 by a code object. |
|
62 |
|
63 @param co reference to a code object to be processed |
|
64 @type code |
|
65 @return tuple of four things, where 'args' and 'kwonlyargs' are lists of |
|
66 argument names, and 'varargs' and 'varkw' are the names of the |
|
67 * and ** arguments or None. |
|
68 @exception TypeError raised if the input parameter is not a code object |
|
69 """ |
|
70 if not iscode(co): |
|
71 raise TypeError("{0!r} is not a code object".format(co)) |
|
72 |
|
73 nargs = co.co_argcount |
|
74 names = co.co_varnames |
|
75 nkwargs = co.co_kwonlyargcount |
|
76 args = list(names[:nargs]) |
|
77 kwonlyargs = list(names[nargs : nargs + nkwargs]) |
|
78 |
|
79 nargs += nkwargs |
|
80 varargs = None |
|
81 if co.co_flags & CO_VARARGS: |
|
82 varargs = co.co_varnames[nargs] |
|
83 nargs += 1 |
|
84 varkw = None |
|
85 if co.co_flags & CO_VARKEYWORDS: |
|
86 varkw = co.co_varnames[nargs] |
|
87 return args, varargs, kwonlyargs, varkw |
|
88 |
|
89 |
|
90 def formatargvalues( |
|
91 args, |
|
92 varargs, |
|
93 varkw, |
|
94 localsDict, |
|
95 formatarg=str, |
|
96 formatvarargs=lambda name: "*" + name, |
|
97 formatvarkw=lambda name: "**" + name, |
|
98 formatvalue=lambda value: "=" + repr(value), |
|
99 ): |
|
100 """ |
|
101 Function to format an argument spec from the 4 values returned |
|
102 by getargvalues. |
|
103 |
|
104 @param args list of argument names |
|
105 @type list of str |
|
106 @param varargs name of the variable arguments |
|
107 @type str |
|
108 @param varkw name of the keyword arguments |
|
109 @type str |
|
110 @param localsDict reference to the local variables dictionary |
|
111 @type dict |
|
112 @param formatarg argument formatting function |
|
113 @type func |
|
114 @param formatvarargs variable arguments formatting function |
|
115 @type func |
|
116 @param formatvarkw keyword arguments formatting function |
|
117 @type func |
|
118 @param formatvalue value formating functtion |
|
119 @type func |
|
120 @return formatted call signature |
|
121 @rtype str |
|
122 """ |
|
123 specs = [] |
|
124 for i in range(len(args)): |
|
125 name = args[i] |
|
126 specs.append(formatarg(name) + formatvalue(localsDict[name])) |
|
127 if varargs: |
|
128 specs.append(formatvarargs(varargs) + formatvalue(localsDict[varargs])) |
|
129 if varkw: |
|
130 specs.append(formatvarkw(varkw) + formatvalue(localsDict[varkw])) |
|
131 argvalues = "(" + ", ".join(specs) + ")" |
|
132 if "__return__" in localsDict: |
|
133 argvalues += " -> " + formatvalue(localsDict["__return__"]) |
|
134 return argvalues |
|
135 |
|
136 |
|
137 def prepareJsonCommand(method, params): |
|
138 """ |
|
139 Function to prepare a single command or response for transmission to |
|
140 the IDE. |
|
141 |
|
142 @param method command or response name to be sent |
|
143 @type str |
|
144 @param params dictionary of named parameters for the command or response |
|
145 @type dict |
|
146 @return prepared JSON command or response string |
|
147 @rtype str |
|
148 """ |
|
149 commandDict = { |
|
150 "jsonrpc": "2.0", |
|
151 "method": method, |
|
152 "params": params, |
|
153 } |
|
154 return json.dumps(commandDict) + "\n" |
|
155 |
|
156 |
|
157 ########################################################################### |
|
158 ## Things related to monkey patching below |
|
159 ########################################################################### |
|
160 |
|
161 |
|
162 PYTHON_NAMES = ["python", "pypy"] |
|
163 |
|
164 |
|
165 def isWindowsPlatform(): |
|
166 """ |
|
167 Function to check, if this is a Windows platform. |
|
168 |
|
169 @return flag indicating Windows platform |
|
170 @rtype bool |
|
171 """ |
|
172 return sys.platform.startswith(("win", "cygwin")) |
|
173 |
|
174 |
|
175 def isExecutable(program): |
|
176 """ |
|
177 Function to check, if the given program is executable. |
|
178 |
|
179 @param program program path to be checked |
|
180 @type str |
|
181 @return flag indicating an executable program |
|
182 @rtype bool |
|
183 """ |
|
184 return os.access(os.path.abspath(program), os.X_OK) |
|
185 |
|
186 |
|
187 def startsWithShebang(program): |
|
188 """ |
|
189 Function to check, if the given program start with a Shebang line. |
|
190 |
|
191 @param program program path to be checked |
|
192 @type str |
|
193 @return flag indicating an existing and valid shebang line |
|
194 @rtype bool |
|
195 """ |
|
196 try: |
|
197 if os.path.exists(program): |
|
198 with open(program) as f: |
|
199 for line in f: |
|
200 line = line.strip() |
|
201 if line: |
|
202 for name in PYTHON_NAMES: # __IGNORE_WARNING_Y110__ |
|
203 if line.startswith("#!/usr/bin/env {0}".format(name)) or ( |
|
204 line.startswith("#!") and name in line |
|
205 ): |
|
206 return True |
|
207 return False |
|
208 else: |
|
209 return False |
|
210 except UnicodeDecodeError: |
|
211 return False |
|
212 except Exception: |
|
213 traceback.print_exc() |
|
214 return False |
|
215 |
|
216 |
|
217 def isPythonProgram(program): |
|
218 """ |
|
219 Function to check, if the given program is a Python interpreter or |
|
220 program. |
|
221 |
|
222 @param program program to be checked |
|
223 @type str |
|
224 @return flag indicating a Python interpreter or program |
|
225 @rtype bool |
|
226 """ |
|
227 if not program: |
|
228 return False |
|
229 |
|
230 prog = os.path.basename(program).lower() |
|
231 if any(pyname in prog for pyname in PYTHON_NAMES): |
|
232 return True |
|
233 |
|
234 return ( |
|
235 not isWindowsPlatform() and isExecutable(program) and startsWithShebang(program) |
|
236 ) |
|
237 |
|
238 |
|
239 def removeQuotesFromArgs(args): |
|
240 """ |
|
241 Function to remove quotes from the arguments list. |
|
242 |
|
243 @param args list of arguments |
|
244 @type list of str |
|
245 @return list of unquoted strings |
|
246 @rtype list of str |
|
247 """ |
|
248 if isWindowsPlatform(): |
|
249 newArgs = [] |
|
250 for x in args: |
|
251 if len(x) > 1 and x.startswith('"') and x.endswith('"'): |
|
252 x = x[1:-1] |
|
253 newArgs.append(x) |
|
254 return newArgs |
|
255 else: |
|
256 return args |
|
257 |
|
258 |
|
259 def quoteArgs(args): |
|
260 """ |
|
261 Function to quote the given list of arguments. |
|
262 |
|
263 @param args list of arguments to be quoted |
|
264 @type list of str |
|
265 @return list of quoted arguments |
|
266 @rtype list of str |
|
267 """ |
|
268 if isWindowsPlatform(): |
|
269 quotedArgs = [] |
|
270 for x in args: |
|
271 if x.startswith('"') and x.endswith('"'): |
|
272 quotedArgs.append(x) |
|
273 else: |
|
274 if " " in x: |
|
275 x = x.replace('"', '\\"') |
|
276 quotedArgs.append('"{0}"'.format(x)) |
|
277 else: |
|
278 quotedArgs.append(x) |
|
279 return quotedArgs |
|
280 else: |
|
281 return args |
|
282 |
|
283 |
|
284 def patchArguments(debugClient, arguments, noRedirect=False): |
|
285 """ |
|
286 Function to patch the arguments given to start a program in order to |
|
287 execute it in our debugger. |
|
288 |
|
289 @param debugClient reference to the debug client object |
|
290 @type DebugClient |
|
291 @param arguments list of program arguments |
|
292 @type list of str |
|
293 @param noRedirect flag indicating to not redirect stdin and stdout |
|
294 @type bool |
|
295 @return modified argument list |
|
296 @rtype list of str |
|
297 """ |
|
298 debugClientScript = os.path.join(os.path.dirname(__file__), "DebugClient.py") |
|
299 if debugClientScript in arguments: |
|
300 # it is already patched |
|
301 return arguments |
|
302 |
|
303 args = list(arguments[:]) # create a copy of the arguments list |
|
304 args = removeQuotesFromArgs(args) |
|
305 |
|
306 # support for shebang line |
|
307 program = os.path.basename(args[0]).lower() |
|
308 for pyname in PYTHON_NAMES: |
|
309 if pyname in program: |
|
310 break |
|
311 else: |
|
312 if (not isWindowsPlatform() and startsWithShebang(args[0])) or ( |
|
313 isWindowsPlatform() and args[0].lower().endswith(".py") |
|
314 ): |
|
315 # 1. insert our interpreter as first argument if not Windows |
|
316 # 2. insert our interpreter as first argument if on Windows and |
|
317 # it is a Python script |
|
318 args.insert(0, sys.executable) |
|
319 |
|
320 # extract list of interpreter arguments, i.e. all arguments before the |
|
321 # first one not starting with '-'. |
|
322 interpreter = args.pop(0) |
|
323 interpreterArgs = [] |
|
324 hasCode = False |
|
325 hasScriptModule = False |
|
326 while args: |
|
327 if args[0].startswith("-"): |
|
328 if args[0] in ("-W", "-X"): |
|
329 # take two elements off the list |
|
330 interpreterArgs.append(args.pop(0)) |
|
331 interpreterArgs.append(args.pop(0)) |
|
332 elif args[0] == "-c": |
|
333 # -c indicates code to be executed and ends the |
|
334 # arguments list |
|
335 args.pop(0) |
|
336 hasCode = True |
|
337 break |
|
338 elif args[0] == "-m": |
|
339 # -m indicates a module to be executed as a script |
|
340 # and ends the arguments list |
|
341 args.pop(0) |
|
342 hasScriptModule = True |
|
343 break |
|
344 else: |
|
345 interpreterArgs.append(args.pop(0)) |
|
346 else: |
|
347 break |
|
348 |
|
349 ( |
|
350 wd, |
|
351 host, |
|
352 port, |
|
353 exceptions, |
|
354 tracePython, |
|
355 redirect, |
|
356 noencoding, |
|
357 ) = debugClient.startOptions[:7] |
|
358 |
|
359 modifiedArguments = [interpreter] |
|
360 modifiedArguments.extend(interpreterArgs) |
|
361 modifiedArguments.extend( |
|
362 [ |
|
363 debugClientScript, |
|
364 "-h", |
|
365 host, |
|
366 "-p", |
|
367 str(port), |
|
368 "--no-passive", |
|
369 ] |
|
370 ) |
|
371 |
|
372 if wd: |
|
373 modifiedArguments.extend(["-w", wd]) |
|
374 if not exceptions: |
|
375 modifiedArguments.append("-e") |
|
376 if tracePython: |
|
377 modifiedArguments.append("-t") |
|
378 if noRedirect or not redirect: |
|
379 modifiedArguments.append("-n") |
|
380 if noencoding: |
|
381 modifiedArguments.append("--no-encoding") |
|
382 if debugClient.multiprocessSupport: |
|
383 modifiedArguments.append("--multiprocess") |
|
384 if hasCode: |
|
385 modifiedArguments.append("--code") |
|
386 modifiedArguments.append(args.pop(0)) |
|
387 if hasScriptModule: |
|
388 modifiedArguments.append("--module") |
|
389 modifiedArguments.append(args.pop(0)) |
|
390 modifiedArguments.append("--") |
|
391 # end the arguments for DebugClient |
|
392 |
|
393 # append the arguments for the program to be debugged |
|
394 modifiedArguments.extend(args) |
|
395 modifiedArguments = quoteArgs(modifiedArguments) |
|
396 |
|
397 return modifiedArguments |
|
398 |
|
399 |
|
400 def stringToArgumentsWindows(args): |
|
401 """ |
|
402 Function to prepare a string of arguments for Windows platform. |
|
403 |
|
404 @param args list of command arguments |
|
405 @type str |
|
406 @return list of command arguments |
|
407 @rtype list of str |
|
408 @exception RuntimeError raised to indicate an illegal arguments parsing |
|
409 condition |
|
410 """ |
|
411 # see http://msdn.microsoft.com/en-us/library/a1y7w461.aspx |
|
412 result = [] |
|
413 |
|
414 DEFAULT = 0 |
|
415 ARG = 1 |
|
416 IN_DOUBLE_QUOTE = 2 |
|
417 |
|
418 state = DEFAULT |
|
419 backslashes = 0 |
|
420 buf = "" |
|
421 |
|
422 argsLen = len(args) |
|
423 i = 0 |
|
424 while i < argsLen: |
|
425 ch = args[i] |
|
426 if ch == "\\": |
|
427 backslashes += 1 |
|
428 i += 1 |
|
429 continue |
|
430 elif backslashes != 0: |
|
431 if ch == '"': |
|
432 while backslashes >= 2: |
|
433 backslashes -= 2 |
|
434 buf += "\\" |
|
435 if backslashes == 1: |
|
436 if state == DEFAULT: |
|
437 state = ARG |
|
438 |
|
439 buf += '"' |
|
440 backslashes = 0 |
|
441 i += 1 |
|
442 continue |
|
443 else: |
|
444 # false alarm, treat passed backslashes literally... |
|
445 if state == DEFAULT: |
|
446 state = ARG |
|
447 |
|
448 while backslashes > 0: |
|
449 backslashes -= 1 |
|
450 buf += "\\" |
|
451 |
|
452 if ch in (" ", "\t"): |
|
453 if state == DEFAULT: |
|
454 # skip |
|
455 i += 1 |
|
456 continue |
|
457 elif state == ARG: |
|
458 state = DEFAULT |
|
459 result.append(buf) |
|
460 buf = "" |
|
461 i += 1 |
|
462 continue |
|
463 |
|
464 if state not in (DEFAULT, ARG, IN_DOUBLE_QUOTE): |
|
465 raise RuntimeError("Illegal condition") |
|
466 |
|
467 if state == IN_DOUBLE_QUOTE: |
|
468 if ch == '"': |
|
469 if i + 1 < argsLen and args[i + 1] == '"': |
|
470 # Undocumented feature in Windows: |
|
471 # Two consecutive double quotes inside a double-quoted |
|
472 # argument are interpreted as a single double quote. |
|
473 buf += '"' |
|
474 i += 1 |
|
475 elif len(buf) == 0: |
|
476 result.append('""') |
|
477 state = DEFAULT |
|
478 else: |
|
479 state = ARG |
|
480 else: |
|
481 buf += ch |
|
482 |
|
483 else: |
|
484 if ch == '"': |
|
485 state = IN_DOUBLE_QUOTE |
|
486 else: |
|
487 state = ARG |
|
488 buf += ch |
|
489 |
|
490 i += 1 |
|
491 |
|
492 if len(buf) > 0 or state != DEFAULT: |
|
493 result.append(buf) |
|
494 |
|
495 return result |
|
496 |
|
497 |
|
498 def patchArgumentStringWindows(debugClient, argStr): |
|
499 """ |
|
500 Function to patch an argument string for Windows. |
|
501 |
|
502 @param debugClient reference to the debug client object |
|
503 @type DebugClient |
|
504 @param argStr argument string |
|
505 @type str |
|
506 @return patched argument string |
|
507 @rtype str |
|
508 """ |
|
509 args = stringToArgumentsWindows(argStr) |
|
510 if not args or not isPythonProgram(args[0]): |
|
511 return argStr |
|
512 |
|
513 argStr = " ".join(patchArguments(debugClient, args)) |
|
514 return argStr |