Commit bb815cfe authored by Cristian Maglie's avatar Cristian Maglie

Inlining methods in ArduinoCoreServiceImpl (part 8: Monitor)

This change is quite challenging because it implements a bidirectional
streaming service. The gRPC implementation is slightly simpler, BTW the
command-line requires a bit of streams fiddling to get the same
behaviour as before because now:

 * the Monitor call do not return anymore a clean io.ReadWriteCloser.
 * the call to srv.Monitor is blocking until the port is closed or the
   context is canceled.
parent 917dcc5d
...@@ -17,15 +17,10 @@ package commands ...@@ -17,15 +17,10 @@ package commands
import ( import (
"context" "context"
"errors"
"io"
"sync/atomic"
"github.com/arduino/arduino-cli/commands/cache" "github.com/arduino/arduino-cli/commands/cache"
"github.com/arduino/arduino-cli/commands/cmderrors"
"github.com/arduino/arduino-cli/commands/updatecheck" "github.com/arduino/arduino-cli/commands/updatecheck"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/sirupsen/logrus"
) )
// NewArduinoCoreServer returns an implementation of the ArduinoCoreService gRPC service // NewArduinoCoreServer returns an implementation of the ArduinoCoreService gRPC service
...@@ -356,102 +351,6 @@ func (s *arduinoCoreServerImpl) EnumerateMonitorPortSettings(ctx context.Context ...@@ -356,102 +351,6 @@ func (s *arduinoCoreServerImpl) EnumerateMonitorPortSettings(ctx context.Context
return EnumerateMonitorPortSettings(ctx, req) return EnumerateMonitorPortSettings(ctx, req)
} }
// Monitor FIXMEDOC
func (s *arduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorServer) error {
syncSend := NewSynchronizedSend(stream.Send)
// The configuration must be sent on the first message
req, err := stream.Recv()
if err != nil {
return err
}
openReq := req.GetOpenRequest()
if openReq == nil {
return &cmderrors.InvalidInstanceError{}
}
portProxy, _, err := Monitor(stream.Context(), openReq)
if err != nil {
return err
}
// Send a message with Success set to true to notify the caller of the port being now active
_ = syncSend.Send(&rpc.MonitorResponse{Success: true})
cancelCtx, cancel := context.WithCancel(stream.Context())
gracefulCloseInitiated := &atomic.Bool{}
gracefuleCloseCtx, gracefulCloseCancel := context.WithCancel(context.Background())
// gRPC stream receiver (gRPC data -> monitor, config, close)
go func() {
defer cancel()
for {
msg, err := stream.Recv()
if errors.Is(err, io.EOF) {
return
}
if err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
return
}
if conf := msg.GetUpdatedConfiguration(); conf != nil {
for _, c := range conf.GetSettings() {
if err := portProxy.Config(c.GetSettingId(), c.GetValue()); err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
}
}
}
if closeMsg := msg.GetClose(); closeMsg {
gracefulCloseInitiated.Store(true)
if err := portProxy.Close(); err != nil {
logrus.WithError(err).Debug("Error closing monitor port")
}
gracefulCloseCancel()
}
tx := msg.GetTxData()
for len(tx) > 0 {
n, err := portProxy.Write(tx)
if errors.Is(err, io.EOF) {
return
}
if err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
return
}
tx = tx[n:]
}
}
}()
// gRPC stream sender (monitor -> gRPC)
go func() {
defer cancel() // unlock the receiver
buff := make([]byte, 4096)
for {
n, err := portProxy.Read(buff)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
break
}
if err := syncSend.Send(&rpc.MonitorResponse{RxData: buff[:n]}); err != nil {
break
}
}
}()
<-cancelCtx.Done()
if gracefulCloseInitiated.Load() {
// Port closing has been initiated in the receiver
<-gracefuleCloseCtx.Done()
} else {
portProxy.Close()
}
return nil
}
// CheckForArduinoCLIUpdates FIXMEDOC // CheckForArduinoCLIUpdates FIXMEDOC
func (s *arduinoCoreServerImpl) CheckForArduinoCLIUpdates(ctx context.Context, req *rpc.CheckForArduinoCLIUpdatesRequest) (*rpc.CheckForArduinoCLIUpdatesResponse, error) { func (s *arduinoCoreServerImpl) CheckForArduinoCLIUpdates(ctx context.Context, req *rpc.CheckForArduinoCLIUpdatesRequest) (*rpc.CheckForArduinoCLIUpdatesResponse, error) {
return updatecheck.CheckForArduinoCLIUpdates(ctx, req) return updatecheck.CheckForArduinoCLIUpdates(ctx, req)
......
...@@ -17,8 +17,10 @@ package commands ...@@ -17,8 +17,10 @@ package commands
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"sync/atomic"
"github.com/arduino/arduino-cli/commands/cmderrors" "github.com/arduino/arduino-cli/commands/cmderrors"
"github.com/arduino/arduino-cli/commands/internal/instances" "github.com/arduino/arduino-cli/commands/internal/instances"
...@@ -27,87 +29,211 @@ import ( ...@@ -27,87 +29,211 @@ import (
pluggableMonitor "github.com/arduino/arduino-cli/internal/arduino/monitor" pluggableMonitor "github.com/arduino/arduino-cli/internal/arduino/monitor"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-properties-orderedmap" "github.com/arduino/go-properties-orderedmap"
"github.com/djherbis/buffer"
"github.com/djherbis/nio/v3"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"google.golang.org/grpc/metadata"
) )
// portProxy is an io.ReadWriteCloser that maps into the monitor port of the board type monitorPipeServer struct {
type portProxy struct { ctx context.Context
rw io.ReadWriter req atomic.Pointer[rpc.MonitorPortOpenRequest]
changeSettingsCB func(setting, value string) error in *nio.PipeReader
closeCB func() error out *nio.PipeWriter
} }
func (p *portProxy) Read(buff []byte) (int, error) { func (s *monitorPipeServer) Send(resp *rpc.MonitorResponse) error {
return p.rw.Read(buff) if len(resp.GetRxData()) > 0 {
if _, err := s.out.Write(resp.GetRxData()); err != nil {
return err
}
}
return nil
}
func (s *monitorPipeServer) Recv() (r *rpc.MonitorRequest, e error) {
if conf := s.req.Swap(nil); conf != nil {
return &rpc.MonitorRequest{Message: &rpc.MonitorRequest_OpenRequest{OpenRequest: conf}}, nil
}
buff := make([]byte, 4096)
n, err := s.in.Read(buff)
if err != nil {
return nil, err
}
return &rpc.MonitorRequest{Message: &rpc.MonitorRequest_TxData{TxData: buff[:n]}}, nil
} }
func (p *portProxy) Write(buff []byte) (int, error) { func (s *monitorPipeServer) Context() context.Context {
return p.rw.Write(buff) return s.ctx
} }
// Config sets the port configuration setting to the specified value func (s *monitorPipeServer) RecvMsg(m any) error { return nil }
func (p *portProxy) Config(setting, value string) error { func (s *monitorPipeServer) SendHeader(metadata.MD) error { return nil }
return p.changeSettingsCB(setting, value) func (s *monitorPipeServer) SendMsg(m any) error { return nil }
func (s *monitorPipeServer) SetHeader(metadata.MD) error { return nil }
func (s *monitorPipeServer) SetTrailer(metadata.MD) {}
type monitorPipeClient struct {
in *nio.PipeReader
out *nio.PipeWriter
close func()
} }
// Close the port func (s *monitorPipeClient) Read(buff []byte) (n int, err error) {
func (p *portProxy) Close() error { return s.in.Read(buff)
return p.closeCB()
} }
// Monitor opens a communication port. It returns a PortProxy to communicate with the port and a PortDescriptor func (s *monitorPipeClient) Write(buff []byte) (n int, err error) {
// that describes the available configuration settings. return s.out.Write(buff)
func Monitor(ctx context.Context, req *rpc.MonitorPortOpenRequest) (*portProxy, *pluggableMonitor.PortDescriptor, error) { }
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
if err != nil {
return nil, nil, err
}
defer release()
m, boardSettings, err := findMonitorAndSettingsForProtocolAndBoard(pme, req.GetPort().GetProtocol(), req.GetFqbn()) func (s *monitorPipeClient) Close() error {
s.in.Close()
s.out.Close()
s.close()
return nil
}
// MonitorServerToReadWriteCloser creates a monitor server that proxies the data to a ReadWriteCloser.
// The server is returned along with the ReadWriteCloser that can be used to send and receive data
// to the server. The MonitorPortOpenRequest is used to configure the monitor.
func MonitorServerToReadWriteCloser(ctx context.Context, req *rpc.MonitorPortOpenRequest) (rpc.ArduinoCoreService_MonitorServer, io.ReadWriteCloser) {
server := &monitorPipeServer{}
client := &monitorPipeClient{}
server.req.Store(req)
server.ctx, client.close = context.WithCancel(ctx)
client.in, server.out = nio.Pipe(buffer.New(32 * 1024))
server.in, client.out = nio.Pipe(buffer.New(32 * 1024))
return server, client
}
// Monitor opens a port monitor and streams data back and forth until the request is kept alive.
func (s *arduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorServer) error {
// The configuration must be sent on the first message
req, err := stream.Recv()
if err != nil { if err != nil {
return nil, nil, err return err
} }
if err := m.Run(); err != nil { openReq := req.GetOpenRequest()
return nil, nil, &cmderrors.FailedMonitorError{Cause: err} if openReq == nil {
return &cmderrors.InvalidInstanceError{}
} }
descriptor, err := m.Describe() pme, release, err := instances.GetPackageManagerExplorer(openReq.GetInstance())
if err != nil { if err != nil {
m.Quit() return err
return nil, nil, &cmderrors.FailedMonitorError{Cause: err}
} }
defer release()
// Apply user-requested settings monitor, boardSettings, err := findMonitorAndSettingsForProtocolAndBoard(pme, openReq.GetPort().GetProtocol(), openReq.GetFqbn())
if portConfig := req.GetPortConfiguration(); portConfig != nil { if err != nil {
return err
}
if err := monitor.Run(); err != nil {
return &cmderrors.FailedMonitorError{Cause: err}
}
if _, err := monitor.Describe(); err != nil {
monitor.Quit()
return &cmderrors.FailedMonitorError{Cause: err}
}
if portConfig := openReq.GetPortConfiguration(); portConfig != nil {
for _, setting := range portConfig.GetSettings() { for _, setting := range portConfig.GetSettings() {
boardSettings.Remove(setting.GetSettingId()) // Remove board settings overridden by the user boardSettings.Remove(setting.GetSettingId())
if err := m.Configure(setting.GetSettingId(), setting.GetValue()); err != nil { if err := monitor.Configure(setting.GetSettingId(), setting.GetValue()); err != nil {
logrus.Errorf("Could not set configuration %s=%s: %s", setting.GetSettingId(), setting.GetValue(), err) logrus.Errorf("Could not set configuration %s=%s: %s", setting.GetSettingId(), setting.GetValue(), err)
} }
} }
} }
// Apply specific board settings
for setting, value := range boardSettings.AsMap() { for setting, value := range boardSettings.AsMap() {
m.Configure(setting, value) monitor.Configure(setting, value)
} }
monitorIO, err := monitor.Open(openReq.GetPort().GetAddress(), openReq.GetPort().GetProtocol())
monIO, err := m.Open(req.GetPort().GetAddress(), req.GetPort().GetProtocol())
if err != nil { if err != nil {
m.Quit() monitor.Quit()
return nil, nil, &cmderrors.FailedMonitorError{Cause: err} return &cmderrors.FailedMonitorError{Cause: err}
} }
logrus.Infof("Port %s successfully opened", openReq.GetPort().GetAddress())
logrus.Infof("Port %s successfully opened", req.GetPort().GetAddress()) monitorClose := func() error {
return &portProxy{ monitor.Close()
rw: monIO, return monitor.Quit()
changeSettingsCB: m.Configure, }
closeCB: func() error {
m.Close() // Send a message with Success set to true to notify the caller of the port being now active
return m.Quit() syncSend := NewSynchronizedSend(stream.Send)
}, _ = syncSend.Send(&rpc.MonitorResponse{Success: true})
}, descriptor, nil
ctx, cancel := context.WithCancel(stream.Context())
gracefulCloseInitiated := &atomic.Bool{}
gracefuleCloseCtx, gracefulCloseCancel := context.WithCancel(context.Background())
// gRPC stream receiver (gRPC data -> monitor, config, close)
go func() {
defer cancel()
for {
msg, err := stream.Recv()
if errors.Is(err, io.EOF) {
return
}
if err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
return
}
if conf := msg.GetUpdatedConfiguration(); conf != nil {
for _, c := range conf.GetSettings() {
if err := monitor.Configure(c.GetSettingId(), c.GetValue()); err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
}
}
}
if closeMsg := msg.GetClose(); closeMsg {
gracefulCloseInitiated.Store(true)
if err := monitorClose(); err != nil {
logrus.WithError(err).Debug("Error closing monitor port")
}
gracefulCloseCancel()
}
tx := msg.GetTxData()
for len(tx) > 0 {
n, err := monitorIO.Write(tx)
if errors.Is(err, io.EOF) {
return
}
if err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
return
}
tx = tx[n:]
}
}
}()
// gRPC stream sender (monitor -> gRPC)
go func() {
defer cancel() // unlock the receiver
buff := make([]byte, 4096)
for {
n, err := monitorIO.Read(buff)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
break
}
if err := syncSend.Send(&rpc.MonitorResponse{RxData: buff[:n]}); err != nil {
break
}
}
}()
<-ctx.Done()
if gracefulCloseInitiated.Load() {
// Port closing has been initiated in the receiver
<-gracefuleCloseCtx.Done()
} else {
monitorClose()
}
return nil
} }
func findMonitorAndSettingsForProtocolAndBoard(pme *packagemanager.Explorer, protocol, fqbn string) (*pluggableMonitor.PluggableMonitor, *properties.Map, error) { func findMonitorAndSettingsForProtocolAndBoard(pme *packagemanager.Explorer, protocol, fqbn string) (*pluggableMonitor.PluggableMonitor, *properties.Map, error) {
......
...@@ -204,20 +204,6 @@ func runMonitorCmd( ...@@ -204,20 +204,6 @@ func runMonitorCmd(
} }
} }
} }
portProxy, _, err := commands.Monitor(context.Background(), &rpc.MonitorPortOpenRequest{
Instance: inst,
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
Fqbn: fqbn,
PortConfiguration: configuration,
})
if err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
}
defer portProxy.Close()
if !quiet {
feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress))
}
ttyIn, ttyOut, err := feedback.InteractiveStreams() ttyIn, ttyOut, err := feedback.InteractiveStreams()
if err != nil { if err != nil {
...@@ -228,7 +214,7 @@ func runMonitorCmd( ...@@ -228,7 +214,7 @@ func runMonitorCmd(
ttyOut = newTimeStampWriter(ttyOut) ttyOut = newTimeStampWriter(ttyOut)
} }
ctx, cancel := cleanup.InterruptableContext(context.Background()) ctx, cancel := cleanup.InterruptableContext(ctx)
if raw { if raw {
if feedback.IsInteractive() { if feedback.IsInteractive() {
if err := feedback.SetRawModeStdin(); err != nil { if err := feedback.SetRawModeStdin(); err != nil {
...@@ -246,6 +232,22 @@ func runMonitorCmd( ...@@ -246,6 +232,22 @@ func runMonitorCmd(
ttyIn = io.TeeReader(ttyIn, ctrlCDetector) ttyIn = io.TeeReader(ttyIn, ctrlCDetector)
} }
monitorServer, portProxy := commands.MonitorServerToReadWriteCloser(ctx, &rpc.MonitorPortOpenRequest{
Instance: inst,
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
Fqbn: fqbn,
PortConfiguration: configuration,
})
go func() {
if !quiet {
feedback.Print(tr("Connecting to %s. Press CTRL-C to exit.", portAddress))
}
if err := srv.Monitor(monitorServer); err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
}
portProxy.Close()
cancel()
}()
go func() { go func() {
_, err := io.Copy(ttyOut, portProxy) _, err := io.Copy(ttyOut, portProxy)
if err != nil && !errors.Is(err, io.EOF) { if err != nil && !errors.Is(err, io.EOF) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment