diff options
Diffstat (limited to 'Scripts/lldb-dap-proxy.pl')
| -rw-r--r-- | Scripts/lldb-dap-proxy.pl | 213 |
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 | ||
| 2 | use strict; | ||
| 3 | use warnings; | ||
| 4 | use IPC::Open3; | ||
| 5 | use IO::Select; | ||
| 6 | use JSON::PP; | ||
| 7 | use Symbol 'gensym'; | ||
| 8 | |||
| 9 | my $adapter_path = $ARGV[0] // $ENV{NOVA_ZIG_LLDB_DAP_PATH}; | ||
| 10 | my $log_path = $ARGV[1] // $ENV{NOVA_ZIG_DEBUG_LOG}; | ||
| 11 | my @adapter_args = @ARGV > 2 ? @ARGV[2 .. $#ARGV] : (); | ||
| 12 | |||
| 13 | sub 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 | |||
| 23 | unless (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 | |||
| 29 | log_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. | ||
| 33 | my ($child_in, $child_out, $child_err) = (gensym(), gensym(), gensym()); | ||
| 34 | my $pid = eval { open3($child_in, $child_out, $child_err, $adapter_path, @adapter_args) }; | ||
| 35 | if ($@) { | ||
| 36 | (my $err = $@) =~ s/\s+$//; | ||
| 37 | log_msg("adapter error $err"); | ||
| 38 | print STDERR "$err\n"; | ||
| 39 | exit(1); | ||
| 40 | } | ||
| 41 | |||
| 42 | binmode($_, ':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 | |||
| 47 | my $json = JSON::PP->new->utf8; | ||
| 48 | |||
| 49 | my ($next_client_seq, $next_adapter_seq) = (1, 1); | ||
| 50 | my (%client_request_seq_map, %adapter_request_seq_map, %request_args_by_client_seq); | ||
| 51 | my ($stdin_buf, $child_buf) = ('', ''); | ||
| 52 | |||
| 53 | my $stdin_fn = fileno(\*STDIN); | ||
| 54 | my $child_out_fn = fileno($child_out); | ||
| 55 | |||
| 56 | my $sel = IO::Select->new(\*STDIN, $child_out, $child_err); | ||
| 57 | |||
| 58 | LOOP: 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 | |||
| 84 | waitpid($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). | ||
| 87 | my $signal = $? & 127; | ||
| 88 | if ($signal) { | ||
| 89 | $SIG{$_} = 'DEFAULT' for qw(INT TERM); | ||
| 90 | kill $signal, $$; | ||
| 91 | sleep 1; | ||
| 92 | } | ||
| 93 | exit($? >> 8); | ||
| 94 | |||
| 95 | sub 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 | |||
| 129 | sub 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 | |||
| 168 | sub 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 | } | ||
