]>
Commit | Line | Data |
---|---|---|
1d506c26 | 1 | # Copyright 2022-2024 Free Software Foundation, Inc. |
de7d7cb5 TT |
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 | # Do not import other gdbdap modules here -- this module must come | |
17 | # first. | |
18 | import functools | |
19 | import gdb | |
20 | import queue | |
de7d7cb5 TT |
21 | import threading |
22 | import traceback | |
954a1f91 | 23 | import sys |
de7d7cb5 | 24 | |
dfc4bd46 | 25 | from enum import IntEnum, auto |
de7d7cb5 | 26 | |
587a1031 TT |
27 | # Adapt to different Queue types. This is exported for use in other |
28 | # modules as well. | |
29 | if sys.version_info[0] == 3 and sys.version_info[1] <= 6: | |
30 | DAPQueue = queue.Queue | |
31 | else: | |
32 | DAPQueue = queue.SimpleQueue | |
33 | ||
34 | ||
de7d7cb5 TT |
35 | # The GDB thread, aka the main thread. |
36 | _gdb_thread = threading.current_thread() | |
37 | ||
38 | ||
39 | # The DAP thread. | |
40 | _dap_thread = None | |
41 | ||
42 | ||
2a89c950 TT |
43 | # "Known" exceptions are wrapped in a DAP exception, so that, by |
44 | # default, only rogue exceptions are logged -- this is then used by | |
45 | # the test suite. | |
46 | class DAPException(Exception): | |
47 | pass | |
48 | ||
49 | ||
50 | # Wrapper for gdb.parse_and_eval that turns exceptions into | |
51 | # DAPException. | |
52 | def parse_and_eval(expression, global_context=False): | |
53 | try: | |
54 | return gdb.parse_and_eval(expression, global_context=global_context) | |
55 | except Exception as e: | |
56 | # Be sure to preserve the summary, as this can propagate to | |
57 | # the client. | |
58 | raise DAPException(str(e)) from e | |
59 | ||
60 | ||
de7d7cb5 TT |
61 | def start_thread(name, target, args=()): |
62 | """Start a new thread, invoking TARGET with *ARGS there. | |
63 | This is a helper function that ensures that any GDB signals are | |
64 | correctly blocked.""" | |
338b21b0 | 65 | result = gdb.Thread(name=name, target=target, args=args, daemon=True) |
560c121c | 66 | result.start() |
de7d7cb5 TT |
67 | |
68 | ||
69 | def start_dap(target): | |
70 | """Start the DAP thread and invoke TARGET there.""" | |
de7d7cb5 | 71 | exec_and_log("set breakpoint pending on") |
44606912 TT |
72 | |
73 | # Functions in this thread contain assertions that check for this | |
74 | # global, so we must set it before letting these functions run. | |
75 | def really_start_dap(): | |
76 | global _dap_thread | |
77 | _dap_thread = threading.current_thread() | |
78 | target() | |
79 | ||
80 | start_thread("DAP", really_start_dap) | |
de7d7cb5 TT |
81 | |
82 | ||
83 | def in_gdb_thread(func): | |
84 | """A decorator that asserts that FUNC must be run in the GDB thread.""" | |
85 | ||
86 | @functools.wraps(func) | |
87 | def ensure_gdb_thread(*args, **kwargs): | |
88 | assert threading.current_thread() is _gdb_thread | |
89 | return func(*args, **kwargs) | |
90 | ||
91 | return ensure_gdb_thread | |
92 | ||
93 | ||
94 | def in_dap_thread(func): | |
95 | """A decorator that asserts that FUNC must be run in the DAP thread.""" | |
96 | ||
97 | @functools.wraps(func) | |
98 | def ensure_dap_thread(*args, **kwargs): | |
99 | assert threading.current_thread() is _dap_thread | |
100 | return func(*args, **kwargs) | |
101 | ||
102 | return ensure_dap_thread | |
103 | ||
104 | ||
dfc4bd46 TT |
105 | # Logging levels. |
106 | class LogLevel(IntEnum): | |
107 | DEFAULT = auto() | |
108 | FULL = auto() | |
109 | ||
110 | ||
111 | class LogLevelParam(gdb.Parameter): | |
112 | """DAP logging level.""" | |
113 | ||
114 | set_doc = "Set the DAP logging level." | |
115 | show_doc = "Show the DAP logging level." | |
116 | ||
117 | def __init__(self): | |
118 | super().__init__( | |
119 | "debug dap-log-level", gdb.COMMAND_MAINTENANCE, gdb.PARAM_ZUINTEGER | |
120 | ) | |
121 | self.value = LogLevel.DEFAULT | |
122 | ||
123 | ||
124 | _log_level = LogLevelParam() | |
125 | ||
126 | ||
de7d7cb5 TT |
127 | class LoggingParam(gdb.Parameter): |
128 | """Whether DAP logging is enabled.""" | |
129 | ||
130 | set_doc = "Set the DAP logging status." | |
131 | show_doc = "Show the DAP logging status." | |
132 | ||
133 | log_file = None | |
134 | ||
135 | def __init__(self): | |
136 | super().__init__( | |
137 | "debug dap-log-file", gdb.COMMAND_MAINTENANCE, gdb.PARAM_OPTIONAL_FILENAME | |
138 | ) | |
139 | self.value = None | |
140 | ||
141 | def get_set_string(self): | |
142 | # Close any existing log file, no matter what. | |
143 | if self.log_file is not None: | |
144 | self.log_file.close() | |
145 | self.log_file = None | |
146 | if self.value is not None: | |
147 | self.log_file = open(self.value, "w") | |
148 | return "" | |
149 | ||
150 | ||
151 | dap_log = LoggingParam() | |
152 | ||
153 | ||
dfc4bd46 | 154 | def log(something, level=LogLevel.DEFAULT): |
de7d7cb5 | 155 | """Log SOMETHING to the log file, if logging is enabled.""" |
dfc4bd46 | 156 | if dap_log.log_file is not None and level <= _log_level.value: |
de7d7cb5 TT |
157 | print(something, file=dap_log.log_file) |
158 | dap_log.log_file.flush() | |
159 | ||
160 | ||
dfc4bd46 | 161 | def log_stack(level=LogLevel.DEFAULT): |
de7d7cb5 | 162 | """Log a stack trace to the log file, if logging is enabled.""" |
dfc4bd46 | 163 | if dap_log.log_file is not None and level <= _log_level.value: |
de7d7cb5 TT |
164 | traceback.print_exc(file=dap_log.log_file) |
165 | ||
166 | ||
167 | @in_gdb_thread | |
168 | def exec_and_log(cmd): | |
169 | """Execute the gdb command CMD. | |
170 | If logging is enabled, log the command and its output.""" | |
171 | log("+++ " + cmd) | |
172 | try: | |
173 | output = gdb.execute(cmd, from_tty=True, to_string=True) | |
174 | if output != "": | |
175 | log(">>> " + output) | |
176 | except gdb.error: | |
177 | log_stack() | |
178 | ||
179 | ||
180 | class Invoker(object): | |
181 | """A simple class that can invoke a gdb command.""" | |
182 | ||
183 | def __init__(self, cmd): | |
184 | self.cmd = cmd | |
185 | ||
186 | # This is invoked in the gdb thread to run the command. | |
187 | @in_gdb_thread | |
188 | def __call__(self): | |
189 | exec_and_log(self.cmd) | |
190 | ||
191 | ||
192 | def send_gdb(cmd): | |
193 | """Send CMD to the gdb thread. | |
194 | CMD can be either a function or a string. | |
195 | If it is a string, it is passed to gdb.execute.""" | |
196 | if isinstance(cmd, str): | |
197 | cmd = Invoker(cmd) | |
198 | gdb.post_event(cmd) | |
199 | ||
200 | ||
201 | def send_gdb_with_response(fn): | |
202 | """Send FN to the gdb thread and return its result. | |
203 | If FN is a string, it is passed to gdb.execute and None is | |
204 | returned as the result. | |
205 | If FN throws an exception, this function will throw the | |
206 | same exception in the calling thread. | |
207 | """ | |
208 | if isinstance(fn, str): | |
209 | fn = Invoker(fn) | |
587a1031 | 210 | result_q = DAPQueue() |
de7d7cb5 TT |
211 | |
212 | def message(): | |
213 | try: | |
214 | val = fn() | |
215 | result_q.put(val) | |
c0a652c2 | 216 | except (Exception, KeyboardInterrupt) as e: |
de7d7cb5 TT |
217 | result_q.put(e) |
218 | ||
219 | send_gdb(message) | |
220 | val = result_q.get() | |
c0a652c2 | 221 | if isinstance(val, (Exception, KeyboardInterrupt)): |
de7d7cb5 TT |
222 | raise val |
223 | return val |