1 | #!/usr/bin/env python3 |
---|
2 | # coding:utf-8 |
---|
3 | |
---|
4 | "Queues(Pipe)-based independent remote client-server Python Debugger (new-py3)" |
---|
5 | |
---|
6 | from __future__ import print_function |
---|
7 | |
---|
8 | __author__ = "Mariano Reingart (reingart@gmail.com)" |
---|
9 | __copyright__ = "Copyright (C) 2011 Mariano Reingart" |
---|
10 | __license__ = "LGPL 3.0" |
---|
11 | __version__ = "1.5.2" |
---|
12 | |
---|
13 | # remote debugger queue-based (jsonrpc-like interface): |
---|
14 | # - bidirectional communication (request - response calls in both ways) |
---|
15 | # - request with id == null is a notification (do not send a response) |
---|
16 | # - request with a value for id is a normal call, wait response |
---|
17 | # based on idle, inspired by pythonwin implementation, taken many code from pdb |
---|
18 | |
---|
19 | import bdb |
---|
20 | import inspect |
---|
21 | import linecache |
---|
22 | import os |
---|
23 | import sys |
---|
24 | import traceback |
---|
25 | import cmd |
---|
26 | import pydoc |
---|
27 | import threading |
---|
28 | import collections |
---|
29 | |
---|
30 | |
---|
31 | # Speed Ups: global variables |
---|
32 | breaks = [] |
---|
33 | |
---|
34 | |
---|
35 | class Qdb(bdb.Bdb): |
---|
36 | "Qdb Debugger Backend" |
---|
37 | |
---|
38 | def __init__(self, pipe, redirect_stdio=True, allow_interruptions=False, |
---|
39 | use_speedups=True, skip=[__name__]): |
---|
40 | global breaks |
---|
41 | kwargs = {} |
---|
42 | if sys.version_info > (2, 7): |
---|
43 | kwargs['skip'] = skip |
---|
44 | bdb.Bdb.__init__(self, **kwargs) |
---|
45 | self.frame = None |
---|
46 | self.i = 1 # sequential RPC call id |
---|
47 | self.waiting = False |
---|
48 | self.pipe = pipe # for communication |
---|
49 | self._wait_for_mainpyfile = False |
---|
50 | self._wait_for_breakpoint = False |
---|
51 | self.mainpyfile = "" |
---|
52 | self._lineno = None # last listed line numbre |
---|
53 | # ignore filenames (avoid spurious interaction specially on py2) |
---|
54 | self.ignore_files = [self.canonic(f) for f in (__file__, bdb.__file__)] |
---|
55 | # replace system standard input and output (send them thru the pipe) |
---|
56 | self.old_stdio = sys.stdin, sys.stdout, sys.stderr |
---|
57 | if redirect_stdio: |
---|
58 | sys.stdin = self |
---|
59 | sys.stdout = self |
---|
60 | sys.stderr = self |
---|
61 | if allow_interruptions: |
---|
62 | # fake breakpoint to prevent removing trace_dispatch on set_continue |
---|
63 | self.breaks[None] = [] |
---|
64 | self.allow_interruptions = allow_interruptions |
---|
65 | self.burst = 0 # do not send notifications ("burst" mode) |
---|
66 | self.params = {} # optional parameters for interaction |
---|
67 | |
---|
68 | # flags to reduce overhead (only stop at breakpoint or interrupt) |
---|
69 | self.use_speedups = use_speedups |
---|
70 | self.fast_continue = False |
---|
71 | |
---|
72 | def pull_actions(self): |
---|
73 | # receive a remote procedure call from the frontend: |
---|
74 | # returns True if action processed |
---|
75 | # None when 'run' notification is received (see 'startup') |
---|
76 | request = self.pipe.recv() |
---|
77 | if request.get("method") == 'run': |
---|
78 | return None |
---|
79 | response = {'version': '1.1', 'id': request.get('id'), |
---|
80 | 'result': None, |
---|
81 | 'error': None} |
---|
82 | try: |
---|
83 | # dispatch message (JSON RPC like) |
---|
84 | method = getattr(self, request['method']) |
---|
85 | response['result'] = method.__call__(*request['args'], |
---|
86 | **request.get('kwargs', {})) |
---|
87 | except Exception as e: |
---|
88 | response['error'] = {'code': 0, 'message': str(e)} |
---|
89 | # send the result for normal method calls, not for notifications |
---|
90 | if request.get('id'): |
---|
91 | self.pipe.send(response) |
---|
92 | return True |
---|
93 | |
---|
94 | # Override Bdb methods |
---|
95 | |
---|
96 | def trace_dispatch(self, frame, event, arg): |
---|
97 | # check for non-interaction rpc (set_breakpoint, interrupt) |
---|
98 | while self.allow_interruptions and self.pipe.poll(): |
---|
99 | self.pull_actions() |
---|
100 | # check for non-interaction rpc (set_breakpoint, interrupt) |
---|
101 | while self.pipe.poll(): |
---|
102 | self.pull_actions() |
---|
103 | if (frame.f_code.co_filename, frame.f_lineno) not in breaks and \ |
---|
104 | self.fast_continue: |
---|
105 | return self.trace_dispatch |
---|
106 | # process the frame (see Bdb.trace_dispatch) |
---|
107 | ##if self.fast_continue: |
---|
108 | ## return self.trace_dispatch |
---|
109 | if self.quitting: |
---|
110 | return # None |
---|
111 | if event == 'line': |
---|
112 | return self.dispatch_line(frame) |
---|
113 | if event == 'call': |
---|
114 | return self.dispatch_call(frame, arg) |
---|
115 | if event == 'return': |
---|
116 | return self.dispatch_return(frame, arg) |
---|
117 | if event == 'exception': |
---|
118 | return self.dispatch_exception(frame, arg) |
---|
119 | return self.trace_dispatch |
---|
120 | |
---|
121 | def user_call(self, frame, argument_list): |
---|
122 | """This method is called when there is the remote possibility |
---|
123 | that we ever need to stop in this function.""" |
---|
124 | if self._wait_for_mainpyfile or self._wait_for_breakpoint: |
---|
125 | return |
---|
126 | if self.stop_here(frame): |
---|
127 | self.interaction(frame) |
---|
128 | |
---|
129 | def user_line(self, frame): |
---|
130 | """This function is called when we stop or break at this line.""" |
---|
131 | if self._wait_for_mainpyfile: |
---|
132 | if (not self.canonic(frame.f_code.co_filename).startswith(self.mainpyfile) |
---|
133 | or frame.f_lineno<= 0): |
---|
134 | return |
---|
135 | self._wait_for_mainpyfile = 0 |
---|
136 | if self._wait_for_breakpoint: |
---|
137 | if not self.break_here(frame): |
---|
138 | return |
---|
139 | self._wait_for_breakpoint = 0 |
---|
140 | self.interaction(frame) |
---|
141 | |
---|
142 | def user_exception(self, frame, info): |
---|
143 | """This function is called if an exception occurs, |
---|
144 | but only if we are to stop at or just below this level.""" |
---|
145 | if self._wait_for_mainpyfile or self._wait_for_breakpoint: |
---|
146 | return |
---|
147 | extype, exvalue, trace = info |
---|
148 | # pre-process stack trace as it isn't pickeable (cannot be sent pure) |
---|
149 | msg = ''.join(traceback.format_exception(extype, exvalue, trace)) |
---|
150 | # in python3.5, convert FrameSummary to tuples (py2.7+ compatibility) |
---|
151 | tb = [tuple(fs) for fs in traceback.extract_tb(trace)] |
---|
152 | title = traceback.format_exception_only(extype, exvalue)[0] |
---|
153 | # send an Exception notification |
---|
154 | msg = {'method': 'exception', |
---|
155 | 'args': (title, extype.__name__, repr(exvalue), tb, msg), |
---|
156 | 'id': None} |
---|
157 | self.pipe.send(msg) |
---|
158 | self.interaction(frame) |
---|
159 | |
---|
160 | def run(self, code, interp=None, *args, **kwargs): |
---|
161 | try: |
---|
162 | return bdb.Bdb.run(self, code, *args, **kwargs) |
---|
163 | finally: |
---|
164 | pass |
---|
165 | |
---|
166 | def runcall(self, function, interp=None, *args, **kwargs): |
---|
167 | try: |
---|
168 | self.interp = interp |
---|
169 | return bdb.Bdb.runcall(self, function, *args, **kwargs) |
---|
170 | finally: |
---|
171 | pass |
---|
172 | |
---|
173 | def _runscript(self, filename): |
---|
174 | # The script has to run in __main__ namespace (clear it) |
---|
175 | import __main__ |
---|
176 | import imp |
---|
177 | filename = os.path.abspath(filename) |
---|
178 | __main__.__dict__.clear() |
---|
179 | __main__.__dict__.update({"__name__" : "__main__", |
---|
180 | "__file__" : filename, |
---|
181 | "__builtins__": __builtins__, |
---|
182 | "imp" : imp, # need for run |
---|
183 | }) |
---|
184 | |
---|
185 | # avoid stopping before we reach the main script |
---|
186 | self._wait_for_mainpyfile = 1 |
---|
187 | self.mainpyfile = self.canonic(filename) |
---|
188 | self._user_requested_quit = 0 |
---|
189 | if sys.version_info>(3,0): |
---|
190 | statement = 'imp.load_source("__main__", "%s")' % filename |
---|
191 | else: |
---|
192 | statement = 'execfile(%r)' % filename |
---|
193 | self.startup() |
---|
194 | self.run(statement) |
---|
195 | |
---|
196 | def startup(self): |
---|
197 | "Notify and wait frontend to set initial params and breakpoints" |
---|
198 | # send some useful info to identify session |
---|
199 | thread = threading.current_thread() |
---|
200 | # get the caller module filename |
---|
201 | frame = sys._getframe() |
---|
202 | fn = self.canonic(frame.f_code.co_filename) |
---|
203 | while frame.f_back and self.canonic(frame.f_code.co_filename) == fn: |
---|
204 | frame = frame.f_back |
---|
205 | args = [__version__, os.getpid(), thread.name, " ".join(sys.argv), |
---|
206 | frame.f_code.co_filename] |
---|
207 | self.pipe.send({'method': 'startup', 'args': args}) |
---|
208 | while self.pull_actions() is not None: |
---|
209 | pass |
---|
210 | |
---|
211 | # General interaction function |
---|
212 | |
---|
213 | def interaction(self, frame): |
---|
214 | # chache frame locals to ensure that modifications are not overwritten |
---|
215 | self.frame_locals = frame and frame.f_locals or {} |
---|
216 | # extract current filename and line number |
---|
217 | code, lineno = frame.f_code, frame.f_lineno |
---|
218 | filename = self.canonic(code.co_filename) |
---|
219 | basename = os.path.basename(filename) |
---|
220 | # check if interaction should be ignored (i.e. debug modules internals) |
---|
221 | if filename in self.ignore_files: |
---|
222 | return |
---|
223 | message = "%s:%s" % (basename, lineno) |
---|
224 | if code.co_name != "?": |
---|
225 | message = "%s: %s()" % (message, code.co_name) |
---|
226 | |
---|
227 | # wait user events |
---|
228 | self.waiting = True |
---|
229 | self.frame = frame |
---|
230 | try: |
---|
231 | while self.waiting: |
---|
232 | # sync_source_line() |
---|
233 | if frame and filename[:1] + filename[-1:] != "<>" and os.path.exists(filename): |
---|
234 | line = linecache.getline(filename, self.frame.f_lineno, |
---|
235 | self.frame.f_globals) |
---|
236 | else: |
---|
237 | line = "" |
---|
238 | # send the notification (debug event) - DOESN'T WAIT RESPONSE |
---|
239 | self.burst -= 1 |
---|
240 | if self.burst < 0: |
---|
241 | kwargs = {} |
---|
242 | if self.params.get('call_stack'): |
---|
243 | kwargs['call_stack'] = self.do_where() |
---|
244 | if self.params.get('environment'): |
---|
245 | kwargs['environment'] = self.do_environment() |
---|
246 | self.pipe.send({'method': 'interaction', 'id': None, |
---|
247 | 'args': (filename, self.frame.f_lineno, line), |
---|
248 | 'kwargs': kwargs}) |
---|
249 | |
---|
250 | self.pull_actions() |
---|
251 | finally: |
---|
252 | self.waiting = False |
---|
253 | self.frame = None |
---|
254 | |
---|
255 | def do_debug(self, mainpyfile=None, wait_breakpoint=1): |
---|
256 | self.reset() |
---|
257 | if not wait_breakpoint or mainpyfile: |
---|
258 | self._wait_for_mainpyfile = 1 |
---|
259 | if not mainpyfile: |
---|
260 | frame = sys._getframe().f_back |
---|
261 | mainpyfile = frame.f_code.co_filename |
---|
262 | self.mainpyfile = self.canonic(mainpyfile) |
---|
263 | self._wait_for_breakpoint = wait_breakpoint |
---|
264 | sys.settrace(self.trace_dispatch) |
---|
265 | |
---|
266 | def set_trace(self, frame=None): |
---|
267 | # start debugger interaction immediatelly |
---|
268 | if frame is None: |
---|
269 | frame = sys._getframe().f_back |
---|
270 | self._wait_for_mainpyfile = frame.f_code.co_filename |
---|
271 | self._wait_for_breakpoint = 0 |
---|
272 | # reinitialize debugger internal settings |
---|
273 | self.fast_continue = False |
---|
274 | bdb.Bdb.set_trace(self, frame) |
---|
275 | |
---|
276 | # Command definitions, called by interaction() |
---|
277 | |
---|
278 | def do_continue(self): |
---|
279 | self.set_continue() |
---|
280 | self.waiting = False |
---|
281 | self.fast_continue = self.use_speedups |
---|
282 | |
---|
283 | def do_step(self): |
---|
284 | self.set_step() |
---|
285 | self.waiting = False |
---|
286 | self.fast_continue = False |
---|
287 | |
---|
288 | def do_return(self): |
---|
289 | self.set_return(self.frame) |
---|
290 | self.waiting = False |
---|
291 | self.fast_continue = False |
---|
292 | |
---|
293 | def do_next(self): |
---|
294 | self.set_next(self.frame) |
---|
295 | self.waiting = False |
---|
296 | self.fast_continue = False |
---|
297 | |
---|
298 | def interrupt(self): |
---|
299 | self.set_trace() |
---|
300 | self.fast_continue = False |
---|
301 | |
---|
302 | def do_quit(self): |
---|
303 | self.set_quit() |
---|
304 | self.waiting = False |
---|
305 | self.fast_continue = False |
---|
306 | |
---|
307 | def do_jump(self, lineno): |
---|
308 | arg = int(lineno) |
---|
309 | try: |
---|
310 | self.frame.f_lineno = arg |
---|
311 | except ValueError as e: |
---|
312 | return str(e) |
---|
313 | |
---|
314 | def do_list(self, arg): |
---|
315 | last = None |
---|
316 | if arg: |
---|
317 | if isinstance(arg, tuple): |
---|
318 | first, last = arg |
---|
319 | else: |
---|
320 | first = arg |
---|
321 | elif not self._lineno: |
---|
322 | first = max(1, self.frame.f_lineno - 5) |
---|
323 | else: |
---|
324 | first = self._lineno + 1 |
---|
325 | if last is None: |
---|
326 | last = first + 10 |
---|
327 | filename = self.frame.f_code.co_filename |
---|
328 | breaklist = self.get_file_breaks(filename) |
---|
329 | lines = [] |
---|
330 | for lineno in range(first, last+1): |
---|
331 | line = linecache.getline(filename, lineno, |
---|
332 | self.frame.f_globals) |
---|
333 | if not line: |
---|
334 | lines.append((filename, lineno, '', "", "<EOF>\n")) |
---|
335 | break |
---|
336 | else: |
---|
337 | breakpoint = "B" if lineno in breaklist else "" |
---|
338 | current = "->" if self.frame.f_lineno == lineno else "" |
---|
339 | lines.append((filename, lineno, breakpoint, current, line)) |
---|
340 | self._lineno = lineno |
---|
341 | return lines |
---|
342 | |
---|
343 | def do_read(self, filename): |
---|
344 | return open(filename, "Ur").read() |
---|
345 | |
---|
346 | def do_set_breakpoint(self, filename, lineno, temporary=0, cond=None): |
---|
347 | global breaks # list for speedups! |
---|
348 | breaks.append((filename.replace("\\", "/"), int(lineno))) |
---|
349 | return self.set_break(filename, int(lineno), temporary, cond) |
---|
350 | |
---|
351 | def do_list_breakpoint(self): |
---|
352 | breaks = [] |
---|
353 | if self.breaks: # There's at least one |
---|
354 | for bp in bdb.Breakpoint.bpbynumber: |
---|
355 | if bp: |
---|
356 | breaks.append((bp.number, bp.file, bp.line, |
---|
357 | bp.temporary, bp.enabled, bp.hits, bp.cond, )) |
---|
358 | return breaks |
---|
359 | |
---|
360 | def do_clear_breakpoint(self, filename, lineno): |
---|
361 | self.clear_break(filename, lineno) |
---|
362 | |
---|
363 | def do_clear_file_breakpoints(self, filename): |
---|
364 | self.clear_all_file_breaks(filename) |
---|
365 | |
---|
366 | def do_clear(self, arg): |
---|
367 | # required by BDB to remove temp breakpoints! |
---|
368 | err = self.clear_bpbynumber(arg) |
---|
369 | if err: |
---|
370 | print('*** DO_CLEAR failed', err) |
---|
371 | |
---|
372 | def do_eval(self, arg, safe=True): |
---|
373 | if self.frame: |
---|
374 | ret = eval(arg, self.frame.f_globals, |
---|
375 | self.frame_locals) |
---|
376 | else: |
---|
377 | ret = RPCError("No current frame available to eval") |
---|
378 | if safe: |
---|
379 | ret = pydoc.cram(repr(ret), 255) |
---|
380 | return ret |
---|
381 | |
---|
382 | def do_exec(self, arg, safe=True): |
---|
383 | if not self.frame: |
---|
384 | ret = RPCError("No current frame available to exec") |
---|
385 | else: |
---|
386 | locals = self.frame_locals |
---|
387 | globals = self.frame.f_globals |
---|
388 | code = compile(arg + '\n', '<stdin>', 'single') |
---|
389 | save_displayhook = sys.displayhook |
---|
390 | self.displayhook_value = None |
---|
391 | try: |
---|
392 | sys.displayhook = self.displayhook |
---|
393 | exec(code, globals, locals) |
---|
394 | ret = self.displayhook_value |
---|
395 | finally: |
---|
396 | sys.displayhook = save_displayhook |
---|
397 | if safe: |
---|
398 | ret = pydoc.cram(repr(ret), 255) |
---|
399 | return ret |
---|
400 | |
---|
401 | def do_where(self): |
---|
402 | "print_stack_trace" |
---|
403 | stack, curindex = self.get_stack(self.frame, None) |
---|
404 | lines = [] |
---|
405 | for frame, lineno in stack: |
---|
406 | filename = frame.f_code.co_filename |
---|
407 | line = linecache.getline(filename, lineno) |
---|
408 | lines.append((filename, lineno, "", "", line, )) |
---|
409 | return lines |
---|
410 | |
---|
411 | def do_environment(self): |
---|
412 | "return current frame local and global environment" |
---|
413 | env = {'locals': {}, 'globals': {}} |
---|
414 | # converts the frame global and locals to a short text representation: |
---|
415 | if self.frame: |
---|
416 | for scope, max_length, vars in ( |
---|
417 | ("locals", 255, list(self.frame_locals.items())), |
---|
418 | ("globals", 20, list(self.frame.f_globals.items())), ): |
---|
419 | for (name, value) in vars: |
---|
420 | try: |
---|
421 | short_repr = pydoc.cram(repr(value), max_length) |
---|
422 | except Exception as e: |
---|
423 | # some objects cannot be represented... |
---|
424 | short_repr = "**exception** %s" % repr(e) |
---|
425 | env[scope][name] = (short_repr, repr(type(value))) |
---|
426 | return env |
---|
427 | |
---|
428 | def get_autocomplete_list(self, expression): |
---|
429 | "Return list of auto-completion options for expression" |
---|
430 | try: |
---|
431 | obj = self.do_eval(expression, safe=False) |
---|
432 | except: |
---|
433 | return [] |
---|
434 | else: |
---|
435 | return dir(obj) |
---|
436 | |
---|
437 | def get_call_tip(self, expression): |
---|
438 | "Return list of auto-completion options for expression" |
---|
439 | try: |
---|
440 | obj = self.do_eval(expression) |
---|
441 | except Exception as e: |
---|
442 | return ('', '', str(e)) |
---|
443 | else: |
---|
444 | name = '' |
---|
445 | try: |
---|
446 | name = obj.__name__ |
---|
447 | except AttributeError: |
---|
448 | pass |
---|
449 | argspec = '' |
---|
450 | drop_self = 0 |
---|
451 | f = None |
---|
452 | try: |
---|
453 | if inspect.isbuiltin(obj): |
---|
454 | pass |
---|
455 | elif inspect.ismethod(obj): |
---|
456 | # Get the function from the object |
---|
457 | f = obj.__func__ |
---|
458 | drop_self = 1 |
---|
459 | elif inspect.isclass(obj): |
---|
460 | # Get the __init__ method function for the class. |
---|
461 | if hasattr(obj, '__init__'): |
---|
462 | f = obj.__init__.__func__ |
---|
463 | else: |
---|
464 | for base in object.__bases__: |
---|
465 | if hasattr(base, '__init__'): |
---|
466 | f = base.__init__.__func__ |
---|
467 | break |
---|
468 | if f is not None: |
---|
469 | drop_self = 1 |
---|
470 | elif isinstance(obj, collections.Callable): |
---|
471 | # use the obj as a function by default |
---|
472 | f = obj |
---|
473 | # Get the __call__ method instead. |
---|
474 | f = obj.__call__.__func__ |
---|
475 | drop_self = 0 |
---|
476 | except AttributeError: |
---|
477 | pass |
---|
478 | if f: |
---|
479 | argspec = inspect.formatargspec(*inspect.getargspec(f)) |
---|
480 | doc = '' |
---|
481 | if isinstance(obj, collections.Callable): |
---|
482 | try: |
---|
483 | doc = inspect.getdoc(obj) |
---|
484 | except: |
---|
485 | pass |
---|
486 | return (name, argspec[1:-1], doc.strip()) |
---|
487 | |
---|
488 | def set_burst(self, val): |
---|
489 | "Set burst mode -multiple command count- (shut up notifications)" |
---|
490 | self.burst = val |
---|
491 | |
---|
492 | def set_params(self, params): |
---|
493 | "Set parameters for interaction" |
---|
494 | self.params.update(params) |
---|
495 | |
---|
496 | def displayhook(self, obj): |
---|
497 | """Custom displayhook for the do_exec which prevents |
---|
498 | assignment of the _ variable in the builtins. |
---|
499 | """ |
---|
500 | self.displayhook_value = repr(obj) |
---|
501 | |
---|
502 | def reset(self): |
---|
503 | bdb.Bdb.reset(self) |
---|
504 | self.waiting = False |
---|
505 | self.frame = None |
---|
506 | |
---|
507 | def post_mortem(self, info=None): |
---|
508 | "Debug an un-handled python exception" |
---|
509 | # check if post mortem mode is enabled: |
---|
510 | if not self.params.get('postmortem', True): |
---|
511 | return |
---|
512 | # handling the default |
---|
513 | if info is None: |
---|
514 | # sys.exc_info() returns (type, value, traceback) if an exception is |
---|
515 | # being handled, otherwise it returns None |
---|
516 | info = sys.exc_info() |
---|
517 | # extract the traceback object: |
---|
518 | t = info[2] |
---|
519 | if t is None: |
---|
520 | raise ValueError("A valid traceback must be passed if no " |
---|
521 | "exception is being handled") |
---|
522 | self.reset() |
---|
523 | # get last frame: |
---|
524 | while t is not None: |
---|
525 | frame = t.tb_frame |
---|
526 | t = t.tb_next |
---|
527 | code, lineno = frame.f_code, frame.f_lineno |
---|
528 | filename = code.co_filename |
---|
529 | line = linecache.getline(filename, lineno) |
---|
530 | #(filename, lineno, "", current, line, )} |
---|
531 | # SyntaxError doesn't execute even one line, so avoid mainpyfile check |
---|
532 | self._wait_for_mainpyfile = False |
---|
533 | # send exception information & request interaction |
---|
534 | self.user_exception(frame, info) |
---|
535 | |
---|
536 | def ping(self): |
---|
537 | "Minimal method to test that the pipe (connection) is alive" |
---|
538 | try: |
---|
539 | # get some non-trivial data to compare: |
---|
540 | args = (id(object()), ) |
---|
541 | msg = {'method': 'ping', 'args': args, 'id': None} |
---|
542 | self.pipe.send(msg) |
---|
543 | msg = self.pipe.recv() |
---|
544 | # check if data received is ok (alive and synchonized!) |
---|
545 | return msg['result'] == args |
---|
546 | except (IOError, EOFError): |
---|
547 | return None |
---|
548 | |
---|
549 | # console file-like object emulation |
---|
550 | def readline(self): |
---|
551 | "Replacement for stdin.readline()" |
---|
552 | msg = {'method': 'readline', 'args': (), 'id': self.i} |
---|
553 | self.pipe.send(msg) |
---|
554 | msg = self.pipe.recv() |
---|
555 | self.i += 1 |
---|
556 | return msg['result'] |
---|
557 | |
---|
558 | def readlines(self): |
---|
559 | "Replacement for stdin.readlines()" |
---|
560 | lines = [] |
---|
561 | while lines[-1:] != ['\n']: |
---|
562 | lines.append(self.readline()) |
---|
563 | return lines |
---|
564 | |
---|
565 | def write(self, text): |
---|
566 | "Replacement for stdout.write()" |
---|
567 | msg = {'method': 'write', 'args': (text, ), 'id': None} |
---|
568 | self.pipe.send(msg) |
---|
569 | |
---|
570 | def writelines(self, l): |
---|
571 | list(map(self.write, l)) |
---|
572 | |
---|
573 | def flush(self): |
---|
574 | pass |
---|
575 | |
---|
576 | def isatty(self): |
---|
577 | return 0 |
---|
578 | |
---|
579 | def encoding(self): |
---|
580 | return None # use default, 'utf-8' should be better... |
---|
581 | |
---|
582 | def close(self): |
---|
583 | # revert redirections and close connection |
---|
584 | if sys: |
---|
585 | sys.stdin, sys.stdout, sys.stderr = self.old_stdio |
---|
586 | try: |
---|
587 | self.pipe.close() |
---|
588 | except: |
---|
589 | pass |
---|
590 | |
---|
591 | def __del__(self): |
---|
592 | self.close() |
---|
593 | |
---|
594 | |
---|
595 | class QueuePipe(object): |
---|
596 | "Simulated pipe for threads (using two queues)" |
---|
597 | |
---|
598 | def __init__(self, name, in_queue, out_queue): |
---|
599 | self.__name = name |
---|
600 | self.in_queue = in_queue |
---|
601 | self.out_queue = out_queue |
---|
602 | |
---|
603 | def send(self, data): |
---|
604 | self.out_queue.put(data, block=True) |
---|
605 | |
---|
606 | def recv(self, count=None, timeout=None): |
---|
607 | data = self.in_queue.get(block=True, timeout=timeout) |
---|
608 | return data |
---|
609 | |
---|
610 | def poll(self, timeout=None): |
---|
611 | return not self.in_queue.empty() |
---|
612 | |
---|
613 | def close(self): |
---|
614 | pass |
---|
615 | |
---|
616 | |
---|
617 | class RPCError(RuntimeError): |
---|
618 | "Remote Error (not user exception)" |
---|
619 | pass |
---|
620 | |
---|
621 | |
---|
622 | class Frontend(object): |
---|
623 | "Qdb generic Frontend interface" |
---|
624 | |
---|
625 | def __init__(self, pipe): |
---|
626 | self.i = 1 |
---|
627 | self.info = () |
---|
628 | self.pipe = pipe |
---|
629 | self.notifies = [] |
---|
630 | self.read_lock = threading.RLock() |
---|
631 | self.write_lock = threading.RLock() |
---|
632 | |
---|
633 | def recv(self): |
---|
634 | self.read_lock.acquire() |
---|
635 | try: |
---|
636 | return self.pipe.recv() |
---|
637 | finally: |
---|
638 | self.read_lock.release() |
---|
639 | |
---|
640 | def send(self, data): |
---|
641 | self.write_lock.acquire() |
---|
642 | try: |
---|
643 | return self.pipe.send(data) |
---|
644 | finally: |
---|
645 | self.write_lock.release() |
---|
646 | |
---|
647 | def startup(self, version, pid, thread_name, argv, filename): |
---|
648 | self.info = (version, pid, thread_name, argv, filename) |
---|
649 | self.send({'method': 'run', 'args': (), 'id': None}) |
---|
650 | |
---|
651 | def interaction(self, filename, lineno, line, *kwargs): |
---|
652 | raise NotImplementedError |
---|
653 | |
---|
654 | def exception(self, title, extype, exvalue, trace, request): |
---|
655 | "Show a user_exception" |
---|
656 | raise NotImplementedError |
---|
657 | |
---|
658 | def write(self, text): |
---|
659 | "Console output (print)" |
---|
660 | raise NotImplementedError |
---|
661 | |
---|
662 | def readline(self, text): |
---|
663 | "Console input/rawinput" |
---|
664 | raise NotImplementedError |
---|
665 | |
---|
666 | def run(self): |
---|
667 | "Main method dispatcher (infinite loop)" |
---|
668 | if self.pipe: |
---|
669 | if not self.notifies: |
---|
670 | # wait for a message... |
---|
671 | request = self.recv() |
---|
672 | else: |
---|
673 | # process an asyncronus notification received earlier |
---|
674 | request = self.notifies.pop(0) |
---|
675 | return self.process_message(request) |
---|
676 | |
---|
677 | def process_message(self, request): |
---|
678 | if request: |
---|
679 | result = None |
---|
680 | if request.get("error"): |
---|
681 | # it is not supposed to get an error here |
---|
682 | # it should be raised by the method call |
---|
683 | raise RPCError(res['error']['message']) |
---|
684 | elif request.get('method') == 'interaction': |
---|
685 | self.interaction(*request.get("args"), **request.get("kwargs")) |
---|
686 | elif request.get('method') == 'startup': |
---|
687 | self.startup(*request['args']) |
---|
688 | elif request.get('method') == 'exception': |
---|
689 | self.exception(*request['args']) |
---|
690 | elif request.get('method') == 'write': |
---|
691 | self.write(*request.get("args")) |
---|
692 | elif request.get('method') == 'readline': |
---|
693 | result = self.readline() |
---|
694 | elif request.get('method') == 'ping': |
---|
695 | result = request['args'] |
---|
696 | if result: |
---|
697 | response = {'version': '1.1', 'id': request.get('id'), |
---|
698 | 'result': result, |
---|
699 | 'error': None} |
---|
700 | self.send(response) |
---|
701 | return True |
---|
702 | |
---|
703 | def call(self, method, *args): |
---|
704 | "Actually call the remote method (inside the thread)" |
---|
705 | req = {'method': method, 'args': args, 'id': self.i} |
---|
706 | self.send(req) |
---|
707 | self.i += 1 # increment the id |
---|
708 | while 1: |
---|
709 | # wait until command acknowledge (response id match the request) |
---|
710 | res = self.recv() |
---|
711 | if 'id' not in res or not res['id']: |
---|
712 | # nested notification received (i.e. write)! process it later... |
---|
713 | self.notifies.append(res) |
---|
714 | elif 'result' not in res: |
---|
715 | # nested request received (i.e. readline)! process it! |
---|
716 | self.process_message(res) |
---|
717 | elif int(req['id']) != int(res['id']): |
---|
718 | print("DEBUGGER wrong packet received: expecting id", req['id'], res['id']) |
---|
719 | # protocol state is unknown |
---|
720 | elif 'error' in res and res['error']: |
---|
721 | raise RPCError(res['error']['message']) |
---|
722 | else: |
---|
723 | return res['result'] |
---|
724 | |
---|
725 | def do_step(self, arg=None): |
---|
726 | "Execute the current line, stop at the first possible occasion" |
---|
727 | self.call('do_step') |
---|
728 | |
---|
729 | def do_next(self, arg=None): |
---|
730 | "Execute the current line, do not stop at function calls" |
---|
731 | self.call('do_next') |
---|
732 | |
---|
733 | def do_continue(self, arg=None): |
---|
734 | "Continue execution, only stop when a breakpoint is encountered." |
---|
735 | self.call('do_continue') |
---|
736 | |
---|
737 | def do_return(self, arg=None): |
---|
738 | "Continue execution until the current function returns" |
---|
739 | self.call('do_return') |
---|
740 | |
---|
741 | def do_jump(self, arg): |
---|
742 | "Set the next line that will be executed (None if sucess or message)" |
---|
743 | res = self.call('do_jump', arg) |
---|
744 | return res |
---|
745 | |
---|
746 | def do_where(self, arg=None): |
---|
747 | "Print a stack trace, with the most recent frame at the bottom." |
---|
748 | return self.call('do_where') |
---|
749 | |
---|
750 | def do_quit(self, arg=None): |
---|
751 | "Quit from the debugger. The program being executed is aborted." |
---|
752 | self.call('do_quit') |
---|
753 | |
---|
754 | def do_eval(self, expr): |
---|
755 | "Inspect the value of the expression" |
---|
756 | return self.call('do_eval', expr) |
---|
757 | |
---|
758 | def do_environment(self): |
---|
759 | "List all the locals and globals variables (string representation)" |
---|
760 | return self.call('do_environment') |
---|
761 | |
---|
762 | def do_list(self, arg=None): |
---|
763 | "List source code for the current file" |
---|
764 | return self.call('do_list', arg) |
---|
765 | |
---|
766 | def do_read(self, filename): |
---|
767 | "Read and send a local filename" |
---|
768 | return self.call('do_read', filename) |
---|
769 | |
---|
770 | def do_set_breakpoint(self, filename, lineno, temporary=0, cond=None): |
---|
771 | "Set a breakpoint at filename:breakpoint" |
---|
772 | self.call('do_set_breakpoint', filename, lineno, temporary, cond) |
---|
773 | |
---|
774 | def do_clear_breakpoint(self, filename, lineno): |
---|
775 | "Remove a breakpoint at filename:breakpoint" |
---|
776 | self.call('do_clear_breakpoint', filename, lineno) |
---|
777 | |
---|
778 | def do_clear_file_breakpoints(self, filename): |
---|
779 | "Remove all breakpoints at filename" |
---|
780 | self.call('do_clear_breakpoints', filename, lineno) |
---|
781 | |
---|
782 | def do_list_breakpoint(self): |
---|
783 | "List all breakpoints" |
---|
784 | return self.call('do_list_breakpoint') |
---|
785 | |
---|
786 | def do_exec(self, statement): |
---|
787 | return self.call('do_exec', statement) |
---|
788 | |
---|
789 | def get_autocomplete_list(self, expression): |
---|
790 | return self.call('get_autocomplete_list', expression) |
---|
791 | |
---|
792 | def get_call_tip(self, expression): |
---|
793 | return self.call('get_call_tip', expression) |
---|
794 | |
---|
795 | def interrupt(self): |
---|
796 | "Immediately stop at the first possible occasion (outside interaction)" |
---|
797 | # this is a notification!, do not expect a response |
---|
798 | req = {'method': 'interrupt', 'args': ()} |
---|
799 | self.send(req) |
---|
800 | |
---|
801 | def set_burst(self, value): |
---|
802 | req = {'method': 'set_burst', 'args': (value, )} |
---|
803 | self.send(req) |
---|
804 | |
---|
805 | def set_params(self, params): |
---|
806 | req = {'method': 'set_params', 'args': (params, )} |
---|
807 | self.send(req) |
---|
808 | |
---|
809 | |
---|
810 | class Cli(Frontend, cmd.Cmd): |
---|
811 | "Qdb Front-end command line interface" |
---|
812 | |
---|
813 | def __init__(self, pipe, completekey='tab', stdin=None, stdout=None, skip=None): |
---|
814 | cmd.Cmd.__init__(self, completekey, stdin, stdout) |
---|
815 | Frontend.__init__(self, pipe) |
---|
816 | |
---|
817 | # redefine Frontend methods: |
---|
818 | |
---|
819 | def run(self): |
---|
820 | while 1: |
---|
821 | try: |
---|
822 | Frontend.run(self) |
---|
823 | except KeyboardInterrupt: |
---|
824 | print("Interupting...") |
---|
825 | self.interrupt() |
---|
826 | |
---|
827 | def interaction(self, filename, lineno, line): |
---|
828 | print("> %s(%d)\n-> %s" % (filename, lineno, line), end=' ') |
---|
829 | self.filename = filename |
---|
830 | self.cmdloop() |
---|
831 | |
---|
832 | def exception(self, title, extype, exvalue, trace, request): |
---|
833 | print("=" * 80) |
---|
834 | print("Exception", title) |
---|
835 | print(request) |
---|
836 | print("-" * 80) |
---|
837 | |
---|
838 | def write(self, text): |
---|
839 | print(text, end=' ') |
---|
840 | |
---|
841 | def readline(self): |
---|
842 | return input() |
---|
843 | |
---|
844 | def postcmd(self, stop, line): |
---|
845 | return not line.startswith("h") # stop |
---|
846 | |
---|
847 | do_h = cmd.Cmd.do_help |
---|
848 | |
---|
849 | do_s = Frontend.do_step |
---|
850 | do_n = Frontend.do_next |
---|
851 | do_c = Frontend.do_continue |
---|
852 | do_r = Frontend.do_return |
---|
853 | do_q = Frontend.do_quit |
---|
854 | |
---|
855 | def do_eval(self, args): |
---|
856 | "Inspect the value of the expression" |
---|
857 | print(Frontend.do_eval(self, args)) |
---|
858 | |
---|
859 | def do_list(self, args): |
---|
860 | "List source code for the current file" |
---|
861 | lines = Frontend.do_list(self, eval(args, {}, {}) if args else None) |
---|
862 | self.print_lines(lines) |
---|
863 | |
---|
864 | def do_where(self, args): |
---|
865 | "Print a stack trace, with the most recent frame at the bottom." |
---|
866 | lines = Frontend.do_where(self) |
---|
867 | self.print_lines(lines) |
---|
868 | |
---|
869 | def do_environment(self, args=None): |
---|
870 | env = Frontend.do_environment(self) |
---|
871 | for key in env: |
---|
872 | print("=" * 78) |
---|
873 | print(key.capitalize()) |
---|
874 | print("-" * 78) |
---|
875 | for name, value in list(env[key].items()): |
---|
876 | print("%-12s = %s" % (name, value)) |
---|
877 | |
---|
878 | def do_list_breakpoint(self, arg=None): |
---|
879 | "List all breakpoints" |
---|
880 | breaks = Frontend.do_list_breakpoint(self) |
---|
881 | print("Num File Line Temp Enab Hits Cond") |
---|
882 | for bp in breaks: |
---|
883 | print('%-4d%-30s%4d %4s %4s %4d %s' % bp) |
---|
884 | print() |
---|
885 | |
---|
886 | def do_set_breakpoint(self, arg): |
---|
887 | "Set a breakpoint at filename:breakpoint" |
---|
888 | if arg: |
---|
889 | if ':' in arg: |
---|
890 | args = arg.split(":") |
---|
891 | else: |
---|
892 | args = (self.filename, arg) |
---|
893 | Frontend.do_set_breakpoint(self, *args) |
---|
894 | else: |
---|
895 | self.do_list_breakpoint() |
---|
896 | |
---|
897 | def do_jump(self, args): |
---|
898 | "Jump to the selected line" |
---|
899 | ret = Frontend.do_jump(self, args) |
---|
900 | if ret: # show error message if failed |
---|
901 | print("cannot jump:", ret) |
---|
902 | |
---|
903 | do_b = do_set_breakpoint |
---|
904 | do_l = do_list |
---|
905 | do_p = do_eval |
---|
906 | do_w = do_where |
---|
907 | do_e = do_environment |
---|
908 | do_j = do_jump |
---|
909 | |
---|
910 | def default(self, line): |
---|
911 | "Default command" |
---|
912 | if line[:1] == '!': |
---|
913 | print(self.do_exec(line[1:])) |
---|
914 | else: |
---|
915 | print("*** Unknown command: ", line) |
---|
916 | |
---|
917 | def print_lines(self, lines): |
---|
918 | for filename, lineno, bp, current, source in lines: |
---|
919 | print("%s:%4d%s%s\t%s" % (filename, lineno, bp, current, source), end=' ') |
---|
920 | print() |
---|
921 | |
---|
922 | |
---|
923 | # WORKAROUND for python3 server using pickle's HIGHEST_PROTOCOL (now 3) |
---|
924 | # but python2 client using pickles's protocol version 2 |
---|
925 | if sys.version_info[0] > 2: |
---|
926 | |
---|
927 | import multiprocessing.reduction # forking in py2 |
---|
928 | |
---|
929 | class ForkingPickler2(multiprocessing.reduction.ForkingPickler): |
---|
930 | def __init__(self, file, protocol=None, fix_imports=True): |
---|
931 | # downgrade to protocol ver 2 |
---|
932 | protocol = 2 |
---|
933 | super().__init__(file, protocol, fix_imports) |
---|
934 | |
---|
935 | multiprocessing.reduction.ForkingPickler = ForkingPickler2 |
---|
936 | |
---|
937 | |
---|
938 | def f(pipe): |
---|
939 | "test function to be debugged" |
---|
940 | print("creating debugger") |
---|
941 | qdb_test = Qdb(pipe=pipe, redirect_stdio=False, allow_interruptions=True) |
---|
942 | print("set trace") |
---|
943 | |
---|
944 | my_var = "Mariano!" |
---|
945 | qdb_test.set_trace() |
---|
946 | print("hello world!") |
---|
947 | for i in range(100000): |
---|
948 | pass |
---|
949 | print("good by!") |
---|
950 | |
---|
951 | |
---|
952 | def test(): |
---|
953 | "Create a backend/frontend and time it" |
---|
954 | if '--process' in sys.argv: |
---|
955 | from multiprocessing import Process, Pipe |
---|
956 | front_conn, child_conn = Pipe() |
---|
957 | p = Process(target=f, args=(child_conn,)) |
---|
958 | else: |
---|
959 | from threading import Thread |
---|
960 | from queue import Queue |
---|
961 | parent_queue, child_queue = Queue(), Queue() |
---|
962 | front_conn = QueuePipe("parent", parent_queue, child_queue) |
---|
963 | child_conn = QueuePipe("child", child_queue, parent_queue) |
---|
964 | p = Thread(target=f, args=(child_conn,)) |
---|
965 | |
---|
966 | p.start() |
---|
967 | import time |
---|
968 | |
---|
969 | class Test(Frontend): |
---|
970 | def interaction(self, *args): |
---|
971 | print("interaction!", args) |
---|
972 | ##self.do_next() |
---|
973 | def exception(self, *args): |
---|
974 | print("exception", args) |
---|
975 | |
---|
976 | qdb_test = Test(front_conn) |
---|
977 | time.sleep(1) |
---|
978 | t0 = time.time() |
---|
979 | |
---|
980 | print("running...") |
---|
981 | while front_conn.poll(): |
---|
982 | Frontend.run(qdb_test) |
---|
983 | qdb_test.do_continue() |
---|
984 | p.join() |
---|
985 | t1 = time.time() |
---|
986 | print("took", t1 - t0, "seconds") |
---|
987 | sys.exit(0) |
---|
988 | |
---|
989 | |
---|
990 | def start(host="localhost", port=6000, authkey='secret password'): |
---|
991 | "Start the CLI server and wait connection from a running debugger backend" |
---|
992 | |
---|
993 | address = (host, port) |
---|
994 | from multiprocessing.connection import Listener |
---|
995 | address = (host, port) # family is deduced to be 'AF_INET' |
---|
996 | if isinstance(authkey, str): |
---|
997 | authkey = authkey.encode("utf8") |
---|
998 | listener = Listener(address, authkey=authkey) |
---|
999 | print("qdb debugger backend: waiting for connection at", address) |
---|
1000 | conn = listener.accept() |
---|
1001 | print('qdb debugger backend: connected to', listener.last_accepted) |
---|
1002 | try: |
---|
1003 | Cli(conn).run() |
---|
1004 | except EOFError: |
---|
1005 | pass |
---|
1006 | finally: |
---|
1007 | conn.close() |
---|
1008 | |
---|
1009 | |
---|
1010 | def main(host='localhost', port=6000, authkey='secret password'): |
---|
1011 | "Debug a script (running under the backend) and connect to remote frontend" |
---|
1012 | |
---|
1013 | if not sys.argv[1:] or sys.argv[1] in ("--help", "-h"): |
---|
1014 | print("usage: pdb.py scriptfile [arg] ...") |
---|
1015 | sys.exit(2) |
---|
1016 | |
---|
1017 | mainpyfile = sys.argv[1] # Get script filename |
---|
1018 | if not os.path.exists(mainpyfile): |
---|
1019 | print('Error:', mainpyfile, 'does not exist') |
---|
1020 | sys.exit(1) |
---|
1021 | |
---|
1022 | del sys.argv[0] # Hide "pdb.py" from argument list |
---|
1023 | |
---|
1024 | # Replace pdb's dir with script's dir in front of module search path. |
---|
1025 | sys.path[0] = os.path.dirname(mainpyfile) |
---|
1026 | |
---|
1027 | # create the backend |
---|
1028 | init(host, port, authkey) |
---|
1029 | try: |
---|
1030 | print("running", mainpyfile) |
---|
1031 | qdb._runscript(mainpyfile) |
---|
1032 | print("The program finished") |
---|
1033 | except SystemExit: |
---|
1034 | # In most cases SystemExit does not warrant a post-mortem session. |
---|
1035 | print("The program exited via sys.exit(). Exit status: ", end=' ') |
---|
1036 | print(sys.exc_info()[1]) |
---|
1037 | raise |
---|
1038 | except Exception: |
---|
1039 | traceback.print_exc() |
---|
1040 | print("Uncaught exception. Entering post mortem debugging") |
---|
1041 | info = sys.exc_info() |
---|
1042 | qdb.post_mortem(info) |
---|
1043 | print("Program terminated!") |
---|
1044 | finally: |
---|
1045 | conn.close() |
---|
1046 | print("qdb debbuger backend: connection closed") |
---|
1047 | |
---|
1048 | |
---|
1049 | # "singleton" to store a unique backend per process |
---|
1050 | qdb = None |
---|
1051 | |
---|
1052 | |
---|
1053 | def init(host='localhost', port=6000, authkey='secret password', redirect=True): |
---|
1054 | "Simplified interface to debug running programs" |
---|
1055 | global qdb, listener, conn |
---|
1056 | |
---|
1057 | # destroy the debugger if the previous connection is lost (i.e. broken pipe) |
---|
1058 | if qdb and not qdb.ping(): |
---|
1059 | qdb.close() |
---|
1060 | qdb = None |
---|
1061 | |
---|
1062 | from multiprocessing.connection import Client |
---|
1063 | # only create it if not currently instantiated |
---|
1064 | if not qdb: |
---|
1065 | address = (host, port) # family is deduced to be 'AF_INET' |
---|
1066 | print("qdb debugger backend: waiting for connection to", address) |
---|
1067 | if isinstance(authkey, str): |
---|
1068 | authkey = authkey.encode("utf8") |
---|
1069 | conn = Client(address, authkey=authkey) |
---|
1070 | print('qdb debugger backend: connected to', address) |
---|
1071 | # create the backend |
---|
1072 | qdb = Qdb(conn, redirect_stdio=redirect, allow_interruptions=True) |
---|
1073 | # initial hanshake |
---|
1074 | qdb.startup() |
---|
1075 | |
---|
1076 | |
---|
1077 | def set_trace(host='localhost', port=6000, authkey='secret password'): |
---|
1078 | "Simplified interface to start debugging immediately" |
---|
1079 | init(host, port, authkey) |
---|
1080 | # start debugger backend: |
---|
1081 | qdb.set_trace() |
---|
1082 | |
---|
1083 | def debug(host='localhost', port=6000, authkey='secret password'): |
---|
1084 | "Simplified interface to start debugging immediately (no stop)" |
---|
1085 | init(host, port, authkey) |
---|
1086 | # start debugger backend: |
---|
1087 | qdb.do_debug() |
---|
1088 | |
---|
1089 | |
---|
1090 | def quit(): |
---|
1091 | "Remove trace and quit" |
---|
1092 | global qdb, listener, conn |
---|
1093 | if qdb: |
---|
1094 | sys.settrace(None) |
---|
1095 | qdb = None |
---|
1096 | if conn: |
---|
1097 | conn.close() |
---|
1098 | conn = None |
---|
1099 | if listener: |
---|
1100 | listener.close() |
---|
1101 | listener = None |
---|
1102 | |
---|
1103 | if __name__ == '__main__': |
---|
1104 | # When invoked as main program: |
---|
1105 | if '--test1' in sys.argv: |
---|
1106 | test() |
---|
1107 | # Check environment for configuration parameters: |
---|
1108 | kwargs = {} |
---|
1109 | for param in 'host', 'port', 'authkey': |
---|
1110 | if 'DBG_%s' % param.upper() in os.environ: |
---|
1111 | kwargs[param] = os.environ['DBG_%s' % param.upper()] |
---|
1112 | |
---|
1113 | if not sys.argv[1:]: |
---|
1114 | # connect to a remote debbuger |
---|
1115 | start(**kwargs) |
---|
1116 | else: |
---|
1117 | # start the debugger on a script |
---|
1118 | # reimport as global __main__ namespace is destroyed |
---|
1119 | import dbg |
---|
1120 | dbg.main(**kwargs) |
---|
1121 | |
---|