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

Appearance settings

feature: Added gRPC close signal to Monitor call (allows graceful close of monitor) #2276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 2, 2024
Prev Previous commit
Next Next commit
Added integration test
  • Loading branch information
cmaglie committed Jan 2, 2024
commit 62f3f1b8ee3e93f644b0f95acd00ae0c3db735d2
19 changes: 19 additions & 0 deletions 19 internal/integrationtest/arduino-cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,22 @@ func (inst *ArduinoCLIInstance) PlatformSearch(ctx context.Context, args string,
resp, err := inst.cli.daemonClient.PlatformSearch(ctx, req)
return resp, err
}

// Monitor calls the "Monitor" gRPC method and sends the OpenRequest message.
func (inst *ArduinoCLIInstance) Monitor(ctx context.Context, port *commands.Port) (commands.ArduinoCoreService_MonitorClient, error) {
req := &commands.MonitorRequest{}
logCallf(">>> Monitor(%+v)\n", req)
monitorClient, err := inst.cli.daemonClient.Monitor(ctx)
if err != nil {
return nil, err
}
err = monitorClient.Send(&commands.MonitorRequest{
Message: &commands.MonitorRequest_OpenRequest{
OpenRequest: &commands.MonitorPortOpenRequest{
Instance: inst.instance,
Port: port,
},
},
})
return monitorClient, err
}
116 changes: 116 additions & 0 deletions 116 internal/integrationtest/monitor/monitor_grpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// This file is part of arduino-cli.
//
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package monitor_test

import (
"context"
"fmt"
"io"
"regexp"
"testing"
"time"

"github.com/arduino/arduino-cli/internal/integrationtest"
"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
)

func TestMonitorGRPCClose(t *testing.T) {
// See: https://github.com/arduino/arduino-cli/issues/2271

env, cli := integrationtest.CreateEnvForDaemon(t)
defer env.CleanUp()

_, _, err := cli.Run("core", "install", "arduino:avr@1.8.6")
require.NoError(t, err)

cli.InstallMockedSerialDiscovery(t)
cli.InstallMockedSerialMonitor(t)

grpcInst := cli.Create()
require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
fmt.Printf("INIT> %v\n", ir.GetMessage())
}))

// Run a one-shot board list
boardListResp, err := grpcInst.BoardList(time.Second)
require.NoError(t, err)
ports := boardListResp.GetPorts()
require.NotEmpty(t, ports)
fmt.Printf("Got boardlist response with %d ports\n", len(ports))

// Open mocked serial-monitor and close it client-side
tmpFileMatcher := regexp.MustCompile("Tmpfile: (.*)\n")
{
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
mon, err := grpcInst.Monitor(ctx, ports[0].Port)
var tmpFile *paths.Path
for {
monResp, err := mon.Recv()
if err != nil {
fmt.Println("MON>", err)
break
}
fmt.Printf("MON> %v\n", monResp)
if rx := monResp.GetRxData(); rx != nil {
if matches := tmpFileMatcher.FindAllStringSubmatch(string(rx), -1); len(matches) > 0 {
fmt.Println("Found tmpFile", matches[0][1])
tmpFile = paths.New(matches[0][1])
}
}
}
require.NotNil(t, tmpFile)
// The port is close client-side, it may be still open server-side
require.True(t, tmpFile.Exist())
cancel()
require.NoError(t, err)
}

// Now close the monitor using MonitorRequest_Close
for tries := 0; tries < 5; tries++ { // Try the test 5 times to avoid flukes
// Keep a timeout to allow the test to exit in any case
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
mon, err := grpcInst.Monitor(ctx, ports[0].Port)
var tmpFile *paths.Path
for {
monResp, err := mon.Recv()
if err == io.EOF {
fmt.Println("MON>", err)
break
}

require.NoError(t, err)
fmt.Printf("MON> %v\n", monResp)
if rx := monResp.GetRxData(); rx != nil {
if matches := tmpFileMatcher.FindAllStringSubmatch(string(rx), -1); len(matches) > 0 {
fmt.Println("Found tmpFile", matches[0][1])
tmpFile = paths.New(matches[0][1])
go func() {
time.Sleep(time.Second)
fmt.Println("<MON Sent close command")
mon.Send(&commands.MonitorRequest{Message: &commands.MonitorRequest_Close{Close: true}})
}()
}
}
}
require.NotNil(t, tmpFile)
// The port is closed serverd-side, it must be already closed once the client has received the EOF
require.False(t, tmpFile.Exist())
cancel()
require.NoError(t, err)
}
}
15 changes: 15 additions & 0 deletions 15 internal/mock_serial_monitor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import (
"os"
"slices"
"strings"
"time"

"github.com/arduino/go-paths-helper"
monitor "github.com/arduino/pluggable-monitor-protocol-handler"
)

Expand All @@ -41,6 +43,7 @@ type SerialMonitor struct {
mockedSerialPort io.ReadWriteCloser
serialSettings *monitor.PortDescriptor
openedPort bool
muxFile *paths.Path
}

// NewSerialMonitor will initialize and return a SerialMonitor
Expand Down Expand Up @@ -129,9 +132,16 @@ func (d *SerialMonitor) Open(boardPort string) (io.ReadWriter, error) {
d.openedPort = true
sideA, sideB := newBidirectionalPipe()
d.mockedSerialPort = sideA
if muxFile, err := paths.MkTempFile(nil, ""); err == nil {
d.muxFile = paths.NewFromFile(muxFile)
muxFile.Close()
}
go func() {
buff := make([]byte, 1024)
d.mockedSerialPort.Write([]byte("Opened port: " + boardPort + "\n"))
if d.muxFile != nil {
d.mockedSerialPort.Write([]byte("Tmpfile: " + d.muxFile.String() + "\n"))
}
for parameter, descriptor := range d.serialSettings.ConfigurationParameter {
d.mockedSerialPort.Write([]byte(
fmt.Sprintf("Configuration %s = %s\n", parameter, descriptor.Selected)))
Expand Down Expand Up @@ -186,6 +196,11 @@ func (d *SerialMonitor) Close() error {
}
d.mockedSerialPort.Close()
d.openedPort = false
if d.muxFile != nil {
time.Sleep(500 * time.Millisecond) // Emulate a small delay closing the port to check gRPC synchronization
d.muxFile.Remove()
d.muxFile = nil
}
return nil
}

Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.