1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the debug base class which based originally on bdb.
8"""
9
10import sys
11import os
12import types
13import atexit
14import inspect
15import ctypes
16import time
17import dis
18import contextlib
19
20from BreakpointWatch import Breakpoint, Watch
21
22import _thread
23from DebugUtilities import getargvalues, formatargvalues
24
25gRecursionLimit = 64
26
27try:
28    GENERATOR_AND_COROUTINE_FLAGS = (
29        inspect.CO_GENERATOR | inspect.CO_COROUTINE |
30        inspect.CO_ASYNC_GENERATOR
31    )
32except AttributeError:
33    # Python < 3.7
34    GENERATOR_AND_COROUTINE_FLAGS = inspect.CO_GENERATOR
35
36
37def printerr(s):
38    """
39    Module function used for debugging the debug client.
40
41    @param s data to be printed
42    """
43    sys.__stderr__.write('{0!s}\n'.format(s))
44    sys.__stderr__.flush()
45
46
47def setRecursionLimit(limit):
48    """
49    Module function to set the recursion limit.
50
51    @param limit recursion limit (integer)
52    """
53    global gRecursionLimit
54    gRecursionLimit = limit
55
56
57class DebugBase:
58    """
59    Class implementing base class of the debugger.
60
61    Provides methods for the 'owning' client to call to step etc.
62    """
63    # Don't thrust distutils.sysconfig.get_python_lib: possible case mismatch
64    #  on Windows
65    lib = os.path.dirname(inspect.__file__)
66    # tuple required because it's accessed a lot of times by startswith method
67    pathsToSkip = ('<', os.path.dirname(__file__), inspect.__file__[:-1])
68    filesToSkip = {}
69
70    # cache for fixed file names
71    _fnCache = {}
72
73    # Stop all timers, when greenlets are used
74    pollTimerEnabled = True
75
76    def __init__(self, dbgClient):
77        """
78        Constructor
79
80        @param dbgClient the owning client
81        """
82        self._dbgClient = dbgClient
83
84        # Some informations about the thread
85        self.isMainThread = False
86        self.quitting = False
87        self.id = -1
88        self.name = ''
89
90        self.tracePythonLibs(0)
91
92        # Special handling of a recursion error
93        self.skipFrames = 0
94
95        self.isBroken = False
96        self.isException = False
97        self.cFrame = None
98
99        # current frame we are at
100        self.currentFrame = None
101
102        # frames, where we want to stop or release debugger
103        self.stopframe = None
104        self.returnframe = None
105        self.stop_everywhere = False
106
107        self.__recursionDepth = -1
108        self.setRecursionDepth(inspect.currentframe())
109
110        # background task to periodicaly check for client interactions
111        self.eventPollFlag = False
112        self.timer = _thread.start_new_thread(self.__eventPollTimer, ())
113
114        # provide a hook to perform a hard breakpoint
115        # Use it like this:
116        # if hasattr(sys, 'breakpoint): sys.breakpoint()
117        sys.breakpoint = self.set_trace
118        if sys.version_info >= (3, 7):
119            sys.breakpointhook = self.set_trace
120
121    def __eventPollTimer(self):
122        """
123        Private method to set a flag every 0.5 s to check for new messages.
124        """
125        while DebugBase.pollTimerEnabled:
126            time.sleep(0.5)
127            self.eventPollFlag = True
128
129        self.eventPollFlag = False
130
131    def getCurrentFrame(self):
132        """
133        Public method to return the current frame.
134
135        @return the current frame
136        @rtype frame object
137        """
138        # Don't show any local frames after the program was stopped
139        if self.quitting:
140            return None
141
142        return self.currentFrame
143
144    def getFrameLocals(self, frmnr=0):
145        """
146        Public method to return the locals dictionary of the current frame
147        or a frame below.
148
149        @param frmnr distance of frame to get locals dictionary of. 0 is
150            the current frame (int)
151        @return locals dictionary of the frame
152        """
153        f = self.currentFrame
154        while f is not None and frmnr > 0:
155            f = f.f_back
156            frmnr -= 1
157        return f.f_locals
158
159    def storeFrameLocals(self, frmnr=0):
160        """
161        Public method to store the locals into the frame, so an access to
162        frame.f_locals returns the last data.
163
164        @param frmnr distance of frame to store locals dictionary to. 0 is
165            the current frame (int)
166        """
167        cf = self.currentFrame
168        while cf is not None and frmnr > 0:
169            cf = cf.f_back
170            frmnr -= 1
171
172        with contextlib.suppress(Exception):
173            if "__pypy__" in sys.builtin_module_names:
174                import __pypy__
175                __pypy__.locals_to_fast(cf)
176                return
177
178        ctypes.pythonapi.PyFrame_LocalsToFast(
179            ctypes.py_object(cf),
180            ctypes.c_int(0))
181
182    def step(self, traceMode):
183        """
184        Public method to perform a step operation in this thread.
185
186        @param traceMode If it is True, then the step is a step into,
187              otherwise it is a step over.
188        """
189        if traceMode:
190            self.set_step()
191        else:
192            self.set_next(self.currentFrame)
193
194    def stepOut(self):
195        """
196        Public method to perform a step out of the current call.
197        """
198        self.set_return(self.currentFrame)
199
200    def go(self, special):
201        """
202        Public method to resume the thread.
203
204        It resumes the thread stopping only at breakpoints or exceptions.
205
206        @param special flag indicating a special continue operation
207        """
208        self.set_continue(special)
209
210    def setRecursionDepth(self, frame):
211        """
212        Public method to determine the current recursion depth.
213
214        @param frame The current stack frame.
215        """
216        self.__recursionDepth = 0
217        while frame is not None:
218            self.__recursionDepth += 1
219            frame = frame.f_back
220
221    def profileWithRecursion(self, frame, event, arg):
222        """
223        Public method used to trace some stuff independent of the debugger
224        trace function.
225
226        @param frame current stack frame
227        @type frame object
228        @param event trace event
229        @type str
230        @param arg arguments
231        @type depends on the previous event parameter
232        @exception RuntimeError raised to indicate too many recursions
233        """
234        if event == 'return':
235            self.cFrame = frame.f_back
236            self.__recursionDepth -= 1
237            if self._dbgClient.callTraceEnabled:
238                self.__sendCallTrace(event, frame, self.cFrame)
239        elif event == 'call':
240            if self._dbgClient.callTraceEnabled:
241                self.__sendCallTrace(event, self.cFrame, frame)
242            self.cFrame = frame
243            self.__recursionDepth += 1
244            if self.__recursionDepth > gRecursionLimit:
245                raise RuntimeError(
246                    'maximum recursion depth exceeded\n'
247                    '(offending frame is two down the stack)')
248
249    def profile(self, frame, event, arg):
250        """
251        Public method used to trace some stuff independent of the debugger
252        trace function.
253
254        @param frame current stack frame
255        @type frame object
256        @param event trace event
257        @type str
258        @param arg arguments
259        @type depends on the previous event parameter
260        """
261        if event == 'return':
262            self.__sendCallTrace(event, frame, frame.f_back)
263        elif event == 'call':
264            self.__sendCallTrace(event, frame.f_back, frame)
265
266    def __sendCallTrace(self, event, fromFrame, toFrame):
267        """
268        Private method to send a call/return trace.
269
270        @param event trace event
271        @type str
272        @param fromFrame originating frame
273        @type frame object
274        @param toFrame destination frame
275        @type frame object
276        """
277        if not self.__skipFrame(fromFrame) and not self.__skipFrame(toFrame):
278            fromInfo = {
279                "filename": self._dbgClient.absPath(
280                    self.fix_frame_filename(fromFrame)),
281                "linenumber": fromFrame.f_lineno,
282                "codename": fromFrame.f_code.co_name,
283            }
284            toInfo = {
285                "filename": self._dbgClient.absPath(
286                    self.fix_frame_filename(toFrame)),
287                "linenumber": toFrame.f_lineno,
288                "codename": toFrame.f_code.co_name,
289            }
290            self._dbgClient.sendCallTrace(event, fromInfo, toInfo)
291
292    def trace_dispatch(self, frame, event, arg):
293        """
294        Public method reimplemented from bdb.py to do some special things.
295
296        This specialty is to check the connection to the debug server
297        for new events (i.e. new breakpoints) while we are going through
298        the code.
299
300        @param frame The current stack frame
301        @type frame object
302        @param event The trace event
303        @type str
304        @param arg The arguments
305        @type depends on the previous event parameter
306        @return local trace function
307        @rtype trace function or None
308        @exception SystemExit
309        """
310        # give the client a chance to push through new break points.
311        if self.eventPollFlag:
312            self._dbgClient.eventPoll()
313            self.eventPollFlag = False
314
315            if self.quitting:
316                raise SystemExit
317
318        if event == 'line':
319            if self.stop_here(frame) or self.break_here(frame):
320                if (
321                    self.stop_everywhere and
322                    frame.f_back and
323                    frame.f_back.f_code.co_name == "prepareJsonCommand"
324                ):
325                    # Just stepped into print statement, so skip these frames
326                    self._set_stopinfo(None, frame.f_back)
327                else:
328                    self.user_line(frame)
329            return self.trace_dispatch
330
331        if event == 'call':
332            if (
333                self.stop_here(frame) or
334                self.__checkBreakInFrame(frame) or
335                Watch.watches != []
336            ) or (
337                self.stopframe and
338                frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS
339            ):
340                return self.trace_dispatch
341            else:
342                # No need to trace this function
343                return None
344
345        if event == 'return':
346            if self.stop_here(frame) or frame == self.returnframe:
347                # Ignore return events in generator except when stepping.
348                if (
349                    self.stopframe and
350                    frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS
351                ):
352                    return self.trace_dispatch
353                # Only true if we didn't stop in this frame, because it's
354                # belonging to the eric debugger.
355                if self.stopframe is frame and self.stoplineno != -1:
356                    self._set_stopinfo(None, frame.f_back)
357            return None
358
359        if event == 'exception':
360            if not self.__skipFrame(frame):
361                # When stepping with next/until/return in a generator frame,
362                # skip the internal StopIteration exception (with no traceback)
363                # triggered by a subiterator run with the 'yield from'
364                # statement.
365                if not (
366                    frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS and
367                    arg[0] is StopIteration and
368                    arg[2] is None
369                ):
370                    self.user_exception(arg)
371            # Stop at the StopIteration or GeneratorExit exception when the
372            # user has set stopframe in a generator by issuing a return
373            # command, or a next/until command at the last statement in the
374            # generator before the exception.
375            elif (
376                self.stopframe and
377                frame is not self.stopframe and
378                (self.stopframe.f_code.co_flags &
379                 GENERATOR_AND_COROUTINE_FLAGS) and
380                arg[0] in (StopIteration, GeneratorExit)
381            ):
382                self.user_exception(arg)
383            return None
384
385        if event == 'c_call':
386            return None
387        if event == 'c_exception':
388            return None
389        if event == 'c_return':
390            return None
391
392        print('DebugBase.trace_dispatch:'       # __IGNORE_WARNING_M801__
393              ' unknown debugging event: ',
394              repr(event))
395
396        return self.trace_dispatch
397
398    def set_trace(self, frame=None):
399        """
400        Public method to start debugging from 'frame'.
401
402        If frame is not specified, debugging starts from caller's frame.
403        Because of jump optimizations it's not possible to use sys.breakpoint()
404        as last instruction in a function or method.
405
406        @param frame frame to start debugging from
407        @type frame object
408        """
409        if frame is None:
410            frame = sys._getframe().f_back  # Skip set_trace method
411
412        stopOnHandleCommand = self._dbgClient.handleJsonCommand.__code__
413
414        frame.f_trace = self.trace_dispatch
415        while frame.f_back is not None:
416            # stop at eric's debugger frame or a threading bootstrap
417            if frame.f_back.f_code == stopOnHandleCommand:
418                frame.f_trace = self.trace_dispatch
419                break
420
421            frame = frame.f_back
422
423        self.stop_everywhere = True
424        sys.settrace(self.trace_dispatch)
425        sys.setprofile(self._dbgClient.callTraceEnabled)
426
427    def bootstrap(self, target, args, kwargs):
428        """
429        Public method to bootstrap a thread.
430
431        It wraps the call to the user function to enable tracing
432        before hand.
433
434        @param target function which is called in the new created thread
435        @type function pointer
436        @param args arguments to pass to target
437        @type tuple
438        @param kwargs keyword arguments to pass to target
439        @type dict
440        """
441        try:
442            # Because in the initial run method the "base debug" function is
443            # set up, it's also valid for the threads afterwards.
444            sys.settrace(self.trace_dispatch)
445
446            target(*args, **kwargs)
447        except Exception:
448            excinfo = sys.exc_info()
449            self.user_exception(excinfo, True)
450        finally:
451            sys.settrace(None)
452            sys.setprofile(None)
453
454    def run(self, cmd, globalsDict=None, localsDict=None, debug=True,
455            closeSession=True):
456        """
457        Public method to start a given command under debugger control.
458
459        @param cmd command / code to execute under debugger control
460        @type str or CodeType
461        @param globalsDict dictionary of global variables for cmd
462        @type dict
463        @param localsDict dictionary of local variables for cmd
464        @type dict
465        @param debug flag if command should run under debugger control
466        @type bool
467        @return exit code of the program
468        @rtype int
469        @param closeSession flag indicating to close the debugger session
470            at exit
471        @type bool
472        """
473        if globalsDict is None:
474            import __main__
475            globalsDict = __main__.__dict__
476
477        if localsDict is None:
478            localsDict = globalsDict
479
480        if not isinstance(cmd, types.CodeType):
481            cmd = compile(cmd, "<string>", "exec")
482
483        if debug:
484            # First time the trace_dispatch function is called, a "base debug"
485            # function has to be returned, which is called at every user code
486            # function call. This is ensured by setting stop_everywhere.
487            self.stop_everywhere = True
488            sys.settrace(self.trace_dispatch)
489
490        try:
491            exec(cmd, globalsDict, localsDict)      # secok
492            atexit._run_exitfuncs()
493            self._dbgClient.progTerminated(0, closeSession=closeSession)
494            exitcode = 0
495        except SystemExit:
496            atexit._run_exitfuncs()
497            excinfo = sys.exc_info()
498            exitcode, message = self.__extractSystemExitMessage(excinfo)
499            self._dbgClient.progTerminated(exitcode, message=message,
500                                           closeSession=closeSession)
501        except Exception:
502            excinfo = sys.exc_info()
503            self.user_exception(excinfo, True)
504            exitcode = 242
505        finally:
506            self.quitting = True
507            sys.settrace(None)
508        return exitcode
509
510    def _set_stopinfo(self, stopframe, returnframe, stoplineno=0):
511        """
512        Protected method to update the frame pointers.
513
514        @param stopframe the frame object where to stop
515        @type frame object
516        @param returnframe the frame object where to stop on a function return
517        @type frame object
518        @param stoplineno line number to stop at. If stoplineno is greater than
519            or equal to 0, then stop at line greater than or equal to the
520            stopline. If stoplineno is -1, then don't stop at all.
521        @type int
522        """
523        self.stopframe = stopframe
524        self.returnframe = returnframe
525        # stoplineno >= 0 means: stop at line >= the stoplineno
526        # stoplineno -1 means: don't stop at all
527        self.stoplineno = stoplineno
528
529        if returnframe is not None:
530            # Ensure to be able to stop on the return frame
531            returnframe.f_trace = self.trace_dispatch
532        self.stop_everywhere = False
533
534    def set_continue(self, special):
535        """
536        Public method to stop only on next breakpoint.
537
538        @param special flag indicating a special continue operation
539        @type bool
540        """
541        # Here we only set a new stop frame if it is a normal continue.
542        if not special:
543            self._set_stopinfo(None, None, -1)
544
545        # Disable tracing if not started in debug mode
546        if not self._dbgClient.debugging:
547            sys.settrace(None)
548            sys.setprofile(None)
549
550    def set_until(self, frame=None, lineno=None):
551        """
552        Public method to stop when the line with the lineno greater than the
553        current one is reached or when returning from current frame.
554
555        @param frame reference to the frame object
556        @type frame object
557        @param lineno line number to continue to
558        @type int
559        """
560        # the name "until" is borrowed from gdb
561        if frame is None:
562            frame = self.currentFrame
563        if lineno is None:
564            lineno = frame.f_lineno + 1
565        self._set_stopinfo(frame, frame, lineno)
566
567    def set_step(self):
568        """
569        Public method to stop after one line of code.
570        """
571        self._set_stopinfo(None, None)
572        self.stop_everywhere = True
573
574    def set_next(self, frame):
575        """
576        Public method to stop on the next line in or below the given frame.
577
578        @param frame the frame object
579        @type frame object
580        """
581        self._set_stopinfo(frame, frame.f_back)
582        frame.f_trace = self.trace_dispatch
583
584    def set_return(self, frame):
585        """
586        Public method to stop when returning from the given frame.
587
588        @param frame the frame object
589        @type frame object
590        """
591        self._set_stopinfo(None, frame.f_back)
592
593    def move_instruction_pointer(self, lineno):
594        """
595        Public methode to move the instruction pointer to another line.
596
597        @param lineno new line number
598        @type int
599        """
600        try:
601            self.currentFrame.f_lineno = lineno
602            stack = self.getStack(self.currentFrame)
603            self._dbgClient.sendResponseLine(stack, self.name)
604        except Exception as e:
605            printerr(e)
606
607    def set_quit(self):
608        """
609        Public method to quit.
610
611        Disables the trace functions and resets all frame pointer.
612        """
613        sys.setprofile(None)
614        self.stopframe = None
615        self.returnframe = None
616        for debugThread in self._dbgClient.threads.values():
617            debugThread.quitting = True
618
619    def fix_frame_filename(self, frame):
620        """
621        Public method used to fixup the filename for a given frame.
622
623        The logic employed here is that if a module was loaded
624        from a .pyc file, then the correct .py to operate with
625        should be in the same path as the .pyc. The reason this
626        logic is needed is that when a .pyc file is generated, the
627        filename embedded and thus what is readable in the code object
628        of the frame object is the fully qualified filepath when the
629        pyc is generated. If files are moved from machine to machine
630        this can break debugging as the .pyc will refer to the .py
631        on the original machine. Another case might be sharing
632        code over a network... This logic deals with that.
633
634        @param frame the frame object
635        @type frame object
636        @return fixed up file name
637        @rtype str
638        """
639        # get module name from __file__
640        fn = frame.f_globals.get('__file__')
641        try:
642            return self._fnCache[fn]
643        except KeyError:
644            if fn is None:
645                return frame.f_code.co_filename
646
647            absFilename = os.path.abspath(fn)
648            if absFilename.endswith(('.pyc', '.pyo', '.pyd')):
649                fixedName = absFilename[:-1]
650                if not os.path.exists(fixedName):
651                    fixedName = absFilename
652            else:
653                fixedName = absFilename
654            # update cache
655            self._fnCache[fn] = fixedName
656            return fixedName
657
658    def __checkBreakInFrame(self, frame):
659        """
660        Private method to check if the function / method has a line number
661        which is a breakpoint.
662
663        @param frame the frame object
664        @type frame object
665        @return Flag indicating a function / method with breakpoint
666        @rtype bool
667        """
668        try:
669            return Breakpoint.breakInFrameCache[
670                frame.f_globals.get('__file__'),
671                frame.f_code.co_firstlineno]
672        except KeyError:
673            filename = self.fix_frame_filename(frame)
674            if filename not in Breakpoint.breakInFile:
675                Breakpoint.breakInFrameCache[
676                    frame.f_globals.get('__file__'),
677                    frame.f_code.co_firstlineno] = False
678                return False
679            lineNo = frame.f_code.co_firstlineno
680            lineNumbers = [lineNo]
681
682            co_lnotab = frame.f_code.co_lnotab[1::2]
683
684            # No need to handle special case if a lot of lines between
685            # (e.g. closure), because the additional lines won't cause a bp
686            for co_lno in co_lnotab:
687                if co_lno >= 0x80:
688                    lineNo -= 0x100
689                lineNo += co_lno
690                lineNumbers.append(lineNo)
691
692            for bp in Breakpoint.breakInFile[filename]:
693                if bp in lineNumbers:
694                    Breakpoint.breakInFrameCache[
695                        frame.f_globals.get('__file__'),
696                        frame.f_code.co_firstlineno] = True
697                    return True
698            Breakpoint.breakInFrameCache[
699                frame.f_globals.get('__file__'),
700                frame.f_code.co_firstlineno] = False
701            return False
702
703    def break_here(self, frame):
704        """
705        Public method reimplemented from bdb.py to fix the filename from the
706        frame.
707
708        See fix_frame_filename for more info.
709
710        @param frame the frame object
711        @type frame object
712        @return flag indicating the break status
713        @rtype bool
714        """
715        filename = self.fix_frame_filename(frame)
716        if (filename, frame.f_lineno) in Breakpoint.breaks:
717            bp, flag = Breakpoint.effectiveBreak(
718                filename, frame.f_lineno, frame)
719            if bp:
720                # flag says ok to delete temp. bp
721                if flag and bp.temporary:
722                    self.__do_clearBreak(filename, frame.f_lineno)
723                return True
724
725        if Watch.watches != []:
726            bp, flag = Watch.effectiveWatch(frame)
727            if bp:
728                # flag says ok to delete temp. watch
729                if flag and bp.temporary:
730                    self.__do_clearWatch(bp.cond)
731                return True
732
733        return False
734
735    def __do_clearBreak(self, filename, lineno):
736        """
737        Private method called to clear a temporary breakpoint.
738
739        @param filename name of the file the bp belongs to
740        @type str
741        @param lineno linenumber of the bp
742        @type int
743        """
744        Breakpoint.clear_break(filename, lineno)
745        self._dbgClient.sendClearTemporaryBreakpoint(filename, lineno)
746
747    def __do_clearWatch(self, cond):
748        """
749        Private method called to clear a temporary watch expression.
750
751        @param cond expression of the watch expression to be cleared
752        @type str
753        """
754        Watch.clear_watch(cond)
755        self._dbgClient.sendClearTemporaryWatch(cond)
756
757    def getStack(self, frame=None, applyTrace=False):
758        """
759        Public method to get the stack.
760
761        @param frame frame object to inspect
762        @type frame object or list
763        @param applyTrace flag to assign trace function to fr.f_trace
764        @type bool
765        @return list of lists with file name (string), line number (integer)
766            and function name (string)
767        """
768        tb_lineno = None
769        if frame is None:
770            fr = self.getCurrentFrame()
771        elif type(frame) == list:
772            fr, tb_lineno = frame.pop(0)
773        else:
774            fr = frame
775
776        stack = []
777        while fr is not None:
778            if applyTrace:
779                # Reset the trace function so we can be sure
780                # to trace all functions up the stack... This gets around
781                # problems where an exception/breakpoint has occurred
782                # but we had disabled tracing along the way via a None
783                # return from dispatch_call
784                fr.f_trace = self.trace_dispatch
785
786            fname = self._dbgClient.absPath(self.fix_frame_filename(fr))
787            # Always show at least one stack frame, even if it's from eric.
788            if stack and os.path.basename(fname).startswith(
789                ("DebugBase.py", "DebugClientBase.py",
790                 "ThreadExtension.py", "threading.py")
791            ):
792                break
793
794            fline = tb_lineno or fr.f_lineno
795            ffunc = fr.f_code.co_name
796
797            if ffunc == '?':
798                ffunc = ''
799
800            if ffunc and not ffunc.startswith("<"):
801                argInfo = getargvalues(fr)
802                try:
803                    fargs = formatargvalues(
804                        argInfo.args, argInfo.varargs,
805                        argInfo.keywords, argInfo.locals)
806                except Exception:
807                    fargs = ""
808            else:
809                fargs = ""
810
811            stack.append([fname, fline, ffunc, fargs])
812
813            # is it a stack frame or exception list?
814            if type(frame) == list:
815                if frame != []:
816                    fr, tb_lineno = frame.pop(0)
817                else:
818                    fr = None
819            else:
820                fr = fr.f_back
821
822        return stack
823
824    def user_line(self, frame):
825        """
826        Public method reimplemented to handle the program about to execute a
827        particular line.
828
829        @param frame the frame object
830        """
831        # We never stop on line 0.
832        if frame.f_lineno == 0:
833            return
834
835        self.isBroken = True
836        self.currentFrame = frame
837        stack = self.getStack(frame, applyTrace=True)
838
839        self._dbgClient.lockClient()
840        self._dbgClient.currentThread = self
841        self._dbgClient.currentThreadExec = self
842
843        self._dbgClient.sendResponseLine(stack, self.name)
844        self._dbgClient.eventLoop()
845
846        self.isBroken = False
847        self._dbgClient.unlockClient()
848
849        self._dbgClient.dumpThreadList()
850
851    def user_exception(self, excinfo, unhandled=False):
852        """
853        Public method reimplemented to report an exception to the debug server.
854
855        @param excinfo details about the exception
856        @type tuple(Exception, excval object, traceback frame object)
857        @param unhandled flag indicating an uncaught exception
858        @type bool
859        """
860        exctype, excval, exctb = excinfo
861
862        if ((exctype in [GeneratorExit, StopIteration] and
863             unhandled is False) or
864                exctype == SystemExit):
865            # ignore these
866            return
867
868        if exctype in [SyntaxError, IndentationError]:
869            try:
870                if type(excval) == tuple:
871                    message, details = excval
872                    filename, lineno, charno, text = details
873                else:
874                    message = excval.msg
875                    filename = excval.filename
876                    lineno = excval.lineno
877                    charno = excval.offset
878
879                if filename is None:
880                    realSyntaxError = False
881                else:
882                    if charno is None:
883                        charno = 0
884
885                    filename = os.path.abspath(filename)
886                    realSyntaxError = os.path.exists(filename)
887
888            except (AttributeError, ValueError):
889                message = ""
890                filename = ""
891                lineno = 0
892                charno = 0
893                realSyntaxError = True
894
895            if realSyntaxError:
896                self._dbgClient.sendSyntaxError(
897                    message, filename, lineno, charno, self.name)
898                self._dbgClient.eventLoop()
899                return
900
901        self.skipFrames = 0
902        if (exctype == RuntimeError and
903                str(excval).startswith('maximum recursion depth exceeded') or
904                sys.version_info >= (3, 5) and
905                exctype == RecursionError):  # __IGNORE_WARNING__
906            excval = 'maximum recursion depth exceeded'
907            depth = 0
908            tb = exctb
909            while tb:
910                tb = tb.tb_next
911
912                if (tb and tb.tb_frame.f_code.co_name == 'trace_dispatch' and
913                        __file__.startswith(tb.tb_frame.f_code.co_filename)):
914                    depth = 1
915                self.skipFrames += depth
916
917            # always 1 if running without debugger
918            self.skipFrames = max(1, self.skipFrames)
919
920        exctype = self.__extractExceptionName(exctype)
921
922        if excval is None:
923            excval = ''
924
925        exctypetxt = (
926            "unhandled {0!s}".format(str(exctype))
927            if unhandled else
928            str(exctype)
929        )
930        excvaltxt = str(excval)
931
932        # Don't step into libraries, which are used by our debugger methods
933        if exctb is not None:
934            self.stop_everywhere = False
935
936        self.isBroken = True
937        self.isException = True
938
939        disassembly = None
940        stack = []
941        if exctb:
942            frlist = self.__extract_stack(exctb)
943            frlist.reverse()
944            disassembly = self.__disassemble(frlist[0][0])
945
946            self.currentFrame = frlist[0][0]
947            stack = self.getStack(frlist[self.skipFrames:])
948
949        self._dbgClient.lockClient()
950        self._dbgClient.currentThread = self
951        self._dbgClient.currentThreadExec = self
952        self._dbgClient.sendException(exctypetxt, excvaltxt, stack, self.name)
953        self._dbgClient.setDisassembly(disassembly)
954        self._dbgClient.dumpThreadList()
955
956        if exctb is not None:
957            # When polling kept enabled, it isn't possible to resume after an
958            # unhandled exception without further user interaction.
959            self._dbgClient.eventLoop(True)
960
961        self.skipFrames = 0
962
963        self.isBroken = False
964        self.isException = False
965        stop_everywhere = self.stop_everywhere
966        self.stop_everywhere = False
967        self.eventPollFlag = False
968        self._dbgClient.unlockClient()
969        self.stop_everywhere = stop_everywhere
970
971        self._dbgClient.dumpThreadList()
972
973    def __extractExceptionName(self, exctype):
974        """
975        Private method to extract the exception name given the exception
976        type object.
977
978        @param exctype type of the exception
979        @return exception name (string)
980        """
981        return str(exctype).replace("<class '", "").replace("'>", "")
982
983    def __extract_stack(self, exctb):
984        """
985        Private member to return a list of stack frames.
986
987        @param exctb exception traceback
988        @return list of stack frames
989        """
990        tb = exctb
991        stack = []
992        while tb is not None:
993            stack.append((tb.tb_frame, tb.tb_lineno))
994            tb = tb.tb_next
995        tb = None
996        return stack
997
998    def __disassemble(self, frame):
999        """
1000        Private method to generate a disassembly of the given code object.
1001
1002        @param frame frame object to be disassembled
1003        @type code
1004        @return dictionary containing the disassembly information
1005        @rtype dict
1006        """
1007        co = frame.f_code
1008        disDict = {
1009            "lasti": frame.f_lasti,
1010            "firstlineno": co.co_firstlineno,
1011            "instructions": [],
1012        }
1013
1014        # 1. disassembly info
1015        for instr in dis.get_instructions(co):
1016            instrDict = {
1017                "lineno":
1018                    0 if instr.starts_line is None else instr.starts_line,
1019                "isJumpTarget": instr.is_jump_target,
1020                "offset": instr.offset,
1021                "opname": instr.opname,
1022                "arg": instr.arg,
1023                "argrepr": instr.argrepr,
1024            }
1025            disDict["instructions"].append(instrDict)
1026
1027        # 2. code info
1028        # Note: keep in sync with PythonDisViewer.__createCodeInfo()
1029        disDict["codeinfo"] = {
1030            "name": co.co_name,
1031            "filename": co.co_filename,
1032            "firstlineno": co.co_firstlineno,
1033            "argcount": co.co_argcount,
1034            "kwonlyargcount": co.co_kwonlyargcount,
1035            "nlocals": co.co_nlocals,
1036            "stacksize": co.co_stacksize,
1037            "flags": dis.pretty_flags(co.co_flags),
1038            "consts": [str(const) for const in co.co_consts],
1039            "names": [str(name) for name in co.co_names],
1040            "varnames": [str(name) for name in co.co_varnames],
1041            "freevars": [str(var) for var in co.co_freevars],
1042            "cellvars": [str(var) for var in co.co_cellvars],
1043        }
1044        try:
1045            disDict["codeinfo"]["posonlyargcount"] = co.co_posonlyargcount
1046        except AttributeError:
1047            # does not exist prior to 3.8.0
1048            disDict["codeinfo"]["posonlyargcount"] = 0
1049
1050        return disDict
1051
1052    def __extractSystemExitMessage(self, excinfo):
1053        """
1054        Private method to get the SystemExit code and message.
1055
1056        @param excinfo details about the SystemExit exception
1057        @type tuple(Exception, excval object, traceback frame object)
1058        @return SystemExit code and message
1059        @rtype int, str
1060        """
1061        exctype, excval, exctb = excinfo
1062        if excval is None:
1063            exitcode = 0
1064            message = ""
1065        elif isinstance(excval, str):
1066            exitcode = 1
1067            message = excval
1068        elif isinstance(excval, bytes):
1069            exitcode = 1
1070            message = excval.decode()
1071        elif isinstance(excval, int):
1072            exitcode = excval
1073            message = ""
1074        elif isinstance(excval, SystemExit):
1075            code = excval.code
1076            if isinstance(code, str):
1077                exitcode = 1
1078                message = code
1079            elif isinstance(code, bytes):
1080                exitcode = 1
1081                message = code.decode()
1082            elif isinstance(code, int):
1083                exitcode = code
1084                message = ""
1085            elif code is None:
1086                exitcode = 0
1087                message = ""
1088            else:
1089                exitcode = 1
1090                message = str(code)
1091        else:
1092            exitcode = 1
1093            message = str(excval)
1094
1095        return exitcode, message
1096
1097    def stop_here(self, frame):
1098        """
1099        Public method reimplemented to filter out debugger files.
1100
1101        Tracing is turned off for files that are part of the
1102        debugger that are called from the application being debugged.
1103
1104        @param frame the frame object
1105        @type frame object
1106        @return flag indicating whether the debugger should stop here
1107        @rtype bool
1108        """
1109        if self.__skipFrame(frame):
1110            return False
1111
1112        if frame is self.stopframe:
1113            if self.stoplineno == -1:
1114                return False
1115            return frame.f_lineno >= self.stoplineno
1116        return self.stop_everywhere or frame is self.returnframe
1117
1118    def tracePythonLibs(self, enable):
1119        """
1120        Public method to update the settings to trace into Python libraries.
1121
1122        @param enable flag to debug into Python libraries
1123        @type bool
1124        """
1125        pathsToSkip = list(self.pathsToSkip)
1126        # don't trace into Python library?
1127        if enable:
1128            pathsToSkip = [x for x in pathsToSkip if not x.endswith(
1129                ("site-packages", "dist-packages", self.lib))]
1130        else:
1131            pathsToSkip.append(self.lib)
1132            localLib = [x for x in sys.path if x.endswith(("site-packages",
1133                        "dist-packages")) and not x.startswith(self.lib)]
1134            pathsToSkip.extend(localLib)
1135
1136        self.pathsToSkip = tuple(set(pathsToSkip))
1137
1138    def __skipFrame(self, frame):
1139        """
1140        Private method to filter out debugger files.
1141
1142        Tracing is turned off for files that are part of the
1143        debugger that are called from the application being debugged.
1144
1145        @param frame the frame object
1146        @type frame object
1147        @return flag indicating whether the debugger should skip this frame
1148        @rtype bool
1149        """
1150        try:
1151            return self.filesToSkip[frame.f_code.co_filename]
1152        except KeyError:
1153            ret = frame.f_code.co_filename.startswith(self.pathsToSkip)
1154            self.filesToSkip[frame.f_code.co_filename] = ret
1155            return ret
1156        except AttributeError:
1157            # if frame is None
1158            return True
1159