Skip to content

Commit a3a434e

Browse files
committed
feat: add bridge monitor command
1 parent 86853c9 commit a3a434e

File tree

5 files changed

+171
-93
lines changed

5 files changed

+171
-93
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()

cmd/arduino-app-cli/app/monitor.go renamed to cmd/arduino-app-cli/monitor/monitor.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,52 @@
1313
// Arduino software without disclosing the source code of your own applications.
1414
// To purchase a commercial license, send an email to license@arduino.cc.
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/internal/monitor"
2325
)
2426

25-
func newMonitorCmd(cfg config.Configuration) *cobra.Command {
27+
func NewMonitorCmd() *cobra.Command {
2628
return &cobra.Command{
2729
Use: "monitor",
2830
Short: "Monitor the Arduino app",
2931
RunE: func(cmd *cobra.Command, args []string) error {
30-
panic("not implemented")
32+
start, err := monitor.NewMonitorHandler(&stdInOutProxy{stdin: os.Stdin, stdout: os.Stdout}) // nolint:forbidigo
33+
if err != nil {
34+
return err
35+
}
36+
go start()
37+
<-cmd.Context().Done()
38+
return nil
3139
},
32-
ValidArgsFunction: completion.ApplicationNames(cfg),
3340
}
3441
}
42+
43+
type stdInOutProxy struct {
44+
stdin io.Reader
45+
stdout io.Writer
46+
}
47+
48+
func (s stdInOutProxy) ReadMessage() (int, []byte, error) {
49+
var p [1024]byte
50+
n, err := s.stdin.Read(p[:])
51+
if err != nil {
52+
return 0, nil, err
53+
}
54+
return 1, p[:n], nil
55+
}
56+
57+
func (s stdInOutProxy) WriteMessage(messageType int, data []byte) error {
58+
_, err := s.stdout.Write(data)
59+
return err
60+
}
61+
62+
func (s stdInOutProxy) Close() error {
63+
return nil
64+
}

internal/api/handlers/monitor.go

Lines changed: 36 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
package handlers
1717

1818
import (
19-
"errors"
2019
"fmt"
21-
"io"
2220
"log/slog"
2321
"net"
2422
"net/http"
@@ -28,59 +26,50 @@ import (
2826
"github.com/gorilla/websocket"
2927

3028
"github.com/arduino/arduino-app-cli/internal/api/models"
29+
"github.com/arduino/arduino-app-cli/internal/monitor"
3130
"github.com/arduino/arduino-app-cli/internal/render"
3231
)
3332

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-
}
33+
func HandleMonitorWS(allowedOrigins []string) http.HandlerFunc {
34+
upgrader := websocket.Upgrader{
35+
ReadBufferSize: 1024,
36+
WriteBufferSize: 1024,
37+
CheckOrigin: func(r *http.Request) bool {
38+
return checkOrigin(r.Header.Get("Origin"), allowedOrigins)
39+
},
4440
}
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()))
41+
42+
return func(w http.ResponseWriter, r *http.Request) {
43+
// Connect to monitor
44+
mon, err := net.DialTimeout("tcp", "127.0.0.1:7500", time.Second)
45+
if err != nil {
46+
slog.Error("Unable to connect to monitor", slog.String("error", err.Error()))
47+
render.EncodeResponse(w, http.StatusServiceUnavailable, models.ErrorResponse{Details: "Unable to connect to monitor: " + err.Error()})
48+
return
4849
}
49-
}
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-
}
50+
51+
// Upgrade the connection to websocket
52+
conn, err := upgrader.Upgrade(w, r, nil)
53+
if err != nil {
54+
// Remember to close monitor connection if websocket upgrade fails.
55+
mon.Close()
56+
57+
slog.Error("Failed to upgrade connection", slog.String("error", err.Error()))
58+
render.EncodeResponse(w, http.StatusInternalServerError, map[string]string{"error": "Failed to upgrade connection: " + err.Error()})
59+
return
6460
}
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-
}
7761

78-
if err := ws.WriteMessage(websocket.BinaryMessage, buff[:n]); err != nil {
79-
logWebsocketError("Error writing to websocket", err)
80-
return
81-
}
62+
// Now the connection is managed by the websocket library, let's move the handlers in the goroutine
63+
start, err := monitor.NewMonitorHandler(conn)
64+
if err != nil {
65+
slog.Error("Unable to start monitor handler", slog.String("error", err.Error()))
66+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "Unable to start monitor handler: " + err.Error()})
67+
return
8268
}
83-
}()
69+
go start()
70+
71+
// and return nothing to the http library
72+
}
8473
}
8574

8675
func splitOrigin(origin string) (scheme, host, port string, err error) {
@@ -125,42 +114,3 @@ func checkOrigin(origin string, allowedOrigins []string) bool {
125114
slog.Error("WebSocket origin check failed", slog.String("origin", origin))
126115
return false
127116
}
128-
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)
132-
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-
}
140-
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-
}
149-
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()
155-
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-
}
160-
161-
// Now the connection is managed by the websocket library, let's move the handlers in the goroutine
162-
go monitorStream(mon, conn)
163-
164-
// and return nothing to the http library
165-
}
166-
}

internal/monitor/monitor.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 license@arduino.cc.
15+
16+
package monitor
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
"io"
22+
"log/slog"
23+
"net"
24+
"time"
25+
26+
"github.com/gorilla/websocket"
27+
)
28+
29+
type MessageReaderWriter interface {
30+
ReadMessage() (messageType int, p []byte, err error)
31+
WriteMessage(messageType int, data []byte) error
32+
Close() error
33+
}
34+
35+
func NewMonitorHandler(ws MessageReaderWriter) (func(), error) {
36+
// Connect to monitor
37+
mon, err := net.DialTimeout("tcp", "127.0.0.1:7500", time.Second)
38+
if err != nil {
39+
return func() {}, err
40+
}
41+
42+
return func() {
43+
monitorStream(mon, ws)
44+
}, nil
45+
}
46+
47+
func monitorStream(mon net.Conn, ws MessageReaderWriter) {
48+
logWebsocketError := func(msg string, err error) {
49+
// Do not log simple close or interruption errors
50+
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
51+
if e, ok := err.(*websocket.CloseError); ok {
52+
slog.Error(msg, slog.String("closecause", fmt.Sprintf("%d: %s", e.Code, err)))
53+
} else {
54+
slog.Error(msg, slog.String("error", err.Error()))
55+
}
56+
}
57+
}
58+
logSocketError := func(msg string, err error) {
59+
if !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
60+
slog.Error(msg, slog.String("error", err.Error()))
61+
}
62+
}
63+
go func() {
64+
defer mon.Close()
65+
defer ws.Close()
66+
for {
67+
// Read from websocket and write to monitor
68+
_, msg, err := ws.ReadMessage()
69+
if err != nil {
70+
logWebsocketError("Error reading from websocket", err)
71+
return
72+
}
73+
if _, err := mon.Write(msg); err != nil {
74+
logSocketError("Error writing to monitor", err)
75+
return
76+
}
77+
}
78+
}()
79+
go func() {
80+
defer mon.Close()
81+
defer ws.Close()
82+
buff := [1024]byte{}
83+
for {
84+
// Read from monitor and write to websocket
85+
n, err := mon.Read(buff[:])
86+
if err != nil {
87+
logSocketError("Error reading from monitor", err)
88+
return
89+
}
90+
91+
if err := ws.WriteMessage(websocket.BinaryMessage, buff[:n]); err != nil {
92+
logWebsocketError("Error writing to websocket", err)
93+
return
94+
}
95+
}
96+
}()
97+
}

0 commit comments

Comments
 (0)