Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Added raw terminal support in monitor command #2291

Merged
merged 5 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 22 additions & 0 deletions internal/cli/feedback/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ func InteractiveStreams() (io.Reader, io.Writer, error) {
return os.Stdin, stdOut, nil
}

var oldStateStdin *term.State

// SetRawModeStdin sets the stdin stream in RAW mode (no buffering, echo disabled,
// no terminal escape codes nor signals interpreted)
func SetRawModeStdin() {
if oldStateStdin != nil {
panic("terminal already in RAW mode")
}
oldStateStdin, _ = term.MakeRaw(int(os.Stdin.Fd()))
}

// RestoreModeStdin restore the terminal settings to the normal non-RAW state. This
// function must be called after SetRawModeStdin to not leave the terminal in an
// undefined state.
func RestoreModeStdin() {
if oldStateStdin == nil {
return
}
_ = term.Restore(int(os.Stdin.Fd()), oldStateStdin)
oldStateStdin = nil
}

func isTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd()))
}
Expand Down
78 changes: 55 additions & 23 deletions internal/cli/monitor/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package monitor

import (
"bytes"
"context"
"errors"
"fmt"
Expand All @@ -35,29 +36,34 @@ import (
"github.com/fatih/color"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.bug.st/cleanup"
)

var (
portArgs arguments.Port
describe bool
configs []string
quiet bool
fqbn arguments.Fqbn
tr = i18n.Tr
)
var tr = i18n.Tr

// NewCommand created a new `monitor` command
func NewCommand() *cobra.Command {
var (
raw bool
portArgs arguments.Port
describe bool
configs []string
quiet bool
fqbn arguments.Fqbn
)
monitorCommand := &cobra.Command{
Use: "monitor",
Short: tr("Open a communication port with a board."),
Long: tr("Open a communication port with a board."),
Example: "" +
" " + os.Args[0] + " monitor -p /dev/ttyACM0\n" +
" " + os.Args[0] + " monitor -p /dev/ttyACM0 --describe",
Run: runMonitorCmd,
Run: func(cmd *cobra.Command, args []string) {
runMonitorCmd(&portArgs, &fqbn, configs, describe, quiet, raw)
},
}
portArgs.AddToCommand(monitorCommand)
monitorCommand.Flags().BoolVar(&raw, "raw", false, tr("Set terminal in raw mode (unbuffered)."))
monitorCommand.Flags().BoolVar(&describe, "describe", false, tr("Show all the settings of the communication port."))
monitorCommand.Flags().StringSliceVarP(&configs, "config", "c", []string{}, tr("Configure communication port settings. The format is <ID>=<value>[,<ID>=<value>]..."))
monitorCommand.Flags().BoolVarP(&quiet, "quiet", "q", false, tr("Run in silent mode, show only monitor input and output."))
Expand All @@ -66,7 +72,7 @@ func NewCommand() *cobra.Command {
return monitorCommand
}

func runMonitorCmd(cmd *cobra.Command, args []string) {
func runMonitorCmd(portArgs *arguments.Port, fqbn *arguments.Fqbn, configs []string, describe, quiet, raw bool) {
instance := instance.CreateAndInit()
logrus.Info("Executing `arduino-cli monitor`")

Expand All @@ -93,12 +99,6 @@ func runMonitorCmd(cmd *cobra.Command, args []string) {
return
}

tty, err := newStdInOutTerminal()
if err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
}
defer tty.Close()

configuration := &rpc.MonitorPortConfiguration{}
if len(configs) > 0 {
for _, config := range configs {
Expand Down Expand Up @@ -151,9 +151,33 @@ func runMonitorCmd(cmd *cobra.Command, args []string) {
}
defer portProxy.Close()

ctx, cancel := context.WithCancel(context.Background())
if !quiet {
feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress))
}

ttyIn, ttyOut, err := feedback.InteractiveStreams()
if err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
}

ctx, cancel := cleanup.InterruptableContext(context.Background())
if raw {
feedback.SetRawModeStdin()
defer func() {
feedback.RestoreModeStdin()
}()

// In RAW mode CTRL-C is not converted into an Interrupt by
// the terminal, we must intercept ASCII 3 (CTRL-C) on our own...
ctrlCDetector := &charDetectorWriter{
callback: cancel,
detectedChar: 3, // CTRL-C
}
ttyIn = io.TeeReader(ttyIn, ctrlCDetector)
}

go func() {
_, err := io.Copy(tty, portProxy)
_, err := io.Copy(ttyOut, portProxy)
if err != nil && !errors.Is(err, io.EOF) {
if !quiet {
feedback.Print(tr("Port closed: %v", err))
Expand All @@ -162,7 +186,7 @@ func runMonitorCmd(cmd *cobra.Command, args []string) {
cancel()
}()
go func() {
_, err := io.Copy(portProxy, tty)
_, err := io.Copy(portProxy, ttyIn)
if err != nil && !errors.Is(err, io.EOF) {
if !quiet {
feedback.Print(tr("Port closed: %v", err))
Expand All @@ -171,14 +195,22 @@ func runMonitorCmd(cmd *cobra.Command, args []string) {
cancel()
}()

if !quiet {
feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress))
}

// Wait for port closed
<-ctx.Done()
}

type charDetectorWriter struct {
callback func()
detectedChar byte
}

func (cd *charDetectorWriter) Write(buf []byte) (int, error) {
if bytes.IndexByte(buf, cd.detectedChar) != -1 {
cd.callback()
}
return len(buf), nil
}

type detailsResult struct {
Settings []*rpc.MonitorPortSettingDescriptor `json:"settings"`
}
Expand Down
51 changes: 0 additions & 51 deletions internal/cli/monitor/term.go

This file was deleted.