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

Sign up
Appearance settings

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 <c.maglie@bug.st> * improve description * apply code review suggestions * propertly implement the internal function * handle also text type * Update internal/monitor/monitor.go Co-authored-by: Davide <davideneri18@gmail.com> * Update internal/monitor/monitor.go Co-authored-by: Davide <davideneri18@gmail.com> * Update internal/monitor/monitor.go Co-authored-by: Davide <davideneri18@gmail.com> * 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 <c.maglie@bug.st> Co-authored-by: Davide <davideneri18@gmail.com>
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 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/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 license@arduino.cc.
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)

AltStyle によって変換されたページ (->オリジナル) /