aboutsummaryrefslogtreecommitdiff
path: root/Scripts/lldb-dap-proxy.pl
diff options
context:
space:
mode:
authorDavid Czihak <git@dcz.at>2026-05-07 14:33:19 +0200
committerDavid Czihak <git@dcz.at>2026-05-07 14:33:19 +0200
commitddf2de739068b5ff0866ccb1d067f3cb53a4fc55 (patch)
tree1a77efe9d73a6172be3c37d29b321eadd4efe379 /Scripts/lldb-dap-proxy.pl
Initial commitv0.1.7
Diffstat (limited to 'Scripts/lldb-dap-proxy.pl')
-rw-r--r--Scripts/lldb-dap-proxy.pl213
1 files changed, 213 insertions, 0 deletions
diff --git a/Scripts/lldb-dap-proxy.pl b/Scripts/lldb-dap-proxy.pl
new file mode 100644
index 0000000..5f545b1
--- /dev/null
+++ b/Scripts/lldb-dap-proxy.pl
@@ -0,0 +1,213 @@
1#!/usr/bin/perl
2use strict;
3use warnings;
4use IPC::Open3;
5use IO::Select;
6use JSON::PP;
7use Symbol 'gensym';
8
9my $adapter_path = $ARGV[0] // $ENV{NOVA_ZIG_LLDB_DAP_PATH};
10my $log_path = $ARGV[1] // $ENV{NOVA_ZIG_DEBUG_LOG};
11my @adapter_args = @ARGV > 2 ? @ARGV[2 .. $#ARGV] : ();
12
13sub log_msg {
14 return unless defined $log_path;
15 my ($msg) = @_;
16 open(my $fh, '>>', $log_path) or return;
17 my @t = gmtime(time);
18 printf $fh "[%04d-%02d-%02dT%02d:%02d:%02dZ] %s\n",
19 $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0], $msg;
20 close($fh);
21}
22
23unless (defined $adapter_path) {
24 log_msg('missing adapter path');
25 print STDERR "lldb-dap proxy requires a target adapter path.\n";
26 exit(1);
27}
28
29log_msg("proxy start adapter=$adapter_path");
30
31# open3 treats an undef handle as "inherit parent STDIN/STDOUT", so pre-allocate
32# anonymous typeglobs to force pipe creation for all three streams.
33my ($child_in, $child_out, $child_err) = (gensym(), gensym(), gensym());
34my $pid = eval { open3($child_in, $child_out, $child_err, $adapter_path, @adapter_args) };
35if ($@) {
36 (my $err = $@) =~ s/\s+$//;
37 log_msg("adapter error $err");
38 print STDERR "$err\n";
39 exit(1);
40}
41
42binmode($_, ':raw') for \*STDIN, \*STDOUT, $child_in, $child_out, $child_err;
43
44$SIG{INT} = sub { kill 'INT', $pid };
45$SIG{TERM} = sub { kill 'TERM', $pid };
46
47my $json = JSON::PP->new->utf8;
48
49my ($next_client_seq, $next_adapter_seq) = (1, 1);
50my (%client_request_seq_map, %adapter_request_seq_map, %request_args_by_client_seq);
51my ($stdin_buf, $child_buf) = ('', '');
52
53my $stdin_fn = fileno(\*STDIN);
54my $child_out_fn = fileno($child_out);
55
56my $sel = IO::Select->new(\*STDIN, $child_out, $child_err);
57
58LOOP: while ($sel->count > 0) {
59 my @ready = $sel->can_read;
60 for my $fh (@ready) {
61 my ($chunk, $n);
62 $n = sysread($fh, $chunk, 65536);
63 unless (defined $n && $n > 0) {
64 $sel->remove($fh);
65 my $fn = fileno($fh);
66 close($child_in) if $fn == $stdin_fn;
67 last LOOP if $fn == $child_out_fn; # adapter stdout closing ends the session
68 next;
69 }
70 my $fn = fileno($fh);
71 if ($fn == $stdin_fn) {
72 $stdin_buf .= $chunk;
73 flush_dap(\$stdin_buf, $child_in, \&rewrite_client_message);
74 } elsif ($fn == $child_out_fn) {
75 $child_buf .= $chunk;
76 flush_dap(\$child_buf, \*STDOUT, \&rewrite_adapter_message);
77 } else {
78 log_msg('stderr ' . $chunk);
79 print STDERR $chunk;
80 }
81 }
82}
83
84waitpid($pid, 0);
85# $? encodes exit code in the high byte and terminating signal in the low 7 bits.
86# Re-raise the signal so the parent sees a signal-killed exit rather than exit(0).
87my $signal = $? & 127;
88if ($signal) {
89 $SIG{$_} = 'DEFAULT' for qw(INT TERM);
90 kill $signal, $$;
91 sleep 1;
92}
93exit($? >> 8);
94
95sub flush_dap {
96 my ($buf_ref, $dest, $rewrite_fn) = @_;
97 while (1) {
98 my $header_end = index($$buf_ref, "\r\n\r\n");
99 last if $header_end == -1;
100
101 my $header = substr($$buf_ref, 0, $header_end);
102 unless ($header =~ /Content-Length:\s*(\d+)/i) {
103 syswrite($dest, $$buf_ref);
104 $$buf_ref = '';
105 return;
106 }
107
108 my $body_len = 0 + $1;
109 my $frame_len = $header_end + 4 + $body_len;
110 last if length($$buf_ref) < $frame_len;
111
112 my $body = substr($$buf_ref, $header_end + 4, $body_len);
113 $$buf_ref = substr($$buf_ref, $frame_len);
114
115 my $msg = eval { $json->decode($body) };
116 if (!$@ && ref($msg) eq 'HASH') {
117 $rewrite_fn->($msg);
118 my $out = $json->encode($msg);
119 syswrite($dest, 'Content-Length: ' . length($out) . "\r\n\r\n");
120 syswrite($dest, $out);
121 } else {
122 log_msg('non-json message forwarded');
123 syswrite($dest, 'Content-Length: ' . $body_len . "\r\n\r\n");
124 syswrite($dest, $body);
125 }
126 }
127}
128
129sub rewrite_client_message {
130 my ($msg) = @_;
131 my $type = $msg->{type} // '';
132 my $command = $msg->{command} // '';
133 log_msg(sprintf 'client message type=%s command=%s seq_in=%s request_seq_in=%s',
134 $type, $command, $msg->{seq} // '', $msg->{request_seq} // '');
135
136 # Nova omits the filters array on setExceptionBreakpoints requests, which
137 # lldb-dap then rejects. Ensure the field is always present before forwarding.
138 if ($type eq 'request' && $command eq 'setExceptionBreakpoints') {
139 $msg->{arguments} = {} unless ref($msg->{arguments}) eq 'HASH';
140 $msg->{arguments}{filters} = [] unless ref($msg->{arguments}{filters}) eq 'ARRAY';
141 }
142
143 if ($type eq 'response' && defined $msg->{request_seq}) {
144 my $rseq = $msg->{request_seq};
145 if (exists $adapter_request_seq_map{$rseq}) {
146 $msg->{request_seq} = $adapter_request_seq_map{$rseq};
147 delete $adapter_request_seq_map{$rseq};
148 }
149 }
150
151 if (defined $msg->{seq}) {
152 my $orig = $msg->{seq};
153 my $rewrite = $next_client_seq++;
154 if ($type eq 'request') {
155 $client_request_seq_map{$rewrite} = $orig;
156 $request_args_by_client_seq{$rewrite} = {
157 command => $command,
158 arguments => $msg->{arguments},
159 };
160 }
161 $msg->{seq} = $rewrite;
162 }
163
164 log_msg(sprintf 'client message seq_out=%s request_seq_out=%s',
165 $msg->{seq} // '', $msg->{request_seq} // '');
166}
167
168sub rewrite_adapter_message {
169 my ($msg) = @_;
170 my $type = $msg->{type} // '';
171 my $command = $msg->{command} // '';
172 log_msg(sprintf 'adapter message type=%s command=%s seq_in=%s request_seq_in=%s',
173 $type, $command, $msg->{seq} // '', $msg->{request_seq} // '');
174
175 if ($type eq 'response' && defined $msg->{request_seq}) {
176 my $rseq = $msg->{request_seq};
177 my $orig_req = $request_args_by_client_seq{$rseq};
178 my $orig_seq = $client_request_seq_map{$rseq};
179
180 if (defined $orig_seq) {
181 $msg->{request_seq} = $orig_seq;
182 delete $client_request_seq_map{$rseq};
183 }
184
185 # lldb-dap returns a successful setExceptionBreakpoints response without
186 # the breakpoints array the DAP spec requires. Synthesise one verified
187 # entry per filter so Nova's client confirms each breakpoint as registered.
188 if (defined $orig_req
189 && ($orig_req->{command} // '') eq 'setExceptionBreakpoints'
190 && $msg->{success})
191 {
192 my $filters = ref($orig_req->{arguments}{filters}) eq 'ARRAY'
193 ? $orig_req->{arguments}{filters} : [];
194 $msg->{body} = {} unless ref($msg->{body}) eq 'HASH';
195 unless (ref($msg->{body}{breakpoints}) eq 'ARRAY') {
196 $msg->{body}{breakpoints} = [ map { +{ verified => JSON::PP::true } } @$filters ];
197 }
198 }
199
200 delete $request_args_by_client_seq{$rseq};
201 }
202
203 if (defined $msg->{seq}) {
204 my $orig = $msg->{seq};
205 my $rewrite = $next_adapter_seq++;
206 $adapter_request_seq_map{$rewrite} = $orig if $type eq 'request';
207 $msg->{seq} = $rewrite;
208 log_msg("adapter seq map $orig=>$rewrite");
209 }
210
211 log_msg(sprintf 'adapter message seq_out=%s request_seq_out=%s',
212 $msg->{seq} // '', $msg->{request_seq} // '');
213}