Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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‎

Lines changed: 289 additions & 31 deletions
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+
deferoutStream.Close()
195+
inStream:=consumeStreamFrom(func() ([]byte, error) {
196+
for {
197+
req, err:=stream.Recv()
198+
iferr!=nil {
199+
returnnil, 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‎

Lines changed: 2 additions & 2 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /