]>
Commit | Line | Data |
---|---|---|
1 | # Copyright 2022-2025 Free Software Foundation, Inc. | |
2 | ||
3 | # This program is free software; you can redistribute it and/or modify | |
4 | # it under the terms of the GNU General Public License as published by | |
5 | # the Free Software Foundation; either version 3 of the License, or | |
6 | # (at your option) any later version. | |
7 | # | |
8 | # This program is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License | |
14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | import gdb | |
17 | ||
18 | from .modules import is_module, make_module | |
19 | from .scopes import set_finish_value | |
20 | from .server import send_event | |
21 | from .startup import exec_and_log, in_gdb_thread, log | |
22 | ||
23 | # True when the inferior is thought to be running, False otherwise. | |
24 | # This may be accessed from any thread, which can be racy. However, | |
25 | # this is unimportant because this global is only used for the | |
26 | # 'notStopped' response, which itself is inherently racy. | |
27 | inferior_running = False | |
28 | ||
29 | ||
30 | @in_gdb_thread | |
31 | def _on_exit(event): | |
32 | global inferior_running | |
33 | inferior_running = False | |
34 | code = 0 | |
35 | if hasattr(event, "exit_code"): | |
36 | code = event.exit_code | |
37 | send_event( | |
38 | "exited", | |
39 | { | |
40 | "exitCode": code, | |
41 | }, | |
42 | ) | |
43 | send_event("terminated") | |
44 | ||
45 | ||
46 | # When None, a "process" event has already been sent. When a string, | |
47 | # it is the "startMethod" for that event. | |
48 | _process_event_kind = None | |
49 | ||
50 | ||
51 | @in_gdb_thread | |
52 | def send_process_event_once(): | |
53 | global _process_event_kind | |
54 | if _process_event_kind is not None: | |
55 | inf = gdb.selected_inferior() | |
56 | is_local = inf.connection.type == "native" | |
57 | data = { | |
58 | "isLocalProcess": is_local, | |
59 | "startMethod": _process_event_kind, | |
60 | # Could emit 'pointerSize' here too if we cared to. | |
61 | } | |
62 | if inf.progspace.filename: | |
63 | data["name"] = inf.progspace.filename | |
64 | if is_local: | |
65 | data["systemProcessId"] = inf.pid | |
66 | send_event("process", data) | |
67 | _process_event_kind = None | |
68 | ||
69 | ||
70 | @in_gdb_thread | |
71 | def expect_process(reason): | |
72 | """Indicate that DAP is starting or attaching to a process. | |
73 | ||
74 | REASON is the "startMethod" to include in the "process" event. | |
75 | """ | |
76 | global _process_event_kind | |
77 | _process_event_kind = reason | |
78 | ||
79 | ||
80 | @in_gdb_thread | |
81 | def thread_event(event, reason): | |
82 | send_process_event_once() | |
83 | send_event( | |
84 | "thread", | |
85 | { | |
86 | "reason": reason, | |
87 | "threadId": event.inferior_thread.global_num, | |
88 | }, | |
89 | ) | |
90 | ||
91 | ||
92 | @in_gdb_thread | |
93 | def _new_thread(event): | |
94 | global inferior_running | |
95 | inferior_running = True | |
96 | thread_event(event, "started") | |
97 | ||
98 | ||
99 | @in_gdb_thread | |
100 | def _thread_exited(event): | |
101 | thread_event(event, "exited") | |
102 | ||
103 | ||
104 | @in_gdb_thread | |
105 | def _new_objfile(event): | |
106 | if is_module(event.new_objfile): | |
107 | send_event( | |
108 | "module", | |
109 | { | |
110 | "reason": "new", | |
111 | "module": make_module(event.new_objfile), | |
112 | }, | |
113 | ) | |
114 | ||
115 | ||
116 | @in_gdb_thread | |
117 | def _objfile_removed(event): | |
118 | send_process_event_once() | |
119 | if is_module(event.objfile): | |
120 | send_event( | |
121 | "module", | |
122 | { | |
123 | "reason": "removed", | |
124 | "module": make_module(event.objfile), | |
125 | }, | |
126 | ) | |
127 | ||
128 | ||
129 | _suppress_cont = False | |
130 | ||
131 | ||
132 | @in_gdb_thread | |
133 | def _cont(event): | |
134 | global inferior_running | |
135 | inferior_running = True | |
136 | global _suppress_cont | |
137 | if _suppress_cont: | |
138 | log("_suppress_cont case") | |
139 | _suppress_cont = False | |
140 | else: | |
141 | send_event( | |
142 | "continued", | |
143 | { | |
144 | "threadId": gdb.selected_thread().global_num, | |
145 | "allThreadsContinued": True, | |
146 | }, | |
147 | ) | |
148 | ||
149 | ||
150 | _expected_stop_reason = None | |
151 | ||
152 | ||
153 | @in_gdb_thread | |
154 | def expect_stop(reason: str): | |
155 | """Indicate that the next stop should be for REASON.""" | |
156 | global _expected_stop_reason | |
157 | _expected_stop_reason = reason | |
158 | ||
159 | ||
160 | _expected_pause = False | |
161 | ||
162 | ||
163 | @in_gdb_thread | |
164 | def exec_and_expect_stop(cmd, expected_pause=False, propagate_exception=False): | |
165 | """A wrapper for exec_and_log that sets the continue-suppression flag. | |
166 | ||
167 | When EXPECTED_PAUSE is True, a stop that looks like a pause (e.g., | |
168 | a SIGINT) will be reported as "pause" instead. | |
169 | """ | |
170 | global _expected_pause | |
171 | _expected_pause = expected_pause | |
172 | global _suppress_cont | |
173 | # If we're expecting a pause, then we're definitely not | |
174 | # continuing. | |
175 | _suppress_cont = not expected_pause | |
176 | # FIXME if the call fails should we clear _suppress_cont? | |
177 | exec_and_log(cmd, propagate_exception) | |
178 | ||
179 | ||
180 | # Map from gdb stop reasons to DAP stop reasons. Some of these can't | |
181 | # be seen ordinarily in DAP -- only if the client lets the user toggle | |
182 | # some settings (e.g. stop-on-solib-events) or enter commands (e.g., | |
183 | # 'until'). | |
184 | stop_reason_map = { | |
185 | "breakpoint-hit": "breakpoint", | |
186 | "watchpoint-trigger": "data breakpoint", | |
187 | "read-watchpoint-trigger": "data breakpoint", | |
188 | "access-watchpoint-trigger": "data breakpoint", | |
189 | "function-finished": "step", | |
190 | "location-reached": "step", | |
191 | "watchpoint-scope": "data breakpoint", | |
192 | "end-stepping-range": "step", | |
193 | "exited-signalled": "exited", | |
194 | "exited": "exited", | |
195 | "exited-normally": "exited", | |
196 | "signal-received": "signal", | |
197 | "solib-event": "solib", | |
198 | "fork": "fork", | |
199 | "vfork": "vfork", | |
200 | "syscall-entry": "syscall-entry", | |
201 | "syscall-return": "syscall-return", | |
202 | "exec": "exec", | |
203 | "no-history": "no-history", | |
204 | } | |
205 | ||
206 | ||
207 | @in_gdb_thread | |
208 | def _on_stop(event): | |
209 | global inferior_running | |
210 | inferior_running = False | |
211 | ||
212 | log("entering _on_stop: " + repr(event)) | |
213 | if hasattr(event, "details"): | |
214 | log(" details: " + repr(event.details)) | |
215 | obj = { | |
216 | "threadId": gdb.selected_thread().global_num, | |
217 | "allThreadsStopped": True, | |
218 | } | |
219 | if isinstance(event, gdb.BreakpointEvent): | |
220 | obj["hitBreakpointIds"] = [x.number for x in event.breakpoints] | |
221 | if hasattr(event, "details") and "finish-value" in event.details: | |
222 | set_finish_value(event.details["finish-value"]) | |
223 | ||
224 | global _expected_pause | |
225 | global _expected_stop_reason | |
226 | if _expected_stop_reason is not None: | |
227 | obj["reason"] = _expected_stop_reason | |
228 | _expected_stop_reason = None | |
229 | elif "reason" not in event.details: | |
230 | # This can only really happen via a "repl" evaluation of | |
231 | # something like "attach". In this case just emit a generic | |
232 | # stop. | |
233 | obj["reason"] = "stopped" | |
234 | elif ( | |
235 | _expected_pause | |
236 | and event.details["reason"] == "signal-received" | |
237 | and event.details["signal-name"] in ("SIGINT", "SIGSTOP") | |
238 | ): | |
239 | obj["reason"] = "pause" | |
240 | else: | |
241 | obj["reason"] = stop_reason_map[event.details["reason"]] | |
242 | _expected_pause = False | |
243 | send_event("stopped", obj) | |
244 | ||
245 | ||
246 | # This keeps a bit of state between the start of an inferior call and | |
247 | # the end. If the inferior was already running when the call started | |
248 | # (as can happen if a breakpoint condition calls a function), then we | |
249 | # do not want to emit 'continued' or 'stop' events for the call. Note | |
250 | # that, for some reason, gdb.events.cont does not fire for an infcall. | |
251 | _infcall_was_running = False | |
252 | ||
253 | ||
254 | @in_gdb_thread | |
255 | def _on_inferior_call(event): | |
256 | global _infcall_was_running | |
257 | global inferior_running | |
258 | if isinstance(event, gdb.InferiorCallPreEvent): | |
259 | _infcall_was_running = inferior_running | |
260 | if not _infcall_was_running: | |
261 | _cont(None) | |
262 | else: | |
263 | # If the inferior is already marked as stopped here, then that | |
264 | # means that the call caused some other stop, and we don't | |
265 | # want to double-report it. | |
266 | if not _infcall_was_running and inferior_running: | |
267 | inferior_running = False | |
268 | obj = { | |
269 | "threadId": gdb.selected_thread().global_num, | |
270 | "allThreadsStopped": True, | |
271 | # DAP says any string is ok. | |
272 | "reason": "function call", | |
273 | } | |
274 | global _expected_pause | |
275 | _expected_pause = False | |
276 | send_event("stopped", obj) | |
277 | ||
278 | ||
279 | gdb.events.stop.connect(_on_stop) | |
280 | gdb.events.exited.connect(_on_exit) | |
281 | gdb.events.new_thread.connect(_new_thread) | |
282 | gdb.events.thread_exited.connect(_thread_exited) | |
283 | gdb.events.cont.connect(_cont) | |
284 | gdb.events.new_objfile.connect(_new_objfile) | |
285 | gdb.events.free_objfile.connect(_objfile_removed) | |
286 | gdb.events.inferior_call.connect(_on_inferior_call) |