vibefpmgui.debug_session

A debug session: a program run under the debugger (vibefpmgui.dap's client, with an adapter from vibefpmgui.debug_adapters), and what it's doing: running, stopped (where, with its call stack and variables), or ended.

The order of things, as adapters expect it: initialize, then launch (gdb answers it only after the configuration is done), and on the initialized event the breakpoints, then configurationDone. At each stop: the threads, the stopped thread's call stack, then the scopes (Locals, Arguments, module variables…) of the frame shown; variables are asked for as they're expanded.

  1"""A debug session: a program run under the debugger (`vibefpmgui.dap`'s
  2client, with an adapter from `vibefpmgui.debug_adapters`), and what it's
  3doing: running, stopped (where, with its call stack and variables), or ended.
  4
  5The order of things, as adapters expect it: ``initialize``, then ``launch``
  6(gdb answers it only after the configuration is done), and on the
  7``initialized`` event the breakpoints, then ``configurationDone``. At each
  8stop: the threads, the stopped thread's call stack, then the scopes (Locals,
  9Arguments, module variables…) of the frame shown; variables are asked for
 10as they're expanded.
 11"""
 12
 13from __future__ import annotations
 14
 15import re
 16from collections.abc import Callable
 17from dataclasses import dataclass, field
 18from pathlib import Path
 19
 20from qtpy.QtCore import QObject, Signal
 21
 22from .dap import DapClient
 23from .debug_adapters import Adapter
 24
 25IDLE, STARTING, RUNNING, STOPPED = "idle", "starting", "running", "stopped"
 26MAX_ELEMENTS = 1000  # an array's elements asked for at once (a big one: the first ones, then "…")
 27
 28# gdb's own messages, sent as the program's output: breakpoints set before the
 29# program is loaded (they're placed when it is: see breakpoint events). With its
 30# styling on (a terminal, a UTF-8 locale), gdb puts a symbol in front: "❌️ No
 31# source file…".
 32_GDB_NOISE = re.compile(r"^[^\w\s]*\s*(No source file named .*\.|Breakpoint \d+ \(.*\) pending\.)\s*$")
 33
 34
 35@dataclass
 36class Frame:
 37    id: int
 38    name: str
 39    path: str | None  # (None: no source: a library's)
 40    line: int
 41
 42
 43@dataclass
 44class Scope:
 45    name: str
 46    reference: int
 47    expensive: bool = False
 48
 49
 50@dataclass
 51class Variable:
 52    name: str
 53    value: str
 54    reference: int = 0  # (non-zero: it has parts: an array's elements, a type's components)
 55    count: int = 0  # (its elements or components, if the adapter says)
 56    type: str = ""
 57
 58
 59@dataclass
 60class BreakpointState:
 61    verified: bool
 62    message: str = ""
 63
 64
 65@dataclass
 66class _Breakpoints:
 67    """The breakpoints sent for each file, and what the debugger said of them."""
 68    lines: dict[str, list[int]] = field(default_factory=dict)
 69    ids: dict[int, tuple[str, int]] = field(default_factory=dict)  # the debugger's id → (file, line)
 70    pending: set[tuple[str, int]] = field(default_factory=set)  # "pending" (gdb's: not placed yet)
 71
 72
 73class DebugSession(QObject):
 74    state_changed = Signal(str)
 75    stopped_at = Signal(object, str)  # [Frame] (the call stack, innermost first), why ("breakpoint", "step"…)
 76    scopes_ready = Signal(int, object)  # frame index, [Scope]
 77    output = Signal(str, str)  # category ("stdout", "stderr", "console"), text
 78    breakpoints_checked = Signal(str, object)  # file, {line: BreakpointState}
 79    ended = Signal(str)  # why ("exited with code 0", "stopped", the debugger's error)
 80
 81    def __init__(self, parent=None):
 82        super().__init__(parent)
 83        self.client = DapClient(self)
 84        self.client.event.connect(self._event)
 85        self.client.stopped.connect(self._adapter_stopped)
 86        self.adapter: Adapter | None = None
 87        self.state = IDLE
 88        self.frames: list[Frame] = []
 89        self.frame_index = 0
 90        self.thread_id: int | None = None
 91        self.exit_code: int | None = None
 92        self.hit_breakpoints: list[tuple[str, int]] = []  # (file, line as set) the last stop was at
 93        self._breakpoints = _Breakpoints()
 94        self._launch: dict = {}
 95        self._ending = False
 96
 97    # ---------------------------------------------------------- state
 98    def is_active(self) -> bool:
 99        return self.state != IDLE
100
101    def _set_state(self, state: str):
102        if state != self.state:
103            self.state = state
104            self.state_changed.emit(state)
105
106    def _end(self, why: str):
107        if self.state == IDLE:
108            return
109        self._ending = True
110        self.client.stop()
111        self._ending = False
112        self.frames, self.thread_id = [], None
113        self._set_state(IDLE)
114        self.ended.emit(why)
115
116    # ---------------------------------------------------------- starting
117    def start(self, adapter: Adapter, program: str, args: list[str], cwd: str,
118              breakpoints: dict[str, list[int]]) -> bool:
119        """Run ``program`` under the debugger, stopping at ``breakpoints``
120        (file → lines)."""
121        if self.is_active():
122            self.stop()
123        self.adapter = adapter
124        self.exit_code = None
125        self.frames, self.thread_id = [], None
126        self._breakpoints = _Breakpoints({path: sorted(set(lines)) for path, lines in breakpoints.items() if lines})
127        self._launch = adapter.launch_arguments(program, args, cwd)
128        self._set_state(STARTING)
129        self.client.start(adapter.command, cwd)
130        self.client.request("initialize", {
131            "clientID": "vibefpmgui", "clientName": "VibeFPMGUI", "adapterID": adapter.kind,
132            "linesStartAt1": True, "columnsStartAt1": True, "pathFormat": "path",
133            "supportsVariableType": True, "supportsRunInTerminalRequest": False,
134        }, self._initialized)
135        return True
136
137    def _initialized(self, success: bool, _body: dict, message: str):
138        if not success:
139            self._end(f"the debugger didn't start: {message}")
140            return
141        # (gdb answers launch only once the configuration is done: no timeout.)
142        self.client.request("launch", self._launch, self._launched, timeout=0)
143
144    def _launched(self, success: bool, _body: dict, message: str):
145        if not success and self.state != IDLE:
146            self._end(f"the program didn't start: {message or 'the debugger refused it'}")
147
148    def _configure(self):
149        """The adapter is ready for the breakpoints (the initialized event)."""
150        for path in list(self._breakpoints.lines):
151            self._send_breakpoints(path)
152        self.client.request("configurationDone", {}, self._configured)
153
154    def _configured(self, success: bool, _body: dict, message: str):
155        if not success:
156            self._end(f"the debugger couldn't start the program: {message}")
157        elif self.state == STARTING:
158            self._set_state(RUNNING)
159
160    # ---------------------------------------------------------- breakpoints
161    def set_breakpoints(self, path: str, lines: list[int]):
162        """A file's breakpoints now (sent to the debugger, if one is running)."""
163        self._breakpoints.lines[path] = sorted(set(lines))
164        if self.state in (RUNNING, STOPPED):
165            self._send_breakpoints(path)
166
167    def _send_breakpoints(self, path: str):
168        lines = self._breakpoints.lines.get(path, [])
169        self.client.request("setBreakpoints", {"source": {"path": path, "name": Path(path).name},
170                                               "breakpoints": [{"line": line} for line in lines]},
171                            lambda success, body, message, p=path, ls=lines: self._breakpoints_set(
172                                p, ls, success, body, message))
173
174    def _breakpoints_set(self, path: str, lines: list[int], success: bool, body: dict, message: str):
175        states: dict[int, BreakpointState] = {}
176        found = body.get("breakpoints") or [] if success else []
177        self._breakpoints.pending -= {(path, n) for n in self._breakpoints.lines.get(path, [])}
178        for line, answer in zip(lines, found, strict=False):
179            if isinstance(answer.get("id"), int):
180                self._breakpoints.ids[answer["id"]] = (path, line)
181            if not answer.get("verified") and answer.get("reason") == "pending":
182                self._breakpoints.pending.add((path, line))
183            states[line] = BreakpointState(_placed(answer), _why_not(answer))
184        for line in lines[len(found):]:
185            states[line] = BreakpointState(False, message or "the debugger didn't place it")
186        self.breakpoints_checked.emit(path, states)
187
188    def _breakpoint_event(self, body: dict):
189        answer = body.get("breakpoint") or {}
190        where = self._breakpoints.ids.get(answer.get("id"))
191        if where is not None and body.get("reason") in ("changed", "new"):
192            path, line = where
193            if answer.get("verified"):
194                self._breakpoints.pending.discard(where)
195            self.breakpoints_checked.emit(path, {line: BreakpointState(_placed(answer), _why_not(answer))})
196
197    # ---------------------------------------------------------- running
198    def _request_running(self, command: str, arguments: dict | None = None):
199        if self.state != STOPPED or self.thread_id is None:
200            return
201        self.frames = []
202        self._set_state(RUNNING)
203        self.client.request(command, {"threadId": self.thread_id, **(arguments or {})}, self._answered)
204
205    def _answered(self, success: bool, _body: dict, message: str):
206        if not success and message and self.state != IDLE:
207            self.output.emit("console", f"{message}\n")
208
209    def continue_(self):
210        self._request_running("continue")
211
212    def step_over(self):
213        self._request_running("next")
214
215    def step_in(self):
216        self._request_running("stepIn")
217
218    def step_out(self):
219        self._request_running("stepOut")
220
221    def pause(self):
222        if self.state == RUNNING:
223            self.client.request("pause", {"threadId": self.thread_id or 1}, self._answered)
224
225    def stop(self):
226        """End the session: the program and the debugger."""
227        if not self.is_active():
228            return
229        self.client.request("disconnect", {"terminateDebuggee": True}, timeout=1000)
230        self._end("stopped")
231
232    # ---------------------------------------------------------- events
233    def _event(self, name: str, body: dict):
234        if name == "initialized":
235            self._configure()
236        elif name == "stopped":
237            self.thread_id = body.get("threadId", self.thread_id)
238            # (Which breakpoints, as they were set: the debugger may have moved one to
239            # the next line with code, where the program stops.)
240            ids = self._breakpoints.ids
241            self.hit_breakpoints = [ids[n] for n in body.get("hitBreakpointIds") or [] if n in ids]
242            self._set_state(STOPPED)
243            self._unplaced()
244            why = str(body.get("reason") or "")
245            self.client.request("threads", {}, lambda ok, b, m: self._threads(ok, b, why))
246        elif name == "continued":
247            if self.state == STOPPED:
248                self.frames = []
249                self._set_state(RUNNING)
250        elif name == "output":
251            self._output(body)
252        elif name == "breakpoint":
253            self._breakpoint_event(body)
254        elif name == "exited":
255            self.exit_code = body.get("exitCode")
256        elif name == "terminated":
257            code = self.exit_code
258            self._end("the program ended" if code is None else f"the program exited with code {code}")
259
260    def _unplaced(self):
261        """Stopped, so the program is loaded: a breakpoint still "pending"
262        never will be placed (gdb leaves one on a line with no code so)."""
263        pending, self._breakpoints.pending = self._breakpoints.pending, set()
264        by_file: dict[str, dict[int, BreakpointState]] = {}
265        for path, line in pending:
266            by_file.setdefault(path, {})[line] = BreakpointState(False, "no code on that line: the debugger "
267                                                                        "couldn't place it")
268        for path, states in by_file.items():
269            self.breakpoints_checked.emit(path, states)
270
271    def _output(self, body: dict):
272        category, text = str(body.get("category") or "console"), str(body.get("output") or "")
273        if category == "telemetry" or not text:
274            return
275        if self.adapter is not None and self.adapter.kind == "gdb":
276            text = "".join(line for line in text.splitlines(keepends=True) if not _GDB_NOISE.match(line))
277        if text:
278            self.output.emit(category, text)
279
280    def _adapter_stopped(self, why: str):
281        if not self._ending:
282            self._end(why)
283
284    # ---------------------------------------------------------- where it stopped
285    def _threads(self, success: bool, body: dict, why: str):
286        threads = body.get("threads") or [] if success else []
287        if self.thread_id is None and threads:
288            self.thread_id = threads[0].get("id")
289        if self.thread_id is None:
290            return
291        self.client.request("stackTrace", {"threadId": self.thread_id, "levels": 100},
292                            lambda ok, b, m, w=why: self._stack(ok, b, w))
293
294    def _stack(self, success: bool, body: dict, why: str):
295        if self.state != STOPPED:
296            return
297        self.frames = [Frame(int(f.get("id", 0)), str(f.get("name") or "?"),
298                             (f.get("source") or {}).get("path"), int(f.get("line") or 0))
299                       for f in (body.get("stackFrames") or [] if success else [])]
300        # The first frame with source (not a library's): where the program is, for the user.
301        self.frame_index = next((i for i, f in enumerate(self.frames) if f.path), 0)
302        self.stopped_at.emit(self.frames, why)
303        self.select_frame(self.frame_index)
304
305    def select_frame(self, index: int):
306        """Show a frame of the call stack: its scopes (scopes_ready)."""
307        if self.state != STOPPED or not 0 <= index < len(self.frames):
308            return
309        self.frame_index = index
310        self.client.request("scopes", {"frameId": self.frames[index].id},
311                            lambda ok, b, m, i=index: self._scopes(i, ok, b))
312
313    def _scopes(self, index: int, success: bool, body: dict):
314        scopes = [Scope(str(s.get("name") or ""), int(s.get("variablesReference") or 0), bool(s.get("expensive")))
315                  for s in (body.get("scopes") or [] if success else [])
316                  # (A processor's registers: not what a Fortran programmer is after.)
317                  if s.get("presentationHint") != "registers" and str(s.get("name")).lower() != "registers"]
318        self.scopes_ready.emit(index, scopes)
319
320    def variables(self, reference: int, callback: Callable[[list[Variable], bool], None]):
321        """The variables of a scope, or the parts of one (an array's
322        elements, a type's components): ``callback(variables, more)``, more:
323        there were more than MAX_ELEMENTS."""
324        if self.state != STOPPED:
325            callback([], False)
326            return
327        self.client.request("variables", {"variablesReference": reference, "start": 0, "count": MAX_ELEMENTS},
328                            lambda ok, b, m: callback(*_variables(ok, b)))
329
330    def evaluate(self, expression: str, callback: Callable[[bool, str, int], None], context: str = "watch"):
331        """An expression's value in the frame shown: ``callback(ok, text, reference)``.
332        ("watch": an expression; gdb takes "repl" as one of its commands.)"""
333        frame = self.frames[self.frame_index].id if self.frames and self.state == STOPPED else None
334        arguments = {"expression": expression, "context": context}
335        if frame is not None:
336            arguments["frameId"] = frame
337        self.client.request("evaluate", arguments, lambda ok, b, m: callback(
338            ok, str(b.get("result", "")) if ok else (m or "can't evaluate that"), int(b.get("variablesReference") or 0)))
339
340
341def _placed(answer: dict) -> bool:
342    """Placed, or as good as: "pending" (gdb's, until the program is loaded,
343    when it says where it went), not shown as a problem meanwhile."""
344    return bool(answer.get("verified")) or answer.get("reason") == "pending"
345
346
347def _why_not(answer: dict) -> str:
348    if _placed(answer):
349        return ""
350    return str(answer.get("message") or "the debugger couldn't place it (no code on that line?)")
351
352
353def _variables(success: bool, body: dict) -> tuple[list[Variable], bool]:
354    found = body.get("variables") or [] if success else []
355    variables = [Variable(str(v.get("name") or ""), str(v.get("value") or ""), int(v.get("variablesReference") or 0),
356                          int(v.get("indexedVariables") or v.get("namedVariables") or 0), str(v.get("type") or ""))
357                 for v in found]
358    return variables, len(found) >= MAX_ELEMENTS
MAX_ELEMENTS = 1000
@dataclass
class Frame:
36@dataclass
37class Frame:
38    id: int
39    name: str
40    path: str | None  # (None: no source: a library's)
41    line: int
Frame(id: int, name: str, path: str | None, line: int)
id: int
name: str
path: str | None
line: int
@dataclass
class Scope:
44@dataclass
45class Scope:
46    name: str
47    reference: int
48    expensive: bool = False
Scope(name: str, reference: int, expensive: bool = False)
name: str
reference: int
expensive: bool = False
@dataclass
class Variable:
51@dataclass
52class Variable:
53    name: str
54    value: str
55    reference: int = 0  # (non-zero: it has parts: an array's elements, a type's components)
56    count: int = 0  # (its elements or components, if the adapter says)
57    type: str = ""
Variable( name: str, value: str, reference: int = 0, count: int = 0, type: str = '')
name: str
value: str
reference: int = 0
count: int = 0
type: str = ''
@dataclass
class BreakpointState:
60@dataclass
61class BreakpointState:
62    verified: bool
63    message: str = ""
BreakpointState(verified: bool, message: str = '')
verified: bool
message: str = ''
class DebugSession(PySide6.QtCore.QObject):
 74class DebugSession(QObject):
 75    state_changed = Signal(str)
 76    stopped_at = Signal(object, str)  # [Frame] (the call stack, innermost first), why ("breakpoint", "step"…)
 77    scopes_ready = Signal(int, object)  # frame index, [Scope]
 78    output = Signal(str, str)  # category ("stdout", "stderr", "console"), text
 79    breakpoints_checked = Signal(str, object)  # file, {line: BreakpointState}
 80    ended = Signal(str)  # why ("exited with code 0", "stopped", the debugger's error)
 81
 82    def __init__(self, parent=None):
 83        super().__init__(parent)
 84        self.client = DapClient(self)
 85        self.client.event.connect(self._event)
 86        self.client.stopped.connect(self._adapter_stopped)
 87        self.adapter: Adapter | None = None
 88        self.state = IDLE
 89        self.frames: list[Frame] = []
 90        self.frame_index = 0
 91        self.thread_id: int | None = None
 92        self.exit_code: int | None = None
 93        self.hit_breakpoints: list[tuple[str, int]] = []  # (file, line as set) the last stop was at
 94        self._breakpoints = _Breakpoints()
 95        self._launch: dict = {}
 96        self._ending = False
 97
 98    # ---------------------------------------------------------- state
 99    def is_active(self) -> bool:
100        return self.state != IDLE
101
102    def _set_state(self, state: str):
103        if state != self.state:
104            self.state = state
105            self.state_changed.emit(state)
106
107    def _end(self, why: str):
108        if self.state == IDLE:
109            return
110        self._ending = True
111        self.client.stop()
112        self._ending = False
113        self.frames, self.thread_id = [], None
114        self._set_state(IDLE)
115        self.ended.emit(why)
116
117    # ---------------------------------------------------------- starting
118    def start(self, adapter: Adapter, program: str, args: list[str], cwd: str,
119              breakpoints: dict[str, list[int]]) -> bool:
120        """Run ``program`` under the debugger, stopping at ``breakpoints``
121        (file → lines)."""
122        if self.is_active():
123            self.stop()
124        self.adapter = adapter
125        self.exit_code = None
126        self.frames, self.thread_id = [], None
127        self._breakpoints = _Breakpoints({path: sorted(set(lines)) for path, lines in breakpoints.items() if lines})
128        self._launch = adapter.launch_arguments(program, args, cwd)
129        self._set_state(STARTING)
130        self.client.start(adapter.command, cwd)
131        self.client.request("initialize", {
132            "clientID": "vibefpmgui", "clientName": "VibeFPMGUI", "adapterID": adapter.kind,
133            "linesStartAt1": True, "columnsStartAt1": True, "pathFormat": "path",
134            "supportsVariableType": True, "supportsRunInTerminalRequest": False,
135        }, self._initialized)
136        return True
137
138    def _initialized(self, success: bool, _body: dict, message: str):
139        if not success:
140            self._end(f"the debugger didn't start: {message}")
141            return
142        # (gdb answers launch only once the configuration is done: no timeout.)
143        self.client.request("launch", self._launch, self._launched, timeout=0)
144
145    def _launched(self, success: bool, _body: dict, message: str):
146        if not success and self.state != IDLE:
147            self._end(f"the program didn't start: {message or 'the debugger refused it'}")
148
149    def _configure(self):
150        """The adapter is ready for the breakpoints (the initialized event)."""
151        for path in list(self._breakpoints.lines):
152            self._send_breakpoints(path)
153        self.client.request("configurationDone", {}, self._configured)
154
155    def _configured(self, success: bool, _body: dict, message: str):
156        if not success:
157            self._end(f"the debugger couldn't start the program: {message}")
158        elif self.state == STARTING:
159            self._set_state(RUNNING)
160
161    # ---------------------------------------------------------- breakpoints
162    def set_breakpoints(self, path: str, lines: list[int]):
163        """A file's breakpoints now (sent to the debugger, if one is running)."""
164        self._breakpoints.lines[path] = sorted(set(lines))
165        if self.state in (RUNNING, STOPPED):
166            self._send_breakpoints(path)
167
168    def _send_breakpoints(self, path: str):
169        lines = self._breakpoints.lines.get(path, [])
170        self.client.request("setBreakpoints", {"source": {"path": path, "name": Path(path).name},
171                                               "breakpoints": [{"line": line} for line in lines]},
172                            lambda success, body, message, p=path, ls=lines: self._breakpoints_set(
173                                p, ls, success, body, message))
174
175    def _breakpoints_set(self, path: str, lines: list[int], success: bool, body: dict, message: str):
176        states: dict[int, BreakpointState] = {}
177        found = body.get("breakpoints") or [] if success else []
178        self._breakpoints.pending -= {(path, n) for n in self._breakpoints.lines.get(path, [])}
179        for line, answer in zip(lines, found, strict=False):
180            if isinstance(answer.get("id"), int):
181                self._breakpoints.ids[answer["id"]] = (path, line)
182            if not answer.get("verified") and answer.get("reason") == "pending":
183                self._breakpoints.pending.add((path, line))
184            states[line] = BreakpointState(_placed(answer), _why_not(answer))
185        for line in lines[len(found):]:
186            states[line] = BreakpointState(False, message or "the debugger didn't place it")
187        self.breakpoints_checked.emit(path, states)
188
189    def _breakpoint_event(self, body: dict):
190        answer = body.get("breakpoint") or {}
191        where = self._breakpoints.ids.get(answer.get("id"))
192        if where is not None and body.get("reason") in ("changed", "new"):
193            path, line = where
194            if answer.get("verified"):
195                self._breakpoints.pending.discard(where)
196            self.breakpoints_checked.emit(path, {line: BreakpointState(_placed(answer), _why_not(answer))})
197
198    # ---------------------------------------------------------- running
199    def _request_running(self, command: str, arguments: dict | None = None):
200        if self.state != STOPPED or self.thread_id is None:
201            return
202        self.frames = []
203        self._set_state(RUNNING)
204        self.client.request(command, {"threadId": self.thread_id, **(arguments or {})}, self._answered)
205
206    def _answered(self, success: bool, _body: dict, message: str):
207        if not success and message and self.state != IDLE:
208            self.output.emit("console", f"{message}\n")
209
210    def continue_(self):
211        self._request_running("continue")
212
213    def step_over(self):
214        self._request_running("next")
215
216    def step_in(self):
217        self._request_running("stepIn")
218
219    def step_out(self):
220        self._request_running("stepOut")
221
222    def pause(self):
223        if self.state == RUNNING:
224            self.client.request("pause", {"threadId": self.thread_id or 1}, self._answered)
225
226    def stop(self):
227        """End the session: the program and the debugger."""
228        if not self.is_active():
229            return
230        self.client.request("disconnect", {"terminateDebuggee": True}, timeout=1000)
231        self._end("stopped")
232
233    # ---------------------------------------------------------- events
234    def _event(self, name: str, body: dict):
235        if name == "initialized":
236            self._configure()
237        elif name == "stopped":
238            self.thread_id = body.get("threadId", self.thread_id)
239            # (Which breakpoints, as they were set: the debugger may have moved one to
240            # the next line with code, where the program stops.)
241            ids = self._breakpoints.ids
242            self.hit_breakpoints = [ids[n] for n in body.get("hitBreakpointIds") or [] if n in ids]
243            self._set_state(STOPPED)
244            self._unplaced()
245            why = str(body.get("reason") or "")
246            self.client.request("threads", {}, lambda ok, b, m: self._threads(ok, b, why))
247        elif name == "continued":
248            if self.state == STOPPED:
249                self.frames = []
250                self._set_state(RUNNING)
251        elif name == "output":
252            self._output(body)
253        elif name == "breakpoint":
254            self._breakpoint_event(body)
255        elif name == "exited":
256            self.exit_code = body.get("exitCode")
257        elif name == "terminated":
258            code = self.exit_code
259            self._end("the program ended" if code is None else f"the program exited with code {code}")
260
261    def _unplaced(self):
262        """Stopped, so the program is loaded: a breakpoint still "pending"
263        never will be placed (gdb leaves one on a line with no code so)."""
264        pending, self._breakpoints.pending = self._breakpoints.pending, set()
265        by_file: dict[str, dict[int, BreakpointState]] = {}
266        for path, line in pending:
267            by_file.setdefault(path, {})[line] = BreakpointState(False, "no code on that line: the debugger "
268                                                                        "couldn't place it")
269        for path, states in by_file.items():
270            self.breakpoints_checked.emit(path, states)
271
272    def _output(self, body: dict):
273        category, text = str(body.get("category") or "console"), str(body.get("output") or "")
274        if category == "telemetry" or not text:
275            return
276        if self.adapter is not None and self.adapter.kind == "gdb":
277            text = "".join(line for line in text.splitlines(keepends=True) if not _GDB_NOISE.match(line))
278        if text:
279            self.output.emit(category, text)
280
281    def _adapter_stopped(self, why: str):
282        if not self._ending:
283            self._end(why)
284
285    # ---------------------------------------------------------- where it stopped
286    def _threads(self, success: bool, body: dict, why: str):
287        threads = body.get("threads") or [] if success else []
288        if self.thread_id is None and threads:
289            self.thread_id = threads[0].get("id")
290        if self.thread_id is None:
291            return
292        self.client.request("stackTrace", {"threadId": self.thread_id, "levels": 100},
293                            lambda ok, b, m, w=why: self._stack(ok, b, w))
294
295    def _stack(self, success: bool, body: dict, why: str):
296        if self.state != STOPPED:
297            return
298        self.frames = [Frame(int(f.get("id", 0)), str(f.get("name") or "?"),
299                             (f.get("source") or {}).get("path"), int(f.get("line") or 0))
300                       for f in (body.get("stackFrames") or [] if success else [])]
301        # The first frame with source (not a library's): where the program is, for the user.
302        self.frame_index = next((i for i, f in enumerate(self.frames) if f.path), 0)
303        self.stopped_at.emit(self.frames, why)
304        self.select_frame(self.frame_index)
305
306    def select_frame(self, index: int):
307        """Show a frame of the call stack: its scopes (scopes_ready)."""
308        if self.state != STOPPED or not 0 <= index < len(self.frames):
309            return
310        self.frame_index = index
311        self.client.request("scopes", {"frameId": self.frames[index].id},
312                            lambda ok, b, m, i=index: self._scopes(i, ok, b))
313
314    def _scopes(self, index: int, success: bool, body: dict):
315        scopes = [Scope(str(s.get("name") or ""), int(s.get("variablesReference") or 0), bool(s.get("expensive")))
316                  for s in (body.get("scopes") or [] if success else [])
317                  # (A processor's registers: not what a Fortran programmer is after.)
318                  if s.get("presentationHint") != "registers" and str(s.get("name")).lower() != "registers"]
319        self.scopes_ready.emit(index, scopes)
320
321    def variables(self, reference: int, callback: Callable[[list[Variable], bool], None]):
322        """The variables of a scope, or the parts of one (an array's
323        elements, a type's components): ``callback(variables, more)``, more:
324        there were more than MAX_ELEMENTS."""
325        if self.state != STOPPED:
326            callback([], False)
327            return
328        self.client.request("variables", {"variablesReference": reference, "start": 0, "count": MAX_ELEMENTS},
329                            lambda ok, b, m: callback(*_variables(ok, b)))
330
331    def evaluate(self, expression: str, callback: Callable[[bool, str, int], None], context: str = "watch"):
332        """An expression's value in the frame shown: ``callback(ok, text, reference)``.
333        ("watch": an expression; gdb takes "repl" as one of its commands.)"""
334        frame = self.frames[self.frame_index].id if self.frames and self.state == STOPPED else None
335        arguments = {"expression": expression, "context": context}
336        if frame is not None:
337            arguments["frameId"] = frame
338        self.client.request("evaluate", arguments, lambda ok, b, m: callback(
339            ok, str(b.get("result", "")) if ok else (m or "can't evaluate that"), int(b.get("variablesReference") or 0)))

QObject(self, /, parent: PySide6.QtCore.QObject | None = None, *, objectName: str | None = None) -> None

DebugSession(parent=None)
82    def __init__(self, parent=None):
83        super().__init__(parent)
84        self.client = DapClient(self)
85        self.client.event.connect(self._event)
86        self.client.stopped.connect(self._adapter_stopped)
87        self.adapter: Adapter | None = None
88        self.state = IDLE
89        self.frames: list[Frame] = []
90        self.frame_index = 0
91        self.thread_id: int | None = None
92        self.exit_code: int | None = None
93        self.hit_breakpoints: list[tuple[str, int]] = []  # (file, line as set) the last stop was at
94        self._breakpoints = _Breakpoints()
95        self._launch: dict = {}
96        self._ending = False

__init__(self, /, parent: PySide6.QtCore.QObject | None = None, *, objectName: str | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

def state_changed(unknown):

The type of the None singleton.

def stopped_at(unknown):

The type of the None singleton.

def scopes_ready(unknown):

The type of the None singleton.

def output(unknown):

The type of the None singleton.

def breakpoints_checked(unknown):

The type of the None singleton.

def ended(unknown):

The type of the None singleton.

client
state
frames: list[Frame]
frame_index
thread_id: int | None
exit_code: int | None
hit_breakpoints: list[tuple[str, int]]
def is_active(self) -> bool:
 99    def is_active(self) -> bool:
100        return self.state != IDLE
def start( self, adapter: vibefpmgui.debug_adapters.Adapter, program: str, args: list[str], cwd: str, breakpoints: dict[str, list[int]]) -> bool:
118    def start(self, adapter: Adapter, program: str, args: list[str], cwd: str,
119              breakpoints: dict[str, list[int]]) -> bool:
120        """Run ``program`` under the debugger, stopping at ``breakpoints``
121        (file → lines)."""
122        if self.is_active():
123            self.stop()
124        self.adapter = adapter
125        self.exit_code = None
126        self.frames, self.thread_id = [], None
127        self._breakpoints = _Breakpoints({path: sorted(set(lines)) for path, lines in breakpoints.items() if lines})
128        self._launch = adapter.launch_arguments(program, args, cwd)
129        self._set_state(STARTING)
130        self.client.start(adapter.command, cwd)
131        self.client.request("initialize", {
132            "clientID": "vibefpmgui", "clientName": "VibeFPMGUI", "adapterID": adapter.kind,
133            "linesStartAt1": True, "columnsStartAt1": True, "pathFormat": "path",
134            "supportsVariableType": True, "supportsRunInTerminalRequest": False,
135        }, self._initialized)
136        return True

Run program under the debugger, stopping at breakpoints (file → lines).

def set_breakpoints(self, path: str, lines: list[int]):
162    def set_breakpoints(self, path: str, lines: list[int]):
163        """A file's breakpoints now (sent to the debugger, if one is running)."""
164        self._breakpoints.lines[path] = sorted(set(lines))
165        if self.state in (RUNNING, STOPPED):
166            self._send_breakpoints(path)

A file's breakpoints now (sent to the debugger, if one is running).

def continue_(self):
210    def continue_(self):
211        self._request_running("continue")
def step_over(self):
213    def step_over(self):
214        self._request_running("next")
def step_in(self):
216    def step_in(self):
217        self._request_running("stepIn")
def step_out(self):
219    def step_out(self):
220        self._request_running("stepOut")
def pause(self):
222    def pause(self):
223        if self.state == RUNNING:
224            self.client.request("pause", {"threadId": self.thread_id or 1}, self._answered)
def stop(self):
226    def stop(self):
227        """End the session: the program and the debugger."""
228        if not self.is_active():
229            return
230        self.client.request("disconnect", {"terminateDebuggee": True}, timeout=1000)
231        self._end("stopped")

End the session: the program and the debugger.

def select_frame(self, index: int):
306    def select_frame(self, index: int):
307        """Show a frame of the call stack: its scopes (scopes_ready)."""
308        if self.state != STOPPED or not 0 <= index < len(self.frames):
309            return
310        self.frame_index = index
311        self.client.request("scopes", {"frameId": self.frames[index].id},
312                            lambda ok, b, m, i=index: self._scopes(i, ok, b))

Show a frame of the call stack: its scopes (scopes_ready).

def variables( self, reference: int, callback: Callable[[list[Variable], bool], None]):
321    def variables(self, reference: int, callback: Callable[[list[Variable], bool], None]):
322        """The variables of a scope, or the parts of one (an array's
323        elements, a type's components): ``callback(variables, more)``, more:
324        there were more than MAX_ELEMENTS."""
325        if self.state != STOPPED:
326            callback([], False)
327            return
328        self.client.request("variables", {"variablesReference": reference, "start": 0, "count": MAX_ELEMENTS},
329                            lambda ok, b, m: callback(*_variables(ok, b)))

The variables of a scope, or the parts of one (an array's elements, a type's components): callback(variables, more), more: there were more than MAX_ELEMENTS.

def evaluate( self, expression: str, callback: Callable[[bool, str, int], None], context: str = 'watch'):
331    def evaluate(self, expression: str, callback: Callable[[bool, str, int], None], context: str = "watch"):
332        """An expression's value in the frame shown: ``callback(ok, text, reference)``.
333        ("watch": an expression; gdb takes "repl" as one of its commands.)"""
334        frame = self.frames[self.frame_index].id if self.frames and self.state == STOPPED else None
335        arguments = {"expression": expression, "context": context}
336        if frame is not None:
337            arguments["frameId"] = frame
338        self.client.request("evaluate", arguments, lambda ok, b, m: callback(
339            ok, str(b.get("result", "")) if ok else (m or "can't evaluate that"), int(b.get("variablesReference") or 0)))

An expression's value in the frame shown: callback(ok, text, reference). ("watch": an expression; gdb takes "repl" as one of its commands.)

staticMetaObject = PySide6.QtCore.QMetaObject("DebugSession" inherits "QObject": Methods: #4 type=Signal, signature=state_changed(QString), parameters=QString #5 type=Signal, signature=stopped_at(PyObject,QString), parameters=PyObject, QString #6 type=Signal, signature=scopes_ready(int,PyObject), parameters=int, PyObject #7 type=Signal, signature=output(QString,QString), parameters=QString, QString #8 type=Signal, signature=breakpoints_checked(QString,PyObject), parameters=QString, PyObject #9 type=Signal, signature=ended(QString), parameters=QString )