bash-dap 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bash_dap/__init__.py +3 -0
- bash_dap/__main__.py +5 -0
- bash_dap/adapter.py +537 -0
- bash_dap/harness.sh +206 -0
- bash_dap-0.1.dist-info/METADATA +88 -0
- bash_dap-0.1.dist-info/RECORD +10 -0
- bash_dap-0.1.dist-info/WHEEL +5 -0
- bash_dap-0.1.dist-info/entry_points.txt +2 -0
- bash_dap-0.1.dist-info/licenses/LICENSE +641 -0
- bash_dap-0.1.dist-info/top_level.txt +1 -0
bash_dap/__init__.py
ADDED
bash_dap/__main__.py
ADDED
bash_dap/adapter.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""bash-dap: a debug adapter of the Debug Adapter Protocol for bash scripts.
|
|
2
|
+
|
|
3
|
+
The adapter speaks DAP on its stdin and stdout. It runs the script under harness.sh, a debugger
|
|
4
|
+
of bash written in bash with the trap DEBUG, and talks to it through two fifos and a few files
|
|
5
|
+
of a directory of its own (see harness.sh). Nothing but bash and Python is needed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import signal
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
HARNESS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "harness.sh")
|
|
19
|
+
VARIABLES_REF = 1 # the reference of the variables of a stop
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def unquote(text):
|
|
23
|
+
"""The string that printf %q of bash has quoted: \\x, '...' and $'...' (C escapes)."""
|
|
24
|
+
out = []
|
|
25
|
+
i, n = 0, len(text)
|
|
26
|
+
escapes = {"n": "\n", "t": "\t", "r": "\r", "a": "\a", "b": "\b", "e": "\x1b", "E": "\x1b",
|
|
27
|
+
"f": "\f", "v": "\v", "\\": "\\", "'": "'", '"': '"', "?": "?"}
|
|
28
|
+
while i < n:
|
|
29
|
+
c = text[i]
|
|
30
|
+
if c == "\\" and i + 1 < n:
|
|
31
|
+
out.append(text[i + 1])
|
|
32
|
+
i += 2
|
|
33
|
+
elif c == "'":
|
|
34
|
+
j = text.find("'", i + 1)
|
|
35
|
+
j = n if j < 0 else j
|
|
36
|
+
out.append(text[i + 1:j])
|
|
37
|
+
i = j + 1
|
|
38
|
+
elif c == "$" and text.startswith("$'", i):
|
|
39
|
+
i += 2
|
|
40
|
+
while i < n and text[i] != "'":
|
|
41
|
+
if text[i] == "\\" and i + 1 < n:
|
|
42
|
+
e = text[i + 1]
|
|
43
|
+
if e in escapes:
|
|
44
|
+
out.append(escapes[e])
|
|
45
|
+
i += 2
|
|
46
|
+
elif e == "x":
|
|
47
|
+
m = re.match(r"[0-9a-fA-F]{1,2}", text[i + 2:])
|
|
48
|
+
out.append(chr(int(m.group(0), 16)) if m else "x")
|
|
49
|
+
i += 2 + (len(m.group(0)) if m else 0)
|
|
50
|
+
elif e in "01234567":
|
|
51
|
+
m = re.match(r"[0-7]{1,3}", text[i + 1:])
|
|
52
|
+
out.append(chr(int(m.group(0), 8)))
|
|
53
|
+
i += 1 + len(m.group(0))
|
|
54
|
+
else:
|
|
55
|
+
out.append(e)
|
|
56
|
+
i += 2
|
|
57
|
+
else:
|
|
58
|
+
out.append(text[i])
|
|
59
|
+
i += 1
|
|
60
|
+
i += 1
|
|
61
|
+
else:
|
|
62
|
+
out.append(c)
|
|
63
|
+
i += 1
|
|
64
|
+
return "".join(out)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def unquote_double(text):
|
|
68
|
+
"""A word of bash in double quotes, as declare -p writes a value: "a \\"b\\" \\$c"."""
|
|
69
|
+
if len(text) >= 2 and text[0] == '"' and text[-1] == '"':
|
|
70
|
+
return re.sub(r'\\(["\\$`])', r"\1", text[1:-1])
|
|
71
|
+
if text.startswith("$'"):
|
|
72
|
+
return unquote(text)
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_declare(text):
|
|
77
|
+
"""What declare -p says of a variable: its flags, and a value or the elements of an array
|
|
78
|
+
as (key, value) pairs; None for something else, an error say."""
|
|
79
|
+
m = re.match(r"declare -(\S+) ([^=\s]+)(?:=(.*))?$", text, re.S)
|
|
80
|
+
if not m:
|
|
81
|
+
return None
|
|
82
|
+
flags, value = m.group(1), m.group(3)
|
|
83
|
+
if value is None:
|
|
84
|
+
return flags, "", None
|
|
85
|
+
if ("a" in flags or "A" in flags) and value.startswith("("):
|
|
86
|
+
pairs = re.findall(r'\[((?:[^\]\\]|\\.)*)\]=("(?:[^"\\]|\\.)*"|\$\'(?:[^\'\\]|\\.)*\'|[^\s)]*)',
|
|
87
|
+
value[1:])
|
|
88
|
+
return flags, value, [(unquote_double(k) if k.startswith('"') else k, unquote_double(v))
|
|
89
|
+
for k, v in pairs]
|
|
90
|
+
return flags, unquote_double(value), None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Adapter:
|
|
94
|
+
def __init__(self, rfile, wfile):
|
|
95
|
+
self.rfile = rfile
|
|
96
|
+
self.wfile = wfile
|
|
97
|
+
self.write_lock = threading.Lock()
|
|
98
|
+
self.seq = 0
|
|
99
|
+
self.client = {}
|
|
100
|
+
self.dir = None
|
|
101
|
+
self.proc = None
|
|
102
|
+
self.pid = None # of the script run in the terminal of the client
|
|
103
|
+
self.commands = None
|
|
104
|
+
self.cv = threading.Condition()
|
|
105
|
+
self.collect = []
|
|
106
|
+
self.done = False
|
|
107
|
+
self.stopped = False
|
|
108
|
+
self.alive = False
|
|
109
|
+
self.pause_asked = False
|
|
110
|
+
self.breaks = {}
|
|
111
|
+
self.gen = 0
|
|
112
|
+
self.refs = {}
|
|
113
|
+
self.next_ref = VARIABLES_REF + 1
|
|
114
|
+
self.callbacks = {}
|
|
115
|
+
self.stop_on_entry = False
|
|
116
|
+
self.configured = False
|
|
117
|
+
self.entered = False
|
|
118
|
+
|
|
119
|
+
# --- the protocol ------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def send(self, message):
|
|
122
|
+
with self.write_lock:
|
|
123
|
+
self.seq += 1
|
|
124
|
+
message["seq"] = self.seq
|
|
125
|
+
body = json.dumps(message).encode()
|
|
126
|
+
self.wfile.write(b"Content-Length: %d\r\n\r\n" % len(body) + body)
|
|
127
|
+
self.wfile.flush()
|
|
128
|
+
return self.seq
|
|
129
|
+
|
|
130
|
+
def event(self, name, body=None):
|
|
131
|
+
message = {"type": "event", "event": name}
|
|
132
|
+
if body is not None:
|
|
133
|
+
message["body"] = body
|
|
134
|
+
self.send(message)
|
|
135
|
+
|
|
136
|
+
def respond(self, request, body=None, success=True, message=None):
|
|
137
|
+
response = {"type": "response", "request_seq": request["seq"],
|
|
138
|
+
"command": request["command"], "success": success}
|
|
139
|
+
if body is not None:
|
|
140
|
+
response["body"] = body
|
|
141
|
+
if message is not None:
|
|
142
|
+
response["message"] = message
|
|
143
|
+
self.send(response)
|
|
144
|
+
|
|
145
|
+
def output(self, text, category="console"):
|
|
146
|
+
self.event("output", {"category": category, "output": text})
|
|
147
|
+
|
|
148
|
+
def read_message(self):
|
|
149
|
+
length = None
|
|
150
|
+
while True:
|
|
151
|
+
line = self.rfile.readline()
|
|
152
|
+
if not line:
|
|
153
|
+
return None
|
|
154
|
+
line = line.strip()
|
|
155
|
+
if not line:
|
|
156
|
+
if length is not None:
|
|
157
|
+
break
|
|
158
|
+
continue
|
|
159
|
+
name, _, value = line.partition(b":")
|
|
160
|
+
if name.strip().lower() == b"content-length":
|
|
161
|
+
length = int(value)
|
|
162
|
+
return json.loads(self.rfile.read(length))
|
|
163
|
+
|
|
164
|
+
# --- the harness -------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
def tell(self, command):
|
|
167
|
+
os.write(self.commands, (command + "\n").encode())
|
|
168
|
+
|
|
169
|
+
def ask(self, command, timeout=10):
|
|
170
|
+
"""A command of the harness while the script is stopped: the lines of its reply."""
|
|
171
|
+
if not self.stopped or self.commands is None:
|
|
172
|
+
return None
|
|
173
|
+
with self.cv:
|
|
174
|
+
self.collect = []
|
|
175
|
+
self.done = False
|
|
176
|
+
self.tell(command)
|
|
177
|
+
end = time.monotonic() + timeout
|
|
178
|
+
while not self.done and self.alive and time.monotonic() < end:
|
|
179
|
+
self.cv.wait(0.2)
|
|
180
|
+
return list(self.collect) if self.done else None
|
|
181
|
+
|
|
182
|
+
def resume(self, command):
|
|
183
|
+
if not self.stopped:
|
|
184
|
+
return False
|
|
185
|
+
self.stopped = False
|
|
186
|
+
self.refs = {}
|
|
187
|
+
self.tell(command)
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
def save_breaks(self):
|
|
191
|
+
"""The breakpoints for the harness: the processes of the script take them at their next
|
|
192
|
+
command, whether the script runs or is stopped."""
|
|
193
|
+
if self.dir is None:
|
|
194
|
+
return
|
|
195
|
+
with open(os.path.join(self.dir, "breaks"), "w") as f:
|
|
196
|
+
for path, lines in self.breaks.items():
|
|
197
|
+
for line in lines:
|
|
198
|
+
f.write("%s:%d\n" % (path, line))
|
|
199
|
+
self.gen += 1
|
|
200
|
+
open(os.path.join(self.dir, "gen.%d" % self.gen), "w").close()
|
|
201
|
+
|
|
202
|
+
def open_fifos(self):
|
|
203
|
+
"""The fifos once the harness has opened its ends; False when it never does."""
|
|
204
|
+
path = os.path.join(self.dir, "commands")
|
|
205
|
+
end = time.monotonic() + 15
|
|
206
|
+
while True:
|
|
207
|
+
try:
|
|
208
|
+
fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
|
|
209
|
+
break
|
|
210
|
+
except OSError:
|
|
211
|
+
if time.monotonic() > end or (self.proc is not None and self.proc.poll() is not None):
|
|
212
|
+
return None
|
|
213
|
+
time.sleep(0.02)
|
|
214
|
+
os.set_blocking(fd, True)
|
|
215
|
+
self.commands = fd
|
|
216
|
+
return open(os.path.join(self.dir, "replies"), "r")
|
|
217
|
+
|
|
218
|
+
def harness_loop(self):
|
|
219
|
+
replies = self.open_fifos()
|
|
220
|
+
if replies is None:
|
|
221
|
+
self.output("bash-dap: the script did not start\n", "stderr")
|
|
222
|
+
self.alive = False
|
|
223
|
+
self.event("terminated")
|
|
224
|
+
return
|
|
225
|
+
self.alive = True
|
|
226
|
+
for line in replies:
|
|
227
|
+
line = line.rstrip("\n")
|
|
228
|
+
kind, _, rest = line.partition(" ")
|
|
229
|
+
if kind == "stopped":
|
|
230
|
+
self.on_stopped(rest)
|
|
231
|
+
elif kind == "exited":
|
|
232
|
+
self.alive = False
|
|
233
|
+
self.event("exited", {"exitCode": int(rest or 0)})
|
|
234
|
+
break
|
|
235
|
+
elif kind == "done":
|
|
236
|
+
with self.cv:
|
|
237
|
+
self.done = True
|
|
238
|
+
self.cv.notify_all()
|
|
239
|
+
else:
|
|
240
|
+
with self.cv:
|
|
241
|
+
self.collect.append((kind, rest))
|
|
242
|
+
self.alive = False
|
|
243
|
+
with self.cv:
|
|
244
|
+
self.cv.notify_all()
|
|
245
|
+
self.event("terminated")
|
|
246
|
+
|
|
247
|
+
def on_stopped(self, rest):
|
|
248
|
+
reason, line, func, path = (rest.split(" ", 3) + ["", "", "", ""])[:4]
|
|
249
|
+
self.stopped = True
|
|
250
|
+
self.refs = {}
|
|
251
|
+
if reason == "entry":
|
|
252
|
+
# the breakpoints, then configurationDone runs it
|
|
253
|
+
self.entered = True
|
|
254
|
+
self.event("initialized")
|
|
255
|
+
return
|
|
256
|
+
if self.pause_asked and reason == "step":
|
|
257
|
+
reason = "pause"
|
|
258
|
+
self.pause_asked = False
|
|
259
|
+
self.event("stopped", {"reason": reason, "threadId": 1, "allThreadsStopped": True})
|
|
260
|
+
|
|
261
|
+
# --- the variables -----------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def variable(self, name, text, evaluate_name):
|
|
264
|
+
parsed = parse_declare(text)
|
|
265
|
+
if parsed is None:
|
|
266
|
+
return {"name": name, "value": text, "variablesReference": 0}
|
|
267
|
+
flags, value, elements = parsed
|
|
268
|
+
item = {"name": name, "value": value, "evaluateName": evaluate_name,
|
|
269
|
+
"type": "array" if "a" in flags else "associative array" if "A" in flags
|
|
270
|
+
else "integer" if "i" in flags else "string", "variablesReference": 0}
|
|
271
|
+
if elements is not None:
|
|
272
|
+
ref = self.next_ref
|
|
273
|
+
self.next_ref += 1
|
|
274
|
+
assoc = "A" in flags
|
|
275
|
+
self.refs[ref] = [
|
|
276
|
+
{"name": "[%s]" % k, "value": v, "variablesReference": 0,
|
|
277
|
+
"evaluateName": '"${%s[%s]}"' % (name, ('"%s"' % k) if assoc else k)}
|
|
278
|
+
for k, v in elements]
|
|
279
|
+
item["value"] = "(%s)" % " ".join(v for _, v in elements)
|
|
280
|
+
item["variablesReference"] = ref
|
|
281
|
+
item["indexedVariables" if not assoc else "namedVariables"] = len(elements)
|
|
282
|
+
return item
|
|
283
|
+
|
|
284
|
+
# --- the requests ------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def on_initialize(self, request):
|
|
287
|
+
self.client = request.get("arguments", {})
|
|
288
|
+
self.respond(request, {
|
|
289
|
+
"supportsConfigurationDoneRequest": True,
|
|
290
|
+
"supportsEvaluateForHovers": True,
|
|
291
|
+
"supportsTerminateRequest": True,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
def on_launch(self, request):
|
|
295
|
+
args = request.get("arguments", {})
|
|
296
|
+
cwd = os.path.realpath(args.get("cwd") or os.getcwd())
|
|
297
|
+
program = args.get("program")
|
|
298
|
+
if not program:
|
|
299
|
+
self.respond(request, success=False, message="no program to debug")
|
|
300
|
+
return
|
|
301
|
+
program = os.path.realpath(os.path.join(cwd, program))
|
|
302
|
+
self.stop_on_entry = bool(args.get("stopOnEntry", False))
|
|
303
|
+
self.dir = tempfile.mkdtemp(prefix="bash-dap-")
|
|
304
|
+
os.mkfifo(os.path.join(self.dir, "commands"))
|
|
305
|
+
os.mkfifo(os.path.join(self.dir, "replies"))
|
|
306
|
+
self.save_breaks()
|
|
307
|
+
env = {"BASH_DAP_DIR": self.dir, "BASH_DAP_HARNESS": HARNESS}
|
|
308
|
+
env.update({k: v for k, v in (args.get("env") or {}).items() if v is not None})
|
|
309
|
+
argv = [args.get("bash") or "bash", "-c", '. "$BASH_DAP_HARNESS"', program]
|
|
310
|
+
argv += [str(a) for a in args.get("args") or []]
|
|
311
|
+
console = args.get("console", "integratedTerminal")
|
|
312
|
+
if console != "internalConsole" and self.client.get("supportsRunInTerminalRequest"):
|
|
313
|
+
# in the terminal of the client: the script reads what is typed there
|
|
314
|
+
def ran(response):
|
|
315
|
+
if not response.get("success"):
|
|
316
|
+
self.respond(request, success=False,
|
|
317
|
+
message=response.get("message", "runInTerminal failed"))
|
|
318
|
+
return
|
|
319
|
+
self.pid = (response.get("body") or {}).get("processId")
|
|
320
|
+
self.respond(request)
|
|
321
|
+
|
|
322
|
+
seq = self.send({"type": "request", "command": "runInTerminal", "arguments": {
|
|
323
|
+
"kind": "external" if console == "externalTerminal" else "integrated",
|
|
324
|
+
"title": "bash-dap", "cwd": cwd, "args": argv, "env": env}})
|
|
325
|
+
self.callbacks[seq] = ran
|
|
326
|
+
else:
|
|
327
|
+
full = dict(os.environ)
|
|
328
|
+
full.update(env)
|
|
329
|
+
try:
|
|
330
|
+
self.proc = subprocess.Popen(argv, cwd=cwd, env=full, stdin=subprocess.DEVNULL,
|
|
331
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
332
|
+
start_new_session=True)
|
|
333
|
+
except OSError as e:
|
|
334
|
+
self.respond(request, success=False, message=str(e))
|
|
335
|
+
return
|
|
336
|
+
for stream, category in ((self.proc.stdout, "stdout"), (self.proc.stderr, "stderr")):
|
|
337
|
+
threading.Thread(target=self.pump, args=(stream, category), daemon=True).start()
|
|
338
|
+
self.respond(request)
|
|
339
|
+
threading.Thread(target=self.harness_loop, daemon=True).start()
|
|
340
|
+
|
|
341
|
+
def pump(self, stream, category):
|
|
342
|
+
for chunk in iter(lambda: stream.read1(4096), b""):
|
|
343
|
+
self.output(chunk.decode(errors="replace"), category)
|
|
344
|
+
|
|
345
|
+
def on_setBreakpoints(self, request):
|
|
346
|
+
args = request.get("arguments", {})
|
|
347
|
+
path = os.path.realpath(args.get("source", {}).get("path", ""))
|
|
348
|
+
lines = [b["line"] for b in args.get("breakpoints", []) if "line" in b]
|
|
349
|
+
self.breaks[path] = lines
|
|
350
|
+
self.save_breaks()
|
|
351
|
+
self.respond(request, {"breakpoints": [
|
|
352
|
+
{"id": i + 1, "verified": True, "line": line, "source": {"path": path}}
|
|
353
|
+
for i, line in enumerate(lines)]})
|
|
354
|
+
|
|
355
|
+
def on_setFunctionBreakpoints(self, request):
|
|
356
|
+
self.respond(request, {"breakpoints": [
|
|
357
|
+
{"verified": False, "message": "bash-dap has breakpoints on lines only"}
|
|
358
|
+
for _ in request.get("arguments", {}).get("breakpoints", [])]})
|
|
359
|
+
|
|
360
|
+
def on_setExceptionBreakpoints(self, request):
|
|
361
|
+
self.respond(request, {"breakpoints": []})
|
|
362
|
+
|
|
363
|
+
def on_configurationDone(self, request):
|
|
364
|
+
self.configured = True
|
|
365
|
+
self.respond(request)
|
|
366
|
+
if self.stop_on_entry:
|
|
367
|
+
self.event("stopped", {"reason": "entry", "threadId": 1, "allThreadsStopped": True})
|
|
368
|
+
else:
|
|
369
|
+
self.resume("run")
|
|
370
|
+
|
|
371
|
+
def on_threads(self, request):
|
|
372
|
+
self.respond(request, {"threads": [{"id": 1, "name": "bash"}]})
|
|
373
|
+
|
|
374
|
+
def on_stackTrace(self, request):
|
|
375
|
+
lines = self.ask("stack") or []
|
|
376
|
+
frames = []
|
|
377
|
+
for kind, rest in lines:
|
|
378
|
+
if kind != "frame":
|
|
379
|
+
continue
|
|
380
|
+
level, func, line, path = (rest.split(" ", 3) + ["", "", "", ""])[:4]
|
|
381
|
+
frames.append({"id": int(level) + 1, "name": func, "line": int(line or 0),
|
|
382
|
+
"column": 1, "source": {"path": path, "name": os.path.basename(path)}})
|
|
383
|
+
args = request.get("arguments", {})
|
|
384
|
+
start = args.get("startFrame", 0)
|
|
385
|
+
levels = args.get("levels") or len(frames)
|
|
386
|
+
self.respond(request, {"stackFrames": frames[start:start + levels],
|
|
387
|
+
"totalFrames": len(frames)})
|
|
388
|
+
|
|
389
|
+
def on_scopes(self, request):
|
|
390
|
+
# bash has dynamic scope: a frame sees the variables of those under it, all at once
|
|
391
|
+
self.respond(request, {"scopes": [
|
|
392
|
+
{"name": "Variables", "presentationHint": "locals",
|
|
393
|
+
"variablesReference": VARIABLES_REF, "expensive": False}]})
|
|
394
|
+
|
|
395
|
+
def on_variables(self, request):
|
|
396
|
+
ref = request.get("arguments", {}).get("variablesReference")
|
|
397
|
+
if ref == VARIABLES_REF:
|
|
398
|
+
items = []
|
|
399
|
+
for kind, rest in self.ask("vars") or []:
|
|
400
|
+
if kind == "var":
|
|
401
|
+
name, _, text = rest.partition(" ")
|
|
402
|
+
items.append(self.variable(name, unquote(text), "$" + name))
|
|
403
|
+
self.respond(request, {"variables": items})
|
|
404
|
+
else:
|
|
405
|
+
self.respond(request, {"variables": self.refs.get(ref, [])})
|
|
406
|
+
|
|
407
|
+
def on_evaluate(self, request):
|
|
408
|
+
args = request.get("arguments", {})
|
|
409
|
+
expression = args.get("expression", "").strip()
|
|
410
|
+
lines = self.ask("print " + expression)
|
|
411
|
+
if lines is None:
|
|
412
|
+
self.respond(request, success=False, message="the script is not stopped")
|
|
413
|
+
return
|
|
414
|
+
values = [unquote(rest) for kind, rest in lines if kind == "value"]
|
|
415
|
+
text = values[0] if values else ""
|
|
416
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", expression):
|
|
417
|
+
if text.startswith("bash:") or "not found" in text:
|
|
418
|
+
self.respond(request, success=False, message="%s is not set" % expression)
|
|
419
|
+
return
|
|
420
|
+
item = self.variable(expression, text, expression)
|
|
421
|
+
self.respond(request, {"result": item["value"],
|
|
422
|
+
"variablesReference": item["variablesReference"]})
|
|
423
|
+
return
|
|
424
|
+
self.respond(request, {"result": text, "variablesReference": 0})
|
|
425
|
+
|
|
426
|
+
def on_continue(self, request):
|
|
427
|
+
self.resume("run")
|
|
428
|
+
self.respond(request, {"allThreadsContinued": True})
|
|
429
|
+
|
|
430
|
+
def on_next(self, request):
|
|
431
|
+
self.resume("next")
|
|
432
|
+
self.respond(request)
|
|
433
|
+
|
|
434
|
+
def on_stepIn(self, request):
|
|
435
|
+
self.resume("step")
|
|
436
|
+
self.respond(request)
|
|
437
|
+
|
|
438
|
+
def on_stepOut(self, request):
|
|
439
|
+
self.resume("finish")
|
|
440
|
+
self.respond(request)
|
|
441
|
+
|
|
442
|
+
def on_pause(self, request):
|
|
443
|
+
# every process of the script stops at its next command
|
|
444
|
+
if self.dir is not None and not self.stopped:
|
|
445
|
+
self.pause_asked = True
|
|
446
|
+
with open(os.path.join(self.dir, "state"), "w") as f:
|
|
447
|
+
f.write("step 0\n")
|
|
448
|
+
self.respond(request)
|
|
449
|
+
|
|
450
|
+
def on_disconnect(self, request):
|
|
451
|
+
self.terminate()
|
|
452
|
+
self.respond(request)
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def on_terminate(self, request):
|
|
456
|
+
self.terminate()
|
|
457
|
+
self.respond(request)
|
|
458
|
+
self.event("terminated")
|
|
459
|
+
|
|
460
|
+
def terminate(self):
|
|
461
|
+
"""The script ended: its group, the session it has to itself. Never by its parent, and
|
|
462
|
+
never the group of the adapter or of the client."""
|
|
463
|
+
group = None
|
|
464
|
+
if self.proc is not None and self.proc.poll() is None:
|
|
465
|
+
group = self.proc.pid # start_new_session: it leads its group
|
|
466
|
+
elif self.pid:
|
|
467
|
+
try:
|
|
468
|
+
group = os.getpgid(self.pid)
|
|
469
|
+
except OSError:
|
|
470
|
+
group = None
|
|
471
|
+
if group and group > 1 and group != os.getpgid(0):
|
|
472
|
+
try:
|
|
473
|
+
os.killpg(group, signal.SIGTERM)
|
|
474
|
+
except OSError:
|
|
475
|
+
pass
|
|
476
|
+
elif self.pid:
|
|
477
|
+
try:
|
|
478
|
+
os.kill(self.pid, signal.SIGTERM)
|
|
479
|
+
except OSError:
|
|
480
|
+
pass
|
|
481
|
+
if self.commands is not None:
|
|
482
|
+
try:
|
|
483
|
+
os.close(self.commands)
|
|
484
|
+
except OSError:
|
|
485
|
+
pass
|
|
486
|
+
self.commands = None
|
|
487
|
+
if self.dir is not None:
|
|
488
|
+
for name in os.listdir(self.dir):
|
|
489
|
+
try:
|
|
490
|
+
os.unlink(os.path.join(self.dir, name))
|
|
491
|
+
except OSError:
|
|
492
|
+
pass
|
|
493
|
+
try:
|
|
494
|
+
os.rmdir(self.dir)
|
|
495
|
+
except OSError:
|
|
496
|
+
pass
|
|
497
|
+
self.dir = None
|
|
498
|
+
|
|
499
|
+
def run(self):
|
|
500
|
+
while True:
|
|
501
|
+
message = self.read_message()
|
|
502
|
+
if message is None:
|
|
503
|
+
break
|
|
504
|
+
if message.get("type") == "response":
|
|
505
|
+
callback = self.callbacks.pop(message.get("request_seq"), None)
|
|
506
|
+
if callback is not None:
|
|
507
|
+
callback(message)
|
|
508
|
+
continue
|
|
509
|
+
if message.get("type") != "request":
|
|
510
|
+
continue
|
|
511
|
+
handler = getattr(self, "on_" + message.get("command", ""), None)
|
|
512
|
+
if handler is None:
|
|
513
|
+
self.respond(message, success=False,
|
|
514
|
+
message="bash-dap: %s is not supported" % message.get("command"))
|
|
515
|
+
continue
|
|
516
|
+
try:
|
|
517
|
+
if handler(message) is False:
|
|
518
|
+
break
|
|
519
|
+
except Exception as e: # a request that fails is told, the session goes on
|
|
520
|
+
self.respond(message, success=False, message="bash-dap: %s" % e)
|
|
521
|
+
self.terminate()
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def main():
|
|
525
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
526
|
+
print("bash-dap: a debug adapter for bash scripts; it speaks the Debug Adapter Protocol "
|
|
527
|
+
"on stdin and stdout")
|
|
528
|
+
return 0
|
|
529
|
+
if len(sys.argv) > 1 and sys.argv[1] == "--version":
|
|
530
|
+
print("bash-dap 0.1")
|
|
531
|
+
return 0
|
|
532
|
+
Adapter(sys.stdin.buffer, sys.stdout.buffer).run()
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
sys.exit(main())
|