Skip to content

Commit ed4e465

Browse files
lucarin91cmagliedido18
authored
feat: add monitor command (#66)
* feat: add bridge monitor command * Update internal/monitor/monitor.go Co-authored-by: Cristian Maglie <[email protected]> * improve description * apply code review suggestions * propertly implement the internal function * handle also text type * Update internal/monitor/monitor.go Co-authored-by: Davide <[email protected]> * Update internal/monitor/monitor.go Co-authored-by: Davide <[email protected]> * Update internal/monitor/monitor.go Co-authored-by: Davide <[email protected]> * add monitor test * fixup! add monitor test * fixup! fixup! add monitor test * fixup! fixup! fixup! add monitor test * fixup! fixup! fixup! fixup! add monitor test --------- Co-authored-by: Cristian Maglie <[email protected]> Co-authored-by: Davide <[email protected]>
1 parent f020963 commit ed4e465

File tree

6 files changed

+272
-88
lines changed

6 files changed

+272
-88
lines changed

cmd/arduino-app-cli/app/app.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ func NewAppCmd(cfg config.Configuration) *cobra.Command {
3838
appCmd.AddCommand(newRestartCmd(cfg))
3939
appCmd.AddCommand(newLogsCmd(cfg))
4040
appCmd.AddCommand(newListCmd(cfg))
41-
appCmd.AddCommand(newMonitorCmd(cfg))
4241
appCmd.AddCommand(newCacheCleanCmd(cfg))
4342

4443
return appCmd

cmd/arduino-app-cli/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/config"
3131
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/daemon"
3232
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/internal/servicelocator"
33+
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/monitor"
3334
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/properties"
3435
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/system"
3536
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/version"
@@ -78,6 +79,7 @@ func run(configuration cfg.Configuration) error {
7879
config.NewConfigCmd(configuration),
7980
system.NewSystemCmd(configuration),
8081
version.NewVersionCmd(Version),
82+
monitor.NewMonitorCmd(),
8183
)
8284

8385
ctx := context.Background()
Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,51 @@
1313
// Arduino software without disclosing the source code of your own applications.
1414
// To purchase a commercial license, send an email to [email protected].
1515

16-
package app
16+
package monitor
1717

1818
import (
19+
"io"
20+
"os"
21+
1922
"github.com/spf13/cobra"
2023

21-
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/completion"
22-
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
24+
"github.com/arduino/arduino-app-cli/cmd/feedback"
25+
"github.com/arduino/arduino-app-cli/internal/monitor"
2326
)
2427

25-
func newMonitorCmd(cfg config.Configuration) *cobra.Command {
28+
func NewMonitorCmd() *cobra.Command {
2629
return &cobra.Command{
2730
Use: "monitor",
28-
Short: "Monitor the Arduino app",
31+
Short: "Attach to the microcontroller serial monitor",
2932
RunE: func(cmd *cobra.Command, args []string) error {
30-
panic("not implemented")
33+
stdout, _, err := feedback.DirectStreams()
34+
if err != nil {
35+
return err
36+
}
37+
start, err := monitor.NewMonitorHandler(&combinedReadWrite{r: os.Stdin, w: stdout}) // nolint:forbidigo
38+
if err != nil {
39+
return err
40+
}
41+
go start()
42+
<-cmd.Context().Done()
43+
return nil
3144
},
32-
ValidArgsFunction: completion.ApplicationNames(cfg),
3345
}
3446
}
47+
48+
type combinedReadWrite struct {
49+
r io.Reader
50+
w io.Writer
51+
}
52+
53+
func (crw *combinedReadWrite) Read(p []byte) (n int, err error) {
54+
return crw.r.Read(p)
55+
}
56+
57+
func (crw *combinedReadWrite) Write(p []byte) (n int, err error) {
58+
return crw.w.Write(p)
59+
}
60+
61+
func (crw *combinedReadWrite) Close() error {
62+
return nil
63+
}

internal/api/handlers/monitor.go

Lines changed: 65 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,71 +16,50 @@
1616
package handlers
1717

1818
import (
19-
"errors"
2019
"fmt"
21-
"io"
2220
"log/slog"
2321
"net"
2422
"net/http"
2523
"strings"
26-
"time"
2724

2825
"github.com/gorilla/websocket"
2926

3027
"github.com/arduino/arduino-app-cli/internal/api/models"
28+
"github.com/arduino/arduino-app-cli/internal/monitor"
3129
"github.com/arduino/arduino-app-cli/internal/render"
3230
)
3331

34-
func monitorStream(mon net.Conn, ws *websocket.Conn) {
35-
logWebsocketError := func(msg string, err error) {
36-
// Do not log simple close or interruption errors
37-
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
38-
if e, ok := err.(*websocket.CloseError); ok {
39-
slog.Error(msg, slog.String("closecause", fmt.Sprintf("%d: %s", e.Code, err)))
40-
} else {
41-
slog.Error(msg, slog.String("error", err.Error()))
42-
}
43-
}
44-
}
45-
logSocketError := func(msg string, err error) {
46-
if !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
47-
slog.Error(msg, slog.String("error", err.Error()))
48-
}
32+
func HandleMonitorWS(allowedOrigins []string) http.HandlerFunc {
33+
upgrader := websocket.Upgrader{
34+
ReadBufferSize: 1024,
35+
WriteBufferSize: 1024,
36+
CheckOrigin: func(r *http.Request) bool {
37+
return checkOrigin(r.Header.Get("Origin"), allowedOrigins)
38+
},
4939
}
50-
go func() {
51-
defer mon.Close()
52-
defer ws.Close()
53-
for {
54-
// Read from websocket and write to monitor
55-
_, msg, err := ws.ReadMessage()
56-
if err != nil {
57-
logWebsocketError("Error reading from websocket", err)
58-
return
59-
}
60-
if _, err := mon.Write(msg); err != nil {
61-
logSocketError("Error writing to monitor", err)
62-
return
63-
}
40+
41+
return func(w http.ResponseWriter, r *http.Request) {
42+
// Upgrade the connection to websocket
43+
conn, err := upgrader.Upgrade(w, r, nil)
44+
if err != nil {
45+
// Remember to close monitor connection if websocket upgrade fails.
46+
47+
slog.Error("Failed to upgrade connection", slog.String("error", err.Error()))
48+
render.EncodeResponse(w, http.StatusInternalServerError, map[string]string{"error": "Failed to upgrade connection: " + err.Error()})
49+
return
6450
}
65-
}()
66-
go func() {
67-
defer mon.Close()
68-
defer ws.Close()
69-
buff := [1024]byte{}
70-
for {
71-
// Read from monitor and write to websocket
72-
n, err := mon.Read(buff[:])
73-
if err != nil {
74-
logSocketError("Error reading from monitor", err)
75-
return
76-
}
77-
78-
if err := ws.WriteMessage(websocket.BinaryMessage, buff[:n]); err != nil {
79-
logWebsocketError("Error writing to websocket", err)
80-
return
81-
}
51+
52+
// Now the connection is managed by the websocket library, let's move the handlers in the goroutine
53+
start, err := monitor.NewMonitorHandler(&wsReadWriteCloser{conn: conn})
54+
if err != nil {
55+
slog.Error("Unable to start monitor handler", slog.String("error", err.Error()))
56+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "Unable to start monitor handler: " + err.Error()})
57+
return
8258
}
83-
}()
59+
go start()
60+
61+
// and return nothing to the http library
62+
}
8463
}
8564

8665
func splitOrigin(origin string) (scheme, host, port string, err error) {
@@ -126,41 +105,47 @@ func checkOrigin(origin string, allowedOrigins []string) bool {
126105
return false
127106
}
128107

129-
func HandleMonitorWS(allowedOrigins []string) http.HandlerFunc {
130-
// Do a dry-run of checkorigin, so it can panic if misconfigured now, not on first request
131-
_ = checkOrigin("http://localhost", allowedOrigins)
108+
type wsReadWriteCloser struct {
109+
conn *websocket.Conn
132110

133-
upgrader := websocket.Upgrader{
134-
ReadBufferSize: 1024,
135-
WriteBufferSize: 1024,
136-
CheckOrigin: func(r *http.Request) bool {
137-
return checkOrigin(r.Header.Get("Origin"), allowedOrigins)
138-
},
139-
}
111+
buff []byte
112+
}
140113

141-
return func(w http.ResponseWriter, r *http.Request) {
142-
// Connect to monitor
143-
mon, err := net.DialTimeout("tcp", "127.0.0.1:7500", time.Second)
144-
if err != nil {
145-
slog.Error("Unable to connect to monitor", slog.String("error", err.Error()))
146-
render.EncodeResponse(w, http.StatusServiceUnavailable, models.ErrorResponse{Details: "Unable to connect to monitor: " + err.Error()})
147-
return
148-
}
114+
func (w *wsReadWriteCloser) Read(p []byte) (n int, err error) {
115+
if len(w.buff) > 0 {
116+
n = copy(p, w.buff)
117+
w.buff = w.buff[n:]
118+
return n, nil
119+
}
149120

150-
// Upgrade the connection to websocket
151-
conn, err := upgrader.Upgrade(w, r, nil)
152-
if err != nil {
153-
// Remember to close monitor connection if websocket upgrade fails.
154-
mon.Close()
121+
ty, message, err := w.conn.ReadMessage()
122+
if err != nil {
123+
return 0, mapWebSocketErrors(err)
124+
}
125+
if ty != websocket.BinaryMessage && ty != websocket.TextMessage {
126+
return
127+
}
128+
n = copy(p, message)
129+
w.buff = message[n:]
130+
return n, nil
131+
}
155132

156-
slog.Error("Failed to upgrade connection", slog.String("error", err.Error()))
157-
render.EncodeResponse(w, http.StatusInternalServerError, map[string]string{"error": "Failed to upgrade connection: " + err.Error()})
158-
return
159-
}
133+
func (w *wsReadWriteCloser) Write(p []byte) (n int, err error) {
134+
err = w.conn.WriteMessage(websocket.BinaryMessage, p)
135+
if err != nil {
136+
return 0, mapWebSocketErrors(err)
137+
}
138+
return len(p), nil
139+
}
160140

161-
// Now the connection is managed by the websocket library, let's move the handlers in the goroutine
162-
go monitorStream(mon, conn)
141+
func (w *wsReadWriteCloser) Close() error {
142+
w.buff = nil
143+
return w.conn.Close()
144+
}
163145

164-
// and return nothing to the http library
146+
func mapWebSocketErrors(err error) error {
147+
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
148+
return net.ErrClosed
165149
}
150+
return err
166151
}

internal/monitor/monitor.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// This file is part of arduino-app-cli.
2+
//
3+
// Copyright 2025 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-app-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 [email protected].
15+
16+
package monitor
17+
18+
import (
19+
"errors"
20+
"io"
21+
"log/slog"
22+
"net"
23+
"time"
24+
25+
"go.bug.st/f"
26+
)
27+
28+
const defaultArduinoRouterMonitorAddress = "127.0.0.1:7500"
29+
30+
func NewMonitorHandler(rw io.ReadWriteCloser, address ...string) (func(), error) {
31+
f.Assert(len(address) <= 1, "NewMonitorHandler accepts at most one address argument")
32+
33+
addr := defaultArduinoRouterMonitorAddress
34+
if len(address) == 1 {
35+
addr = address[0]
36+
}
37+
38+
// Connect to monitor
39+
monitor, err := net.DialTimeout("tcp", addr, time.Second)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
return func() {
45+
monitorStream(monitor, rw)
46+
}, nil
47+
}
48+
49+
func monitorStream(mon net.Conn, rw io.ReadWriteCloser) {
50+
logSocketError := func(msg string, err error) {
51+
if !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
52+
slog.Error(msg, slog.String("error", err.Error()))
53+
}
54+
}
55+
go func() {
56+
defer mon.Close()
57+
defer rw.Close()
58+
buff := [1024]byte{}
59+
for {
60+
// Read from reader and write to monitor
61+
n, err := rw.Read(buff[:])
62+
if err != nil {
63+
logSocketError("Error reading from websocket", err)
64+
return
65+
}
66+
if _, err := mon.Write(buff[:n]); err != nil {
67+
logSocketError("Error writing to monitor", err)
68+
return
69+
}
70+
}
71+
}()
72+
go func() {
73+
defer mon.Close()
74+
defer rw.Close()
75+
buff := [1024]byte{}
76+
for {
77+
// Read from monitor and write to writer
78+
n, err := mon.Read(buff[:])
79+
if err != nil {
80+
logSocketError("Error reading from monitor", err)
81+
return
82+
}
83+
84+
if _, err := rw.Write(buff[:n]); err != nil {
85+
logSocketError("Error writing to buffer", err)
86+
return
87+
}
88+
}
89+
}()
90+
}

0 commit comments

Comments
 (0)