@@ -18,59 +18,317 @@ package commands
1818import (
1919 "context"
2020 "errors"
21+ "fmt"
22+ "io"
2123 "os"
24+ "path/filepath"
25+ "runtime"
26+ "sync"
27+ "sync/atomic"
28+ "time"
2229
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"
2333 "github.com/arduino/arduino-cli/internal/i18n"
2434 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"
2540)
2641
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.
30168func (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+
31178 // Grab the first message
32- msg , err := stream .Recv ()
179+ debugConfReqMsg , err := stream .Recv ()
33180 if err != nil {
34181 return err
35182 }
36183
37184 // 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 {
40187 return errors .New (i18n .Tr ("First message must contain debug request, not data" ))
41188 }
42189
43190 // Launch debug recipe attaching stdin and out to grpc streaming
44191 signalChan := make (chan os.Signal )
45192 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 () {
55202 signalChan <- os .Interrupt
56203 }
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+ }
67240
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 {})
71255}
72256
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
76334}
0 commit comments