|
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(args, varargs, varkw, localsDict, |
|
91 formatarg=str, |
|
92 formatvarargs=lambda name: '*' + name, |
|
93 formatvarkw=lambda name: '**' + name, |
|
94 formatvalue=lambda value: '=' + repr(value)): |
|
95 """ |
|
96 Function to format an argument spec from the 4 values returned |
|
97 by getargvalues. |
|
98 |
|
99 @param args list of argument names |
|
100 @type list of str |
|
101 @param varargs name of the variable arguments |
|
102 @type str |
|
103 @param varkw name of the keyword arguments |
|
104 @type str |
|
105 @param localsDict reference to the local variables dictionary |
|
106 @type dict |
|
107 @param formatarg argument formatting function |
|
108 @type func |
|
109 @param formatvarargs variable arguments formatting function |
|
110 @type func |
|
111 @param formatvarkw keyword arguments formatting function |
|
112 @type func |
|
113 @param formatvalue value formating functtion |
|
114 @type func |
|
115 @return formatted call signature |
|
116 @rtype str |
|
117 """ |
|
118 specs = [] |
|
119 for i in range(len(args)): |
|
120 name = args[i] |
|
121 specs.append(formatarg(name) + formatvalue(localsDict[name])) |
|
122 if varargs: |
|
123 specs.append(formatvarargs(varargs) + formatvalue(localsDict[varargs])) |
|
124 if varkw: |
|
125 specs.append(formatvarkw(varkw) + formatvalue(localsDict[varkw])) |
|
126 argvalues = '(' + ', '.join(specs) + ')' |
|
127 if '__return__' in localsDict: |
|
128 argvalues += " -> " + formatvalue(localsDict['__return__']) |
|
129 return argvalues |
|
130 |
|
131 |
|
132 def prepareJsonCommand(method, params): |
|
133 """ |
|
134 Function to prepare a single command or response for transmission to |
|
135 the IDE. |
|
136 |
|
137 @param method command or response name to be sent |
|
138 @type str |
|
139 @param params dictionary of named parameters for the command or response |
|
140 @type dict |
|
141 @return prepared JSON command or response string |
|
142 @rtype str |
|
143 """ |
|
144 commandDict = { |
|
145 "jsonrpc": "2.0", |
|
146 "method": method, |
|
147 "params": params, |
|
148 } |
|
149 return json.dumps(commandDict) + '\n' |
|
150 |
|
151 ########################################################################### |
|
152 ## Things related to monkey patching below |
|
153 ########################################################################### |
|
154 |
|
155 |
|
156 PYTHON_NAMES = ["python", "pypy"] |
|
157 |
|
158 |
|
159 def isWindowsPlatform(): |
|
160 """ |
|
161 Function to check, if this is a Windows platform. |
|
162 |
|
163 @return flag indicating Windows platform |
|
164 @rtype bool |
|
165 """ |
|
166 return sys.platform.startswith(("win", "cygwin")) |
|
167 |
|
168 |
|
169 def isExecutable(program): |
|
170 """ |
|
171 Function to check, if the given program is executable. |
|
172 |
|
173 @param program program path to be checked |
|
174 @type str |
|
175 @return flag indicating an executable program |
|
176 @rtype bool |
|
177 """ |
|
178 return os.access(os.path.abspath(program), os.X_OK) |
|
179 |
|
180 |
|
181 def startsWithShebang(program): |
|
182 """ |
|
183 Function to check, if the given program start with a Shebang line. |
|
184 |
|
185 @param program program path to be checked |
|
186 @type str |
|
187 @return flag indicating an existing and valid shebang line |
|
188 @rtype bool |
|
189 """ |
|
190 try: |
|
191 if os.path.exists(program): |
|
192 with open(program) as f: |
|
193 for line in f: |
|
194 line = line.strip() |
|
195 if line: |
|
196 for name in PYTHON_NAMES: # __IGNORE_WARNING_Y110__ |
|
197 if ( |
|
198 line.startswith( |
|
199 '#!/usr/bin/env {0}'.format(name)) or |
|
200 (line.startswith('#!') and name in line) |
|
201 ): |
|
202 return True |
|
203 return False |
|
204 else: |
|
205 return False |
|
206 except UnicodeDecodeError: |
|
207 return False |
|
208 except Exception: |
|
209 traceback.print_exc() |
|
210 return False |
|
211 |
|
212 |
|
213 def isPythonProgram(program): |
|
214 """ |
|
215 Function to check, if the given program is a Python interpreter or |
|
216 program. |
|
217 |
|
218 @param program program to be checked |
|
219 @type str |
|
220 @return flag indicating a Python interpreter or program |
|
221 @rtype bool |
|
222 """ |
|
223 if not program: |
|
224 return False |
|
225 |
|
226 prog = os.path.basename(program).lower() |
|
227 if any(pyname in prog for pyname in PYTHON_NAMES): |
|
228 return True |
|
229 |
|
230 return ( |
|
231 not isWindowsPlatform() and |
|
232 isExecutable(program) and |
|
233 startsWithShebang(program) |
|
234 ) |
|
235 |
|
236 |
|
237 def removeQuotesFromArgs(args): |
|
238 """ |
|
239 Function to remove quotes from the arguments list. |
|
240 |
|
241 @param args list of arguments |
|
242 @type list of str |
|
243 @return list of unquoted strings |
|
244 @rtype list of str |
|
245 """ |
|
246 if isWindowsPlatform(): |
|
247 newArgs = [] |
|
248 for x in args: |
|
249 if len(x) > 1 and x.startswith('"') and x.endswith('"'): |
|
250 x = x[1:-1] |
|
251 newArgs.append(x) |
|
252 return newArgs |
|
253 else: |
|
254 return args |
|
255 |
|
256 |
|
257 def quoteArgs(args): |
|
258 """ |
|
259 Function to quote the given list of arguments. |
|
260 |
|
261 @param args list of arguments to be quoted |
|
262 @type list of str |
|
263 @return list of quoted arguments |
|
264 @rtype list of str |
|
265 """ |
|
266 if isWindowsPlatform(): |
|
267 quotedArgs = [] |
|
268 for x in args: |
|
269 if x.startswith('"') and x.endswith('"'): |
|
270 quotedArgs.append(x) |
|
271 else: |
|
272 if ' ' in x: |
|
273 x = x.replace('"', '\\"') |
|
274 quotedArgs.append('"{0}"'.format(x)) |
|
275 else: |
|
276 quotedArgs.append(x) |
|
277 return quotedArgs |
|
278 else: |
|
279 return args |
|
280 |
|
281 |
|
282 def patchArguments(debugClient, arguments, noRedirect=False): |
|
283 """ |
|
284 Function to patch the arguments given to start a program in order to |
|
285 execute it in our debugger. |
|
286 |
|
287 @param debugClient reference to the debug client object |
|
288 @type DebugClient |
|
289 @param arguments list of program arguments |
|
290 @type list of str |
|
291 @param noRedirect flag indicating to not redirect stdin and stdout |
|
292 @type bool |
|
293 @return modified argument list |
|
294 @rtype list of str |
|
295 """ |
|
296 debugClientScript = os.path.join( |
|
297 os.path.dirname(__file__), "DebugClient.py") |
|
298 if debugClientScript in arguments: |
|
299 # it is already patched |
|
300 return arguments |
|
301 |
|
302 args = list(arguments[:]) # create a copy of the arguments list |
|
303 args = removeQuotesFromArgs(args) |
|
304 |
|
305 # support for shebang line |
|
306 program = os.path.basename(args[0]).lower() |
|
307 for pyname in PYTHON_NAMES: |
|
308 if pyname in program: |
|
309 break |
|
310 else: |
|
311 if ( |
|
312 (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 (wd, host, port, exceptions, tracePython, redirect, noencoding |
|
350 ) = debugClient.startOptions[:7] |
|
351 |
|
352 modifiedArguments = [interpreter] |
|
353 modifiedArguments.extend(interpreterArgs) |
|
354 modifiedArguments.extend([ |
|
355 debugClientScript, |
|
356 "-h", host, |
|
357 "-p", str(port), |
|
358 "--no-passive", |
|
359 ]) |
|
360 |
|
361 if wd: |
|
362 modifiedArguments.extend(["-w", wd]) |
|
363 if not exceptions: |
|
364 modifiedArguments.append("-e") |
|
365 if tracePython: |
|
366 modifiedArguments.append("-t") |
|
367 if noRedirect or not redirect: |
|
368 modifiedArguments.append("-n") |
|
369 if noencoding: |
|
370 modifiedArguments.append("--no-encoding") |
|
371 if debugClient.multiprocessSupport: |
|
372 modifiedArguments.append("--multiprocess") |
|
373 if hasCode: |
|
374 modifiedArguments.append("--code") |
|
375 modifiedArguments.append(args.pop(0)) |
|
376 if hasScriptModule: |
|
377 modifiedArguments.append("--module") |
|
378 modifiedArguments.append(args.pop(0)) |
|
379 modifiedArguments.append("--") |
|
380 # end the arguments for DebugClient |
|
381 |
|
382 # append the arguments for the program to be debugged |
|
383 modifiedArguments.extend(args) |
|
384 modifiedArguments = quoteArgs(modifiedArguments) |
|
385 |
|
386 return modifiedArguments |
|
387 |
|
388 |
|
389 def stringToArgumentsWindows(args): |
|
390 """ |
|
391 Function to prepare a string of arguments for Windows platform. |
|
392 |
|
393 @param args list of command arguments |
|
394 @type str |
|
395 @return list of command arguments |
|
396 @rtype list of str |
|
397 @exception RuntimeError raised to indicate an illegal arguments parsing |
|
398 condition |
|
399 """ |
|
400 # see http://msdn.microsoft.com/en-us/library/a1y7w461.aspx |
|
401 result = [] |
|
402 |
|
403 DEFAULT = 0 |
|
404 ARG = 1 |
|
405 IN_DOUBLE_QUOTE = 2 |
|
406 |
|
407 state = DEFAULT |
|
408 backslashes = 0 |
|
409 buf = '' |
|
410 |
|
411 argsLen = len(args) |
|
412 i = 0 |
|
413 while i < argsLen: |
|
414 ch = args[i] |
|
415 if ch == '\\': |
|
416 backslashes += 1 |
|
417 i += 1 |
|
418 continue |
|
419 elif backslashes != 0: |
|
420 if ch == '"': |
|
421 while backslashes >= 2: |
|
422 backslashes -= 2 |
|
423 buf += '\\' |
|
424 if backslashes == 1: |
|
425 if state == DEFAULT: |
|
426 state = ARG |
|
427 |
|
428 buf += '"' |
|
429 backslashes = 0 |
|
430 i += 1 |
|
431 continue |
|
432 else: |
|
433 # false alarm, treat passed backslashes literally... |
|
434 if state == DEFAULT: |
|
435 state = ARG |
|
436 |
|
437 while backslashes > 0: |
|
438 backslashes -= 1 |
|
439 buf += '\\' |
|
440 |
|
441 if ch in (' ', '\t'): |
|
442 if state == DEFAULT: |
|
443 # skip |
|
444 i += 1 |
|
445 continue |
|
446 elif state == ARG: |
|
447 state = DEFAULT |
|
448 result.append(buf) |
|
449 buf = '' |
|
450 i += 1 |
|
451 continue |
|
452 |
|
453 if state not in (DEFAULT, ARG, IN_DOUBLE_QUOTE): |
|
454 raise RuntimeError('Illegal condition') |
|
455 |
|
456 if state == IN_DOUBLE_QUOTE: |
|
457 if ch == '"': |
|
458 if i + 1 < argsLen and args[i + 1] == '"': |
|
459 # Undocumented feature in Windows: |
|
460 # Two consecutive double quotes inside a double-quoted |
|
461 # argument are interpreted as a single double quote. |
|
462 buf += '"' |
|
463 i += 1 |
|
464 elif len(buf) == 0: |
|
465 result.append("\"\"") |
|
466 state = DEFAULT |
|
467 else: |
|
468 state = ARG |
|
469 else: |
|
470 buf += ch |
|
471 |
|
472 else: |
|
473 if ch == '"': |
|
474 state = IN_DOUBLE_QUOTE |
|
475 else: |
|
476 state = ARG |
|
477 buf += ch |
|
478 |
|
479 i += 1 |
|
480 |
|
481 if len(buf) > 0 or state != DEFAULT: |
|
482 result.append(buf) |
|
483 |
|
484 return result |
|
485 |
|
486 |
|
487 def patchArgumentStringWindows(debugClient, argStr): |
|
488 """ |
|
489 Function to patch an argument string for Windows. |
|
490 |
|
491 @param debugClient reference to the debug client object |
|
492 @type DebugClient |
|
493 @param argStr argument string |
|
494 @type str |
|
495 @return patched argument string |
|
496 @rtype str |
|
497 """ |
|
498 args = stringToArgumentsWindows(argStr) |
|
499 if not args or not isPythonProgram(args[0]): |
|
500 return argStr |
|
501 |
|
502 argStr = ' '.join(patchArguments(debugClient, args)) |
|
503 return argStr |