Skip to content

Commit 1a3250b

Browse files
authored
Added implementation of pluggable monitor client (#1455)
1 parent 9f160e8 commit 1a3250b

File tree

6 files changed

+503
-4
lines changed

6 files changed

+503
-4
lines changed

arduino/monitor/monitor.go

+356
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package monitor
17+
18+
import (
19+
"encoding/json"
20+
"fmt"
21+
"io"
22+
"net"
23+
"strings"
24+
"sync"
25+
"time"
26+
27+
"github.com/arduino/arduino-cli/arduino/discovery"
28+
"github.com/arduino/arduino-cli/cli/globals"
29+
"github.com/arduino/arduino-cli/executils"
30+
"github.com/arduino/arduino-cli/i18n"
31+
"github.com/pkg/errors"
32+
"github.com/sirupsen/logrus"
33+
)
34+
35+
// To work correctly a Pluggable Monitor must respect the state machine specifed on the documentation:
36+
// https://arduino.github.io/arduino-cli/latest/pluggable-monitor-specification/#state-machine
37+
// States a PluggableMonitor can be in
38+
const (
39+
Alive int = iota
40+
Idle
41+
Opened
42+
Dead
43+
)
44+
45+
// PluggableMonitor is a tool that communicates with a board through a communication port.
46+
type PluggableMonitor struct {
47+
id string
48+
process *executils.Process
49+
outgoingCommandsPipe io.Writer
50+
incomingMessagesChan <-chan *monitorMessage
51+
supportedProtocol string
52+
53+
// All the following fields are guarded by statusMutex
54+
statusMutex sync.Mutex
55+
incomingMessagesError error
56+
state int
57+
}
58+
59+
type monitorMessage struct {
60+
EventType string `json:"eventType"`
61+
Message string `json:"message"`
62+
Error bool `json:"error"`
63+
ProtocolVersion int `json:"protocolVersion"` // Used in HELLO command
64+
PortDescription *PortDescriptor `json:"port_description,omitempty"`
65+
}
66+
67+
// PortDescriptor is a struct to describe the characteristic of a port
68+
type PortDescriptor struct {
69+
Protocol string `json:"protocol,omitempty"`
70+
ConfigurationParameters map[string]*PortParameterDescriptor `json:"configuration_parameters,omitempty"`
71+
}
72+
73+
// PortParameterDescriptor contains characteristics for every parameter
74+
type PortParameterDescriptor struct {
75+
Label string `json:"label,omitempty"`
76+
Type string `json:"type,omitempty"`
77+
Values []string `json:"value,omitempty"`
78+
Selected string `json:"selected,omitempty"`
79+
}
80+
81+
func (msg monitorMessage) String() string {
82+
s := fmt.Sprintf("type: %s", msg.EventType)
83+
if msg.Message != "" {
84+
s = fmt.Sprintf("%[1]s, message: %[2]s", s, msg.Message)
85+
}
86+
if msg.ProtocolVersion != 0 {
87+
s = fmt.Sprintf("%[1]s, protocol version: %[2]d", s, msg.ProtocolVersion)
88+
}
89+
if msg.PortDescription != nil {
90+
s = fmt.Sprintf("%s, port descriptor: protocol %s, %d parameters",
91+
s, msg.PortDescription.Protocol, len(msg.PortDescription.ConfigurationParameters))
92+
}
93+
return s
94+
}
95+
96+
var tr = i18n.Tr
97+
98+
// New create and connect to the given pluggable monitor
99+
func New(id string, args ...string) (*PluggableMonitor, error) {
100+
proc, err := executils.NewProcess(args...)
101+
if err != nil {
102+
return nil, err
103+
}
104+
stdout, err := proc.StdoutPipe()
105+
if err != nil {
106+
return nil, err
107+
}
108+
stdin, err := proc.StdinPipe()
109+
if err != nil {
110+
return nil, err
111+
}
112+
messageChan := make(chan *monitorMessage)
113+
disc := &PluggableMonitor{
114+
id: id,
115+
process: proc,
116+
incomingMessagesChan: messageChan,
117+
outgoingCommandsPipe: stdin,
118+
state: Dead,
119+
}
120+
go disc.jsonDecodeLoop(stdout, messageChan)
121+
return disc, nil
122+
}
123+
124+
// GetID returns the identifier for this monitor
125+
func (mon *PluggableMonitor) GetID() string {
126+
return mon.id
127+
}
128+
129+
func (mon *PluggableMonitor) String() string {
130+
return mon.id
131+
}
132+
133+
func (mon *PluggableMonitor) jsonDecodeLoop(in io.Reader, outChan chan<- *monitorMessage) {
134+
decoder := json.NewDecoder(in)
135+
closeAndReportError := func(err error) {
136+
mon.statusMutex.Lock()
137+
mon.state = Dead
138+
mon.incomingMessagesError = err
139+
close(outChan)
140+
mon.statusMutex.Unlock()
141+
logrus.Errorf("stopped monitor %s decode loop", mon.id)
142+
}
143+
144+
for {
145+
var msg monitorMessage
146+
if err := decoder.Decode(&msg); err != nil {
147+
closeAndReportError(err)
148+
return
149+
}
150+
logrus.Infof("from monitor %s received message %s", mon.id, msg)
151+
if msg.EventType == "port_closed" {
152+
mon.statusMutex.Lock()
153+
mon.state = Idle
154+
mon.statusMutex.Unlock()
155+
} else {
156+
outChan <- &msg
157+
}
158+
}
159+
}
160+
161+
// State returns the current state of this PluggableMonitor
162+
func (mon *PluggableMonitor) State() int {
163+
mon.statusMutex.Lock()
164+
defer mon.statusMutex.Unlock()
165+
return mon.state
166+
}
167+
168+
func (mon *PluggableMonitor) waitMessage(timeout time.Duration) (*monitorMessage, error) {
169+
select {
170+
case msg := <-mon.incomingMessagesChan:
171+
if msg == nil {
172+
// channel has been closed
173+
return nil, mon.incomingMessagesError
174+
}
175+
return msg, nil
176+
case <-time.After(timeout):
177+
return nil, fmt.Errorf(tr("timeout waiting for message from monitor %s"), mon.id)
178+
}
179+
}
180+
181+
func (mon *PluggableMonitor) sendCommand(command string) error {
182+
logrus.Infof("sending command %s to monitor %s", strings.TrimSpace(command), mon)
183+
data := []byte(command)
184+
for {
185+
n, err := mon.outgoingCommandsPipe.Write(data)
186+
if err != nil {
187+
return err
188+
}
189+
if n == len(data) {
190+
return nil
191+
}
192+
data = data[n:]
193+
}
194+
}
195+
196+
func (mon *PluggableMonitor) runProcess() error {
197+
logrus.Infof("starting monitor %s process", mon.id)
198+
if err := mon.process.Start(); err != nil {
199+
return err
200+
}
201+
mon.statusMutex.Lock()
202+
defer mon.statusMutex.Unlock()
203+
mon.state = Alive
204+
logrus.Infof("started monitor %s process", mon.id)
205+
return nil
206+
}
207+
208+
func (mon *PluggableMonitor) killProcess() error {
209+
logrus.Infof("killing monitor %s process", mon.id)
210+
if err := mon.process.Kill(); err != nil {
211+
return err
212+
}
213+
mon.statusMutex.Lock()
214+
defer mon.statusMutex.Unlock()
215+
mon.state = Dead
216+
logrus.Infof("killed monitor %s process", mon.id)
217+
return nil
218+
}
219+
220+
// Run starts the monitor executable process and sends the HELLO command to the monitor to agree on the
221+
// pluggable monitor protocol. This must be the first command to run in the communication with the monitor.
222+
// If the process is started but the HELLO command fails the process is killed.
223+
func (mon *PluggableMonitor) Run() (err error) {
224+
if err = mon.runProcess(); err != nil {
225+
return err
226+
}
227+
228+
defer func() {
229+
// If the monitor process is started successfully but the HELLO handshake
230+
// fails the monitor is an unusable state, we kill the process to avoid
231+
// further issues down the line.
232+
if err == nil {
233+
return
234+
}
235+
if killErr := mon.killProcess(); killErr != nil {
236+
// Log failure to kill the process, ideally that should never happen
237+
// but it's best to know it if it does
238+
logrus.Errorf("Killing monitor %s after unsuccessful start: %s", mon.id, killErr)
239+
}
240+
}()
241+
242+
if err = mon.sendCommand("HELLO 1 \"arduino-cli " + globals.VersionInfo.VersionString + "\"\n"); err != nil {
243+
return err
244+
}
245+
if msg, err := mon.waitMessage(time.Second * 10); err != nil {
246+
return fmt.Errorf(tr("calling %[1]s: %[2]w"), "HELLO", err)
247+
} else if msg.EventType != "hello" {
248+
return errors.Errorf(tr("communication out of sync, expected 'hello', received '%s'"), msg.EventType)
249+
} else if msg.Message != "OK" || msg.Error {
250+
return errors.Errorf(tr("command failed: %s"), msg.Message)
251+
} else if msg.ProtocolVersion > 1 {
252+
return errors.Errorf(tr("protocol version not supported: requested 1, got %d"), msg.ProtocolVersion)
253+
}
254+
mon.statusMutex.Lock()
255+
defer mon.statusMutex.Unlock()
256+
mon.state = Idle
257+
return nil
258+
}
259+
260+
// Describe returns a description of the Port and the configuration parameters.
261+
func (mon *PluggableMonitor) Describe() (*PortDescriptor, error) {
262+
if err := mon.sendCommand("DESCRIBE\n"); err != nil {
263+
return nil, err
264+
}
265+
if msg, err := mon.waitMessage(time.Second * 10); err != nil {
266+
return nil, fmt.Errorf("calling %s: %w", "", err)
267+
} else if msg.EventType != "describe" {
268+
return nil, errors.Errorf(tr("communication out of sync, expected 'describe', received '%s'"), msg.EventType)
269+
} else if msg.Message != "OK" || msg.Error {
270+
return nil, errors.Errorf(tr("command failed: %s"), msg.Message)
271+
} else {
272+
mon.supportedProtocol = msg.PortDescription.Protocol
273+
return msg.PortDescription, nil
274+
}
275+
}
276+
277+
// Configure sets a port configuration parameter.
278+
func (mon *PluggableMonitor) Configure(param, value string) error {
279+
if err := mon.sendCommand(fmt.Sprintf("CONFIGURE %s %s\n", param, value)); err != nil {
280+
return err
281+
}
282+
if msg, err := mon.waitMessage(time.Second * 10); err != nil {
283+
return fmt.Errorf("calling %s: %w", "", err)
284+
} else if msg.EventType != "configure" {
285+
return errors.Errorf(tr("communication out of sync, expected 'configure', received '%s'"), msg.EventType)
286+
} else if msg.Message != "OK" || msg.Error {
287+
return errors.Errorf(tr("command failed: %s"), msg.Message)
288+
} else {
289+
return nil
290+
}
291+
}
292+
293+
// Open connects to the given Port. A communication channel is opened
294+
func (mon *PluggableMonitor) Open(port *discovery.Port) (io.ReadWriter, error) {
295+
mon.statusMutex.Lock()
296+
defer mon.statusMutex.Unlock()
297+
298+
if mon.state == Opened {
299+
return nil, fmt.Errorf("a port is already opened")
300+
}
301+
if mon.state != Idle {
302+
return nil, fmt.Errorf("the monitor is not started")
303+
}
304+
if port.Protocol != mon.supportedProtocol {
305+
return nil, fmt.Errorf("invalid monitor protocol '%s': only '%s' is accepted", port.Protocol, mon.supportedProtocol)
306+
}
307+
308+
tcpListener, err := net.Listen("tcp", "127.0.0.1:")
309+
if err != nil {
310+
return nil, err
311+
}
312+
defer tcpListener.Close()
313+
tcpListenerPort := tcpListener.Addr().(*net.TCPAddr).Port
314+
315+
if err := mon.sendCommand(fmt.Sprintf("OPEN 127.0.0.1:%d %s\n", tcpListenerPort, port.Address)); err != nil {
316+
return nil, err
317+
}
318+
if msg, err := mon.waitMessage(time.Second * 10); err != nil {
319+
return nil, fmt.Errorf("calling %s: %w", "", err)
320+
} else if msg.EventType != "open" {
321+
return nil, errors.Errorf(tr("communication out of sync, expected 'open', received '%s'"), msg.EventType)
322+
} else if msg.Message != "OK" || msg.Error {
323+
return nil, errors.Errorf(tr("command failed: %s"), msg.Message)
324+
}
325+
326+
conn, err := tcpListener.Accept()
327+
if err != nil {
328+
return nil, err // TODO
329+
}
330+
331+
mon.state = Opened
332+
return conn, nil
333+
}
334+
335+
// Close the communication port with the board.
336+
func (mon *PluggableMonitor) Close() error {
337+
mon.statusMutex.Lock()
338+
defer mon.statusMutex.Unlock()
339+
340+
if mon.state != Opened {
341+
return fmt.Errorf("monitor port already closed")
342+
}
343+
if err := mon.sendCommand("CLOSE\n"); err != nil {
344+
return err
345+
}
346+
if msg, err := mon.waitMessage(time.Second * 10); err != nil {
347+
return fmt.Errorf("calling %s: %w", "", err)
348+
} else if msg.EventType != "close" {
349+
return errors.Errorf(tr("communication out of sync, expected 'close', received '%s'"), msg.EventType)
350+
} else if msg.Message != "OK" || msg.Error {
351+
return errors.Errorf(tr("command failed: %s"), msg.Message)
352+
} else {
353+
mon.state = Idle
354+
return nil
355+
}
356+
}

0 commit comments

Comments
 (0)