Skip to content

Commit 0dbd871

Browse files
authored
[breaking] feature: Added gRPC close signal to Monitor call (allows graceful close of monitor) (#2276)
* Refactored gRPC Monitor API * Added Close request to gRPC Monitor API * Updated docs * Made CreateEnvForDaeamon available in all integration tests * Added integration test * lint: avoid redefinition of the built-in function close * lint: comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error * Allow up to 5 seconds for a pluggable monitor to gracefully close * Made the gRPC daemon actually wait for port close completion
1 parent 07cf265 commit 0dbd871

File tree

13 files changed

+572
-181
lines changed

13 files changed

+572
-181
lines changed

commands/daemon/daemon.go

+26-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"errors"
2121
"fmt"
2222
"io"
23+
"sync/atomic"
2324

2425
"github.com/arduino/arduino-cli/commands"
2526
"github.com/arduino/arduino-cli/commands/board"
@@ -477,7 +478,11 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
477478
return err
478479
}
479480

480-
portProxy, _, err := monitor.Monitor(stream.Context(), req)
481+
openReq := req.GetOpenRequest()
482+
if openReq == nil {
483+
return &cmderrors.InvalidInstanceError{}
484+
}
485+
portProxy, _, err := monitor.Monitor(stream.Context(), openReq)
481486
if err != nil {
482487
return err
483488
}
@@ -486,6 +491,10 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
486491
_ = syncSend.Send(&rpc.MonitorResponse{Success: true})
487492

488493
cancelCtx, cancel := context.WithCancel(stream.Context())
494+
gracefulCloseInitiated := &atomic.Bool{}
495+
gracefuleCloseCtx, gracefulCloseCancel := context.WithCancel(context.Background())
496+
497+
// gRPC stream receiver (gRPC data -> monitor, config, close)
489498
go func() {
490499
defer cancel()
491500
for {
@@ -497,13 +506,20 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
497506
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
498507
return
499508
}
500-
if conf := msg.GetPortConfiguration(); conf != nil {
509+
if conf := msg.GetUpdatedConfiguration(); conf != nil {
501510
for _, c := range conf.GetSettings() {
502511
if err := portProxy.Config(c.GetSettingId(), c.GetValue()); err != nil {
503512
syncSend.Send(&rpc.MonitorResponse{Error: err.Error()})
504513
}
505514
}
506515
}
516+
if closeMsg := msg.GetClose(); closeMsg {
517+
gracefulCloseInitiated.Store(true)
518+
if err := portProxy.Close(); err != nil {
519+
logrus.WithError(err).Debug("Error closing monitor port")
520+
}
521+
gracefulCloseCancel()
522+
}
507523
tx := msg.GetTxData()
508524
for len(tx) > 0 {
509525
n, err := portProxy.Write(tx)
@@ -519,8 +535,9 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
519535
}
520536
}()
521537

538+
// gRPC stream sender (monitor -> gRPC)
522539
go func() {
523-
defer cancel()
540+
defer cancel() // unlock the receiver
524541
buff := make([]byte, 4096)
525542
for {
526543
n, err := portProxy.Read(buff)
@@ -538,6 +555,11 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
538555
}()
539556

540557
<-cancelCtx.Done()
541-
portProxy.Close()
558+
if gracefulCloseInitiated.Load() {
559+
// Port closing has been initiated in the receiver
560+
<-gracefuleCloseCtx.Done()
561+
} else {
562+
portProxy.Close()
563+
}
542564
return nil
543565
}

commands/daemon/term_example/main.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ func connectToPort(cli commands.ArduinoCoreServiceClient, instance *commands.Ins
8989
log.Fatal("Error opening Monitor:", err)
9090
}
9191
if err := monitorClient.Send(&commands.MonitorRequest{
92-
Instance: instance,
93-
Port: port,
92+
Message: &commands.MonitorRequest_OpenRequest{OpenRequest: &commands.MonitorPortOpenRequest{
93+
Instance: instance,
94+
Port: port,
95+
}},
9496
}); err != nil {
9597
log.Fatal("Error sending Monitor config:", err)
9698
}
@@ -106,9 +108,9 @@ func connectToPort(cli commands.ArduinoCoreServiceClient, instance *commands.Ins
106108
}
107109
}()
108110

