8000 Merge branch 'master' into scerza/fix-grpc-board-list-watch · arduino/arduino-cli@60790c9 · GitHub
[go: up one dir, main page]

Skip to content
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

Commit 60790c9

Browse files
authored
Merge branch 'master' into scerza/fix-grpc-board-list-watch
2 parents 5d3bbb7 + f759b08 commit 60790c9

File tree

9 files changed

+668
-4
lines changed

9 files changed

+668
-4
lines changed

arduino/monitor/monitor.go

Lines changed: 356 additions & 0 deletions
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+
}
10000 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)
0