diff --git a/internal/cli/feedback/terminal.go b/internal/cli/feedback/terminal.go index 5b645cbaf67..11c466bae8b 100644 --- a/internal/cli/feedback/terminal.go +++ b/internal/cli/feedback/terminal.go @@ -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())) } diff --git a/internal/cli/monitor/monitor.go b/internal/cli/monitor/monitor.go index 6fcf4a87de6..65b5c599a76 100644 --- a/internal/cli/monitor/monitor.go +++ b/internal/cli/monitor/monitor.go @@ -16,6 +16,7 @@ package monitor import ( + "bytes" "context" "errors" "fmt" @@ -35,19 +36,21 @@ 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."), @@ -55,9 +58,12 @@ func NewCommand() *cobra.Command { 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 =[,=]...")) monitorCommand.Flags().BoolVarP(&quiet, "quiet", "q", false, tr("Run in silent mode, show only monitor input and output.")) @@ -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`") @@ -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 { @@ -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)) @@ -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)) @@ -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"` } diff --git a/internal/cli/monitor/term.go b/internal/cli/monitor/term.go deleted file mode 100644 index 34e444b5591..00000000000 --- a/internal/cli/monitor/term.go +++ /dev/null @@ -1,51 +0,0 @@ -// This file is part of arduino-cli. -// -// Copyright 2020 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-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 ( - "io" - - "github.com/arduino/arduino-cli/internal/cli/feedback" -) - -type stdInOut struct { - in io.Reader - out io.Writer -} - -func newStdInOutTerminal() (*stdInOut, error) { - in, out, err := feedback.InteractiveStreams() - if err != nil { - return nil, err - } - - return &stdInOut{ - in: in, - out: out, - }, nil -} - -func (n *stdInOut) Close() error { - return nil -} - -func (n *stdInOut) Read(buff []byte) (int, error) { - return n.in.Read(buff) -} - -func (n *stdInOut) Write(buff []byte) (int, error) { - return n.out.Write(buff) -}