109-
hello := &commands.MonitorRequest{
111+
hello := &commands.MonitorRequest{Message: &commands.MonitorRequest_TxData{
110112
TxData: []byte("HELLO!"),
111-
}
113+
}}
112114
fmt.Println("Send:", hello)
113115
if err := monitorClient.Send(hello); err != nil {
114116
log.Fatal("Monitor send HELLO:", err)

commands/monitor/monitor.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (p *PortProxy) Close() error {
6060

6161
// Monitor opens a communication port. It returns a PortProxy to communicate with the port and a PortDescriptor
6262
// that describes the available configuration settings.
63-
func Monitor(ctx context.Context, req *rpc.MonitorRequest) (*PortProxy, *pluggableMonitor.PortDescriptor, error) {
63+
func Monitor(ctx context.Context, req *rpc.MonitorPortOpenRequest) (*PortProxy, *pluggableMonitor.PortDescriptor, error) {
6464
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
6565
if err != nil {
6666
return nil, nil, err

docs/UPGRADING.md

+59
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,65 @@ Here you can find a list of migration guides to handle breaking changes between
88

99
We're dropping the `builtin.tools` support. It was the equivalent of Arduino IDE 1.x bundled tools directory.
1010

11+
### The gRPC `cc.arduino.cli.commands.v1.MonitorRequest` message has been changed.
12+
13+
Previously the `MonitorRequest` was a single message used to open the monitor, to stream data, and to change the port
14+
configuration:
15+
16+
```proto
17+
message MonitorRequest {
18+
// Arduino Core Service instance from the `Init` response.
19+
Instance instance = 1;
20+
// Port to open, must be filled only on the first request
21+
Port port = 2;
22+
// The board FQBN we are trying to connect to. This is optional, and it's
23+
// needed to disambiguate if more than one platform provides the pluggable
24+
// monitor for a given port protocol.
25+
string fqbn = 3;
26+
// Data to send to the port
27+
bytes tx_data = 4;
28+
// Port configuration, optional, contains settings of the port to be applied
29+
MonitorPortConfiguration port_configuration = 5;
30+
}
31+
```
32+
33+
Now the meaning of the fields has been clarified with the `oneof` clause, making it more explicit:
34+
35+
```proto
36+
message MonitorRequest {
37+
oneof message {
38+
// Open request, it must be the first incoming message
39+
MonitorPortOpenRequest open_request = 1;
40+
// Data to send to the port
41+
bytes tx_data = 2;
42+
// Port configuration, contains settings of the port to be changed
43+
MonitorPortConfiguration updated_configuration = 3;
44+
// Close message, set to true to gracefully close a port (this ensure
45+
// that the gRPC streaming call is closed by the daemon AFTER the port
46+
// has been successfully closed)
47+
bool close = 4;
48+
}
49+
}
50+
51+
message MonitorPortOpenRequest {
52+
// Arduino Core Service instance from the `Init` response.
53+
Instance instance = 1;
54+
// Port to open, must be filled only on the first request
55+
Port port = 2;
56+
// The board FQBN we are trying to connect to. This is optional, and it's
57+
// needed to disambiguate if more than one platform provides the pluggable
58+
// monitor for a given port protocol.
59+
string fqbn = 3;
60+
// Port configuration, optional, contains settings of the port to be applied
61+
MonitorPortConfiguration port_configuration = 4;
62+
}
63+
```
64+
65+
Now the message field `MonitorPortOpenRequest.open_request` must be sent in the first message after opening the
66+
streaming gRPC call.
67+
68+
The identification number of the fields has been changed, this change is not binary compatible with old clients.
69+
1170
### Some golang modules from `github.com/arduino/arduino-cli/*` have been made private.
1271

1372
The following golang modules are no longer available as public API:

internal/arduino/monitor/monitor.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ func (mon *PluggableMonitor) Close() error {
292292
if err := mon.sendCommand("CLOSE\n"); err != nil {
293293
return err
294294
}
295-
_, err := mon.waitMessage(time.Millisecond*250, "close")
295+
_, err := mon.waitMessage(time.Millisecond*5000, "close")
296296
return err
297297
}
298298

internal/cli/monitor/monitor.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func runMonitorCmd(
203203
}
204204
}
205205
}
206-
portProxy, _, err := monitor.Monitor(context.Background(), &rpc.MonitorRequest{
206+
portProxy, _, err := monitor.Monitor(context.Background(), &rpc.MonitorPortOpenRequest{
207207
Instance: inst,
208208
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
209209
Fqbn: fqbn,

internal/integrationtest/arduino-cli.go

+34
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ func NewArduinoCliWithinEnvironment(env *Environment, config *ArduinoCLIConfig)
119119
return cli
120120
}
121121

122+
// CreateEnvForDaemon performs the minimum required operations to start the arduino-cli daemon.
123+
// It returns a testsuite.Environment and an ArduinoCLI client to perform the integration tests.
124+
// The Environment must be disposed by calling the CleanUp method via defer.
125+
func CreateEnvForDaemon(t *testing.T) (*Environment, *ArduinoCLI) {
126+
env := NewEnvironment(t)
127+
128+
cli := NewArduinoCliWithinEnvironment(env, &ArduinoCLIConfig{
129+
ArduinoCLIPath: FindRepositoryRootPath(t).Join("arduino-cli"),
130+
UseSharedStagingFolder: true,
131+
})
132+
133+
_ = cli.StartDaemon(false)
134+
return env, cli
135+
}
136+
122137
// CleanUp closes the Arduino CLI client.
123138
func (cli *ArduinoCLI) CleanUp() {
124139
if cli.proc != nil {
@@ -596,3 +611,22 @@ func (inst *ArduinoCLIInstance) PlatformSearch(ctx context.Context, args string,
596611
resp, err := inst.cli.daemonClient.PlatformSearch(ctx, req)
597612
return resp, err
598613
}
614+
615+
// Monitor calls the "Monitor" gRPC method and sends the OpenRequest message.
616+
func (inst *ArduinoCLIInstance) Monitor(ctx context.Context, port *commands.Port) (commands.ArduinoCoreService_MonitorClient, error) {
617+
req := &commands.MonitorRequest{}
618+
logCallf(">>> Monitor(%+v)\n", req)
619+
monitorClient, err := inst.cli.daemonClient.Monitor(ctx)
620+
if err != nil {
621+
return nil, err
622+
}
623+
err = monitorClient.Send(&commands.MonitorRequest{
624+
Message: &commands.MonitorRequest_OpenRequest{
625+
OpenRequest: &commands.MonitorPortOpenRequest{
626+
Instance: inst.instance,
627+
Port: port,
628+
},
629+
},
630+
})
631+
return monitorClient, err
632+
}

internal/integrationtest/daemon/daemon_concurrency_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"testing"
2424
"time"
2525

26+
"github.com/arduino/arduino-cli/internal/integrationtest"
2627
"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
2728
"github.com/arduino/go-paths-helper"
2829
"github.com/stretchr/testify/require"
@@ -31,7 +32,7 @@ import (
3132
func TestArduinoCliDaemonCompileWithLotOfOutput(t *testing.T) {
3233
// See: https://github.com/arduino/arduino-cli/issues/2169
3334

34-
env, cli := createEnvForDaemon(t)
35+
env, cli := integrationtest.CreateEnvForDaemon(t)
3536
defer env.CleanUp()
3637

3738
_, _, err := cli.Run("core", "install", "arduino:avr")

internal/integrationtest/daemon/daemon_test.go

+11-26
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import (
3737
func TestArduinoCliDaemon(t *testing.T) {
3838
// See: https://github.com/arduino/arduino-cli/pull/1804
3939

40-
env, cli := createEnvForDaemon(t)
40+
env, cli := integrationtest.CreateEnvForDaemon(t)
4141
defer env.CleanUp()
4242

4343
grpcInst := cli.Create()
@@ -96,7 +96,7 @@ func TestArduinoCliDaemon(t *testing.T) {
9696
func TestDaemonAutoUpdateIndexOnFirstInit(t *testing.T) {
9797
// https://github.com/arduino/arduino-cli/issues/1529
9898

99-
env, cli := createEnvForDaemon(t)
99+
env, cli := integrationtest.CreateEnvForDaemon(t)
100100
defer env.CleanUp()
101101

102102
grpcInst := cli.Create()
@@ -110,26 +110,11 @@ func TestDaemonAutoUpdateIndexOnFirstInit(t *testing.T) {
110110
require.FileExists(t, cli.DataDir().Join("package_index.json").String())
111111
}
112112

113-
// createEnvForDaemon performs the minimum required operations to start the arduino-cli daemon.
114-
// It returns a testsuite.Environment and an ArduinoCLI client to perform the integration tests.
115-
// The Environment must be disposed by calling the CleanUp method via defer.
116-
func createEnvForDaemon(t *testing.T) (*integrationtest.Environment, *integrationtest.ArduinoCLI) {
117-
env := integrationtest.NewEnvironment(t)
118-
119-
cli := integrationtest.NewArduinoCliWithinEnvironment(env, &integrationtest.ArduinoCLIConfig{
120-
ArduinoCLIPath: integrationtest.FindRepositoryRootPath(t).Join("arduino-cli"),
121-
UseSharedStagingFolder: true,
122-
})
123-
124-
_ = cli.StartDaemon(false)
125-
return env, cli
126-
}
127-
128113
func TestDaemonCompileOptions(t *testing.T) {
129114
// See: https://github.com/arduino/arduino-cli/issues/1614
130115
// See: https://github.com/arduino/arduino-cli/pull/1820
131116

132-
env, cli := createEnvForDaemon(t)
117+
env, cli := integrationtest.CreateEnvForDaemon(t)
133118
defer env.CleanUp()
134119

135120
grpcInst := cli.Create()
@@ -203,7 +188,7 @@ func TestDaemonCompileOptions(t *testing.T) {
203188
func TestDaemonCompileAfterFailedLibInstall(t *testing.T) {
204189
// See: https://github.com/arduino/arduino-cli/issues/1812
205190

206-
env, cli := createEnvForDaemon(t)
191+
env, cli := integrationtest.CreateEnvForDaemon(t)
207192
defer env.CleanUp()
208193

209194
grpcInst := cli.Create()
@@ -233,7 +218,7 @@ func TestDaemonCompileAfterFailedLibInstall(t *testing.T) {
233218
}
234219

235220
func TestDaemonCoreUpdateIndex(t *testing.T) {
236-
env, cli := createEnvForDaemon(t)
221+
env, cli := integrationtest.CreateEnvForDaemon(t)
237222
defer env.CleanUp()
238223

239224
grpcInst := cli.Create()
@@ -269,7 +254,7 @@ func TestDaemonCoreUpdateIndex(t *testing.T) {
269254
}
270255

271256
func TestDaemonBundleLibInstall(t *testing.T) {
272-
env, cli := createEnvForDaemon(t)
257+
env, cli := integrationtest.CreateEnvForDaemon(t)
273258
defer env.CleanUp()
274259

275260
grpcInst := cli.Create()
@@ -409,7 +394,7 @@ func TestDaemonLibrariesRescanOnInstall(t *testing.T) {
409394
with the gprc instance
410395
The last attempt is expected to not raise an error
411396
*/
412-
env, cli := createEnvForDaemon(t)
397+
env, cli := integrationtest.CreateEnvForDaemon(t)
413398
defer env.CleanUp()
414399

415400
grpcInst := cli.Create()
@@ -465,7 +450,7 @@ func TestDaemonCoreUpgradePlatform(t *testing.T) {
465450

466451
t.Run("upgraded successfully with additional urls", func(t *testing.T) {
467452
t.Run("and install.json is present", func(t *testing.T) {
468-
env, cli := createEnvForDaemon(t)
453+
env, cli := integrationtest.CreateEnvForDaemon(t)
469454
defer env.CleanUp()
470455

471456
grpcInst := cli.Create()
@@ -481,7 +466,7 @@ func TestDaemonCoreUpgradePlatform(t *testing.T) {
481466
require.False(t, platform.GetRelease().GetMissingMetadata()) // install.json is present
482467
})
483468
t.Run("and install.json is missing", func(t *testing.T) {
484-
env, cli := createEnvForDaemon(t)
469+
env, cli := integrationtest.CreateEnvForDaemon(t)
485470
defer env.CleanUp()
486471

487472
grpcInst := cli.Create()
@@ -504,7 +489,7 @@ func TestDaemonCoreUpgradePlatform(t *testing.T) {
504489

505490
t.Run("upgrade failed", func(t *testing.T) {
506491
t.Run("without additional URLs", func(t *testing.T) {
507-
env, cli := createEnvForDaemon(t)
492+
env, cli := integrationtest.CreateEnvForDaemon(t)
508493
defer env.CleanUp()
509494

510495
grpcInst := cli.Create()
@@ -524,7 +509,7 @@ func TestDaemonCoreUpgradePlatform(t *testing.T) {
524509
require.False(t, platform.GetRelease().GetMissingMetadata()) // install.json is present
525510
})
526511
t.Run("missing both additional URLs and install.json", func(t *testing.T) {
527-
env, cli := createEnvForDaemon(t)
512+
env, cli := integrationtest.CreateEnvForDaemon(t)
528513
defer env.CleanUp()
529514

530515
grpcInst := cli.Create()

0 commit comments

Comments
 (0)