Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cmd/arduino-app-cli/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ func NewAppCmd(cfg config.Configuration) *cobra.Command {
appCmd.AddCommand(newRestartCmd(cfg))
appCmd.AddCommand(newLogsCmd(cfg))
appCmd.AddCommand(newListCmd(cfg))
appCmd.AddCommand(newMonitorCmd(cfg))
appCmd.AddCommand(newCacheCleanCmd(cfg))

return appCmd
Expand Down
2 changes: 2 additions & 0 deletions cmd/arduino-app-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/config"
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/daemon"
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/internal/servicelocator"
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/monitor"
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/properties"
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/system"
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/version"
Expand Down Expand Up @@ -78,6 +79,7 @@ func run(configuration cfg.Configuration) error {
config.NewConfigCmd(configuration),
system.NewSystemCmd(configuration),
version.NewVersionCmd(Version),
monitor.NewMonitorCmd(),
)

ctx := context.Background()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,52 @@
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package app
package monitor

import (
"io"
"os"

"github.com/spf13/cobra"

"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/completion"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
"github.com/arduino/arduino-app-cli/internal/monitor"
)

func newMonitorCmd(cfg config.Configuration) *cobra.Command {
func NewMonitorCmd() *cobra.Command {
return &cobra.Command{
Use: "monitor",
Short: "Monitor the Arduino app",
Short: "Attach to the microcontroller serial monitor",
RunE: func(cmd *cobra.Command, args []string) error {
panic("not implemented")
start, err := monitor.NewMonitorHandler(&stdInOutProxy{stdin: os.Stdin, stdout: os.Stdout}) // nolint:forbidigo
if err != nil {
return err
}
go start()
<-cmd.Context().Done()
return nil
},
ValidArgsFunction: completion.ApplicationNames(cfg),
}
}

type stdInOutProxy struct {
stdin io.Reader
stdout io.Writer
}

func (s stdInOutProxy) ReadMessage() (int, []byte, error) {
var p [1024]byte
n, err := s.stdin.Read(p[:])
if err != nil {
return 0, nil, err
}
return 1, p[:n], nil
}

func (s stdInOutProxy) WriteMessage(messageType int, data []byte) error {
_, err := s.stdout.Write(data)
return err
}

func (s stdInOutProxy) Close() error {
return nil
}
122 changes: 36 additions & 86 deletions internal/api/handlers/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
package handlers

import (
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
Expand All @@ -28,59 +26,50 @@ import (
"github.com/gorilla/websocket"

"github.com/arduino/arduino-app-cli/internal/api/models"
"github.com/arduino/arduino-app-cli/internal/monitor"
"github.com/arduino/arduino-app-cli/internal/render"
)

func monitorStream(mon net.Conn, ws *websocket.Conn) {
logWebsocketError := func(msg string, err error) {
// Do not log simple close or interruption errors
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
if e, ok := err.(*websocket.CloseError); ok {
slog.Error(msg, slog.String("closecause", fmt.Sprintf("%d: %s", e.Code, err)))
} else {
slog.Error(msg, slog.String("error", err.Error()))
}
}
func HandleMonitorWS(allowedOrigins []string) http.HandlerFunc {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return checkOrigin(r.Header.Get("Origin"), allowedOrigins)
},
}
logSocketError := func(msg string, err error) {
if !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
slog.Error(msg, slog.String("error", err.Error()))

return func(w http.ResponseWriter, r *http.Request) {
// Connect to monitor
mon, err := net.DialTimeout("tcp", "127.0.0.1:7500", time.Second)
if err != nil {
slog.Error("Unable to connect to monitor", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusServiceUnavailable, models.ErrorResponse{Details: "Unable to connect to monitor: " + err.Error()})
return
}
}
go func() {
defer mon.Close()
defer ws.Close()
for {
// Read from websocket and write to monitor
_, msg, err := ws.ReadMessage()
if err != nil {
logWebsocketError("Error reading from websocket", err)
return
}
if _, err := mon.Write(msg); err != nil {
logSocketError("Error writing to monitor", err)
return
}

// Upgrade the connection to websocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
// Remember to close monitor connection if websocket upgrade fails.
mon.Close()

slog.Error("Failed to upgrade connection", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, map[string]string{"error": "Failed to upgrade connection: " + err.Error()})
return
}
}()
go func() {
defer mon.Close()
defer ws.Close()
buff := [1024]byte{}
for {
// Read from monitor and write to websocket
n, err := mon.Read(buff[:])
if err != nil {
logSocketError("Error reading from monitor", err)
return
}

if err := ws.WriteMessage(websocket.BinaryMessage, buff[:n]); err != nil {
logWebsocketError("Error writing to websocket", err)
return
}
// Now the connection is managed by the websocket library, let's move the handlers in the goroutine
start, err := monitor.NewMonitorHandler(conn)
if err != nil {
slog.Error("Unable to start monitor handler", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "Unable to start monitor handler: " + err.Error()})
return
}
}()
go start()

// and return nothing to the http library
}
}

func splitOrigin(origin string) (scheme, host, port string, err error) {
Expand Down Expand Up @@ -125,42 +114,3 @@ func checkOrigin(origin string, allowedOrigins []string) bool {
slog.Error("WebSocket origin check failed", slog.String("origin", origin))
return false
}

func HandleMonitorWS(allowedOrigins []string) http.HandlerFunc {
// Do a dry-run of checkorigin, so it can panic if misconfigured now, not on first request
_ = checkOrigin("http://localhost", allowedOrigins)

upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return checkOrigin(r.Header.Get("Origin"), allowedOrigins)
},
}

return func(w http.ResponseWriter, r *http.Request) {
// Connect to monitor
mon, err := net.DialTimeout("tcp", "127.0.0.1:7500", time.Second)
if err != nil {
slog.Error("Unable to connect to monitor", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusServiceUnavailable, models.ErrorResponse{Details: "Unable to connect to monitor: " + err.Error()})
return
}

// Upgrade the connection to websocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
// Remember to close monitor connection if websocket upgrade fails.
mon.Close()

slog.Error("Failed to upgrade connection", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, map[string]string{"error": "Failed to upgrade connection: " + err.Error()})
return
}

// Now the connection is managed by the websocket library, let's move the handlers in the goroutine
go monitorStream(mon, conn)

// and return nothing to the http library
}
}
97 changes: 97 additions & 0 deletions internal/monitor/monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// This file is part of arduino-app-cli.
//
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-app-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package monitor

import (
"errors"
"fmt"
"io"
"log/slog"
"net"
"time"

"github.com/gorilla/websocket"
)

type MessageReaderWriter interface {
ReadMessage() (messageType int, p []byte, err error)
WriteMessage(messageType int, data []byte) error
Close() error
}

func NewMonitorHandler(ws MessageReaderWriter) (func(), error) {
// Connect to monitor
mon, err := net.DialTimeout("tcp", "127.0.0.1:7500", time.Second)
if err != nil {
return nil, err
}

return func() {
monitorStream(mon, ws)
}, nil
}

func monitorStream(mon net.Conn, ws MessageReaderWriter) {
logWebsocketError := func(msg string, err error) {
// Do not log simple close or interruption errors
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
if e, ok := err.(*websocket.CloseError); ok {
slog.Error(msg, slog.String("closecause", fmt.Sprintf("%d: %s", e.Code, err)))
} else {
slog.Error(msg, slog.String("error", err.Error()))
}
}
}
logSocketError := func(msg string, err error) {
if !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
slog.Error(msg, slog.String("error", err.Error()))
}
}
go func() {
defer mon.Close()
defer ws.Close()
for {
// Read from websocket and write to monitor
_, msg, err := ws.ReadMessage()
if err != nil {
logWebsocketError("Error reading from websocket", err)
return
}
if _, err := mon.Write(msg); err != nil {
logSocketError("Error writing to monitor", err)
return
}
}
}()
go func() {
defer mon.Close()
defer ws.Close()
buff := [1024]byte{}
for {
// Read from monitor and write to websocket
n, err := mon.Read(buff[:])
if err != nil {
logSocketError("Error reading from monitor", err)
return
}

if err := ws.WriteMessage(websocket.BinaryMessage, buff[:n]); err != nil {
logWebsocketError("Error writing to websocket", err)
return
}
}
}()
}