@@ -18,59 +18,317 @@ package commands
18
18
import (
19
19
"context"
20
20
"errors"
21
+ "fmt"
22
+ "io"
21
23
"os"
24
+ "path/filepath"
25
+ "runtime"
26
+ "sync"
27
+ "sync/atomic"
28
+ "time"
22
29
30
+ "github.com/arduino/arduino-cli/commands/cmderrors"
31
+ "github.com/arduino/arduino-cli/commands/internal/instances"
32
+ "github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
23
33
"github.com/arduino/arduino-cli/internal/i18n"
24
34
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
35
+ paths "github.com/arduino/go-paths-helper"
36
+ "github.com/djherbis/buffer"
37
+ "github.com/djherbis/nio/v3"
38
+ "github.com/sirupsen/logrus"
39
+ "google.golang.org/grpc/metadata"
25
40
)
26
41
27
- // Debug returns a stream response that can be used to fetch data from the
28
- // target. The first message passed through the `Debug` request must
29
- // contain DebugRequest configuration params, not data.
42
+ type debugServer struct {
43
+ ctx context.Context
44
+ req atomic.Pointer [rpc.GetDebugConfigRequest ]
45
+ in io.Reader
46
+ inSignal bool
47
+ inData bool
48
+ inEvent * sync.Cond
49
+ inLock sync.Mutex
50
+ out io.Writer
51
+ resultCB func (* rpc.DebugResponse_Result )
52
+ done chan bool
53
+ }
54
+
55
+ func (s * debugServer ) Send (resp * rpc.DebugResponse ) error {
56
+ if len (resp .GetData ()) > 0 {
57
+ if _ , err := s .out .Write (resp .GetData ()); err != nil {
58
+ return err
59
+ }
60
+ }
61
+ if res := resp .GetResult (); res != nil {
62
+ s .resultCB (res )
63
+ s .close ()
64
+ }
65
+ return nil
66
+ }
67
+
68
+ func (s * debugServer ) Recv () (r * rpc.DebugRequest , e error ) {
69
+ if conf := s .req .Swap (nil ); conf != nil {
70
+ return & rpc.DebugRequest {DebugRequest : conf }, nil
71
+ }
72
+
73
+ s .inEvent .L .Lock ()
74
+ for ! s .inSignal && ! s .inData {
75
+ s .inEvent .Wait ()
76
+ }
77
+ defer s .inEvent .L .Unlock ()
78
+
79
+ if s .inSignal {
80
+ s .inSignal = false
81
+ return & rpc.DebugRequest {SendInterrupt : true }, nil
82
+ }
83
+
84
+ if s .inData {
85
+ s .inData = false
86
+ buff := make ([]byte , 4096 )
87
+ n , err := s .in .Read (buff )
88
+ if err != nil {
89
+ return nil , err
90
+ }
91
+ return & rpc.DebugRequest {Data : buff [:n ]}, nil
92
+ }
93
+
94
+ panic ("invalid state in debug" )
95
+ }
96
+
97
+ func (s * debugServer ) close () {
98
+ close (s .done )
99
+ }
100
+
101
+ func (s * debugServer ) Context () context.Context { return s .ctx }
102
+ func (s * debugServer ) RecvMsg (m any ) error { return nil }
103
+ func (s * debugServer ) SendHeader (metadata.MD ) error { return nil }
104
+ func (s * debugServer ) SendMsg (m any ) error { return nil }
105
+ func (s * debugServer ) SetHeader (metadata.MD ) error { return nil }
106
+ func (s * debugServer ) SetTrailer (metadata.MD ) {}
107
+
108
+ // DebugServerToStreams creates a debug server that proxies the data to the given io streams.
109
+ // The GetDebugConfigRequest is used to configure the debbuger. sig is a channel that can be
110
+ // used to send os.Interrupt to the debug process. resultCB is a callback function that will
111
+ // receive the Debug result and closes the debug server.
112
+ func DebugServerToStreams (
113
+ ctx context.Context ,
114
+ req * rpc.GetDebugConfigRequest ,
115
+ in io.Reader , out io.Writer ,
116
+ sig chan os.Signal ,
117
+ resultCB func (* rpc.DebugResponse_Result ),
118
+ ) rpc.ArduinoCoreService_DebugServer {
119
+ server := & debugServer {
120
+ ctx : ctx ,
121
+ in : in ,
122
+ out : out ,
123
+ resultCB : resultCB ,
124
+ done : make (chan bool ),
125
+ }
126
+ serverIn , clientOut := nio .Pipe (buffer .New (32 * 1024 ))
127
+ server .in = serverIn
128
+ server .inEvent = sync .NewCond (& server .inLock )
129
+ server .req .Store (req )
130
+ go func () {
131
+ for {
132
+ select {
133
+ case <- sig :
134
+ server .inEvent .L .Lock ()
135
+ server .inSignal = true
136
+ server .inEvent .Broadcast ()
137
+ server .inEvent .L .Unlock ()
138
+ case <- server .done :
139
+ return
140
+ }
141
+ }
142
+ }()
143
+ go func () {
144
+ defer clientOut .Close ()
145
+ buff := make ([]byte , 4096 )
146
+ for {
147
+ n , readErr := in .Read (buff )
148
+
149
+ server .inEvent .L .Lock ()
150
+ var writeErr error
151
+ if readErr == nil {
152
+ _ , writeErr = clientOut .Write (buff [:n ])
153
+ }
154
+ server .inData = true
155
+ server .inEvent .Broadcast ()
156
+ server .inEvent .L .Unlock ()
157
+ if readErr != nil || writeErr != nil {
158
+ // exit on error
159
+ return
160
+ }
161
+ }
162
+ }()
163
+ return server
164
+ }
165
+
166
+ // Debug starts a debugging session. The first message passed through the `Debug` request must
167
+ // contain DebugRequest configuration params and no data.
30
168
func (s * arduinoCoreServerImpl ) Debug (stream rpc.ArduinoCoreService_DebugServer ) error {
169
+ // Utility functions
170
+ syncSend := NewSynchronizedSend (stream .Send )
171
+ sendResult := func (res * rpc.DebugResponse_Result ) error {
172
+ return syncSend .Send (& rpc.DebugResponse {Message : & rpc.DebugResponse_Result_ {Result : res }})
173
+ }
174
+ sendData := func (data []byte ) {
175
+ _ = syncSend .Send (& rpc.DebugResponse {Message : & rpc.DebugResponse_Data {Data : data }})
176
+ }
177
+
31
178
// Grab the first message
32
- msg , err := stream .Recv ()
179
+ debugConfReqMsg , err := stream .Recv ()
33
180
if err != nil {
34
181
return err
35
182
}
36
183
37
184
// Ensure it's a config message and not data
38
- req := msg .GetDebugRequest ()
39
- if req == nil {
185
+ debugConfReq := debugConfReqMsg .GetDebugRequest ()
186
+ if debugConfReq == nil {
40
187
return errors .New (i18n .Tr ("First message must contain debug request, not data" ))
41
188
}
42
189
43
190
// Launch debug recipe attaching stdin and out to grpc streaming
44
191
signalChan := make (chan os.Signal )
45
192
defer close (signalChan )
46
- outStream := feedStreamTo (func ( data [] byte ) {
47
- stream . Send ( & rpc. DebugResponse { Message : & rpc. DebugResponse_Data {
48
- Data : data ,
49
- }})
50
- } )
51
- resp , debugErr := Debug ( stream . Context (), req ,
52
- consumeStreamFrom ( func () ([] byte , error ) {
53
- command , err := stream . Recv ()
54
- if command .GetSendInterrupt () {
193
+ outStream := feedStreamTo (sendData )
194
+ defer outStream . Close ()
195
+ inStream := consumeStreamFrom ( func () ([] byte , error ) {
196
+ for {
197
+ req , err := stream . Recv ( )
198
+ if err != nil {
199
+ return nil , err
200
+ }
201
+ if req .GetSendInterrupt () {
55
202
signalChan <- os .Interrupt
56
203
}
57
- return command .GetData (), err
58
- }),
59
- outStream ,
60
- signalChan )
61
- outStream .Close ()
62
- if debugErr != nil {
63
- return debugErr
64
- }
65
- return stream .Send (resp )
66
- }
204
+ if data := req .GetData (); len (data ) > 0 {
205
+ return data , nil
206
+ }
207
+ }
208
+ })
209
+
210
+ pme , release , err := instances .GetPackageManagerExplorer (debugConfReq .GetInstance ())
211
+ if err != nil {
212
+ return err
213
+ }
214
+ defer release ()
215
+
216
+ // Exec debugger
217
+ commandLine , err := getCommandLine (debugConfReq , pme )
218
+ if err != nil {
219
+ return err
220
+ }
221
+ entry := logrus .NewEntry (logrus .StandardLogger ())
222
+ for i , param := range commandLine {
223
+ entry = entry .WithField (fmt .Sprintf ("param%d" , i ), param )
224
+ }
225
+ entry .Debug ("Executing debugger" )
226
+ cmd , err := paths .NewProcess (pme .GetEnvVarsForSpawnedProcess (), commandLine ... )
227
+ if err != nil {
228
+ return & cmderrors.FailedDebugError {Message : i18n .Tr ("Cannot execute debug tool" ), Cause : err }
229
+ }
230
+ in , err := cmd .StdinPipe ()
231
+ if err != nil {
232
+ return sendResult (& rpc.DebugResponse_Result {Error : err .Error ()})
233
+ }
234
+ defer in .Close ()
235
+ cmd .RedirectStdoutTo (io .Writer (outStream ))
236
+ cmd .RedirectStderrTo (io .Writer (outStream ))
237
+ if err := cmd .Start (); err != nil {
238
+ return sendResult (& rpc.DebugResponse_Result {Error : err .Error ()})
239
+ }
67
240
68
- // GetDebugConfig return metadata about a debug session
69
- func (s * arduinoCoreServerImpl ) GetDebugConfig (ctx context.Context , req * rpc.GetDebugConfigRequest ) (* rpc.GetDebugConfigResponse , error ) {
70
- return GetDebugConfig (ctx , req )
241
+ go func () {
242
+ for sig := range signalChan {
243
+ cmd .Signal (sig )
244
+ }
245
+ }()
246
+ go func () {
247
+ io .Copy (in , inStream )
248
+ time .Sleep (time .Second )
249
+ cmd .Kill ()
250
+ }()
251
+ if err := cmd .Wait (); err != nil {
252
+ return sendResult (& rpc.DebugResponse_Result {Error : err .Error ()})
253
+ }
254
+ return sendResult (& rpc.DebugResponse_Result {})
71
255
}
72
256
73
- // IsDebugSupported checks if debugging is supported for a given configuration
74
- func (s * arduinoCoreServerImpl ) IsDebugSupported (ctx context.Context , req * rpc.IsDebugSupportedRequest ) (* rpc.IsDebugSupportedResponse , error ) {
75
- return IsDebugSupported (ctx , req )
257
+ // getCommandLine compose a debug command represented by a core recipe
258
+ func getCommandLine (req * rpc.GetDebugConfigRequest , pme * packagemanager.Explorer ) ([]string , error ) {
259
+ debugInfo , err := getDebugProperties (req , pme , false )
260
+ if err != nil {
261
+ return nil , err
262
+ }
263
+
264
+ cmdArgs := []string {}
265
+ add := func (s string ) { cmdArgs = append (cmdArgs , s ) }
266
+
267
+ // Add path to GDB Client to command line
268
+ var gdbPath * paths.Path
269
+ switch debugInfo .GetToolchain () {
270
+ case "gcc" :
271
+ gdbexecutable := debugInfo .GetToolchainPrefix () + "-gdb"
272
+ if runtime .GOOS == "windows" {
273
+ gdbexecutable += ".exe"
274
+ }
275
+ gdbPath = paths .New (debugInfo .GetToolchainPath ()).Join (gdbexecutable )
276
+ default :
277
+ return nil , & cmderrors.FailedDebugError {Message : i18n .Tr ("Toolchain '%s' is not supported" , debugInfo .GetToolchain ())}
278
+ }
279
+ add (gdbPath .String ())
280
+
281
+ // Set GDB interpreter (default value should be "console")
282
+ gdbInterpreter := req .GetInterpreter ()
283
+ if gdbInterpreter == "" {
284
+ gdbInterpreter = "console"
285
+ }
286
+ add ("--interpreter=" + gdbInterpreter )
287
+ if gdbInterpreter != "console" {
288
+ add ("-ex" )
289
+ add ("set pagination off" )
290
+ }
291
+
292
+ // Add extra GDB execution commands
293
+ add ("-ex" )
294
+ add ("set remotetimeout 5" )
295
+
296
+ // Extract path to GDB Server
297
+ switch debugInfo .GetServer () {
298
+ case "openocd" :
299
+ var openocdConf rpc.DebugOpenOCDServerConfiguration
300
+ if err := debugInfo .GetServerConfiguration ().UnmarshalTo (& openocdConf ); err != nil {
301
+ return nil , err
302
+ }
303
+
304
+ serverCmd := fmt .Sprintf (`target extended-remote | "%s"` , debugInfo .GetServerPath ())
305
+
306
+ if cfg := openocdConf .GetScriptsDir (); cfg != "" {
307
+ serverCmd += fmt .Sprintf (` -s "%s"` , cfg )
308
+ }
309
+
310
+ for _ , script := range openocdConf .GetScripts () {
311
+ serverCmd += fmt .Sprintf (` --file "%s"` , script )
312
+ }
313
+
314
+ serverCmd += ` -c "gdb_port pipe"`
315
+ serverCmd += ` -c "telnet_port 0"`
316
+
317
+ add ("-ex" )
318
+ add (serverCmd )
319
+
320
+ default :
321
+ return nil , & cmderrors.FailedDebugError {Message : i18n .Tr ("GDB server '%s' is not supported" , debugInfo .GetServer ())}
322
+ }
323
+
324
+ // Add executable
325
+ add (debugInfo .GetExecutable ())
326
+
327
+ // Transform every path to forward slashes (on Windows some tools further
328
+ // escapes the command line so the backslash "\" gets in the way).
329
+ for i , param := range cmdArgs {
330
+ cmdArgs [i ] = filepath .ToSlash (param )
331
+ }
332
+
333
+ return cmdArgs , nil
76
334
}
0 commit comments