Skip to content

Commit 5f01000

Browse files
authored
[skip-changelog] Made the Debug* gRPC API implementation in par with the rest (#2672)
* Inlined gRPC methods to implement GetDebugConfig and IsDebugSupported * Inlined function * Renamed vars for clarity * Added Debug gRPC adapter function * Moved function and removed useless file * Forward os.interrupt (aka CTRL-C) signal to the gdb process This a challenging problem because we must wait on both an io.Read(...) and a channel-read but, unfortunately, go native select can wait only on channels. To overcome this limitation I had to resort to a conditional variable and write some boilerplate code to make everything synchronized.
1 parent a353f86 commit 5f01000

File tree

5 files changed

+297
-238
lines changed

5 files changed

+297
-238
lines changed

commands/service_debug.go

+289-31
Original file line numberDiff line numberDiff line change
@@ -18,59 +18,317 @@ package commands
1818
import (
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.
30168
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+
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
}

commands/service_debug_config.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import (
3838
)
3939

4040
// GetDebugConfig returns metadata to start debugging with the specified board
41-
func GetDebugConfig(ctx context.Context, req *rpc.GetDebugConfigRequest) (*rpc.GetDebugConfigResponse, error) {
41+
func (s *arduinoCoreServerImpl) GetDebugConfig(ctx context.Context, req *rpc.GetDebugConfigRequest) (*rpc.GetDebugConfigResponse, error) {
4242
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
4343
if err != nil {
4444
return nil, err
@@ -48,7 +48,7 @@ func GetDebugConfig(ctx context.Context, req *rpc.GetDebugConfigRequest) (*rpc.G
4848
}
4949

5050
// IsDebugSupported checks if the given board/programmer configuration supports debugging.
51-
func IsDebugSupported(ctx context.Context, req *rpc.IsDebugSupportedRequest) (*rpc.IsDebugSupportedResponse, error) {
51+
func (s *arduinoCoreServerImpl) IsDebugSupported(ctx context.Context, req *rpc.IsDebugSupportedRequest) (*rpc.IsDebugSupportedResponse, error) {
5252
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
5353
if err != nil {
5454
return nil, err

0 commit comments

Comments
 (0)