From 094a22c190fdc238be40bd654af379a7f0879a4a Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 18 Aug 2023 21:34:25 +0200 Subject: [PATCH 1/5] Removed useless tty abstraction --- internal/cli/monitor/monitor.go | 7 ++--- internal/cli/monitor/term.go | 51 --------------------------------- 2 files changed, 3 insertions(+), 55 deletions(-) delete mode 100644 internal/cli/monitor/term.go diff --git a/internal/cli/monitor/monitor.go b/internal/cli/monitor/monitor.go index 6fcf4a87de6..b0af0c9bae8 100644 --- a/internal/cli/monitor/monitor.go +++ b/internal/cli/monitor/monitor.go @@ -93,11 +93,10 @@ func runMonitorCmd(cmd *cobra.Command, args []string) { return } - tty, err := newStdInOutTerminal() + ttyIn, ttyOut, err := feedback.InteractiveStreams() if err != nil { feedback.FatalError(err, feedback.ErrGeneric) } - defer tty.Close() configuration := &rpc.MonitorPortConfiguration{} if len(configs) > 0 { @@ -153,7 +152,7 @@ func runMonitorCmd(cmd *cobra.Command, args []string) { ctx, cancel := context.WithCancel(context.Background()) 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 +161,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)) 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) -} From c1a7713e33b1f042e33760c83d281bb4aa5029f5 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 18 Aug 2023 21:36:12 +0200 Subject: [PATCH 2/5] Print 'Connected to...' message immediatly after connection --- internal/cli/monitor/monitor.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cli/monitor/monitor.go b/internal/cli/monitor/monitor.go index b0af0c9bae8..ee2f2be5c9c 100644 --- a/internal/cli/monitor/monitor.go +++ b/internal/cli/monitor/monitor.go @@ -150,6 +150,10 @@ func runMonitorCmd(cmd *cobra.Command, args []string) { } defer portProxy.Close() + if !quiet { + feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress)) + } + ctx, cancel := context.WithCancel(context.Background()) go func() { _, err := io.Copy(ttyOut, portProxy) @@ -170,10 +174,6 @@ 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() } From dd1e44ae2e96a70430b1046560d5c912c1a40e17 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Sat, 19 Aug 2023 00:28:57 +0200 Subject: [PATCH 3/5] Added terminal raw mode --- internal/cli/feedback/terminal.go | 23 +++++++++++++++ internal/cli/monitor/monitor.go | 49 ++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/internal/cli/feedback/terminal.go b/internal/cli/feedback/terminal.go index 5b645cbaf67..3dc707e1556 100644 --- a/internal/cli/feedback/terminal.go +++ b/internal/cli/feedback/terminal.go @@ -22,6 +22,7 @@ import ( "io" "os" + "golang.org/x/crypto/ssh/terminal" "golang.org/x/term" ) @@ -41,6 +42,28 @@ func InteractiveStreams() (io.Reader, io.Writer, error) { return os.Stdin, stdOut, nil } +var oldStateStdin *terminal.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, _ = terminal.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 + } + _ = terminal.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 ee2f2be5c9c..446ef10749e 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,6 +36,7 @@ import ( "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "go.bug.st/cleanup" ) var ( @@ -48,6 +50,7 @@ var ( // NewCommand created a new `monitor` command func NewCommand() *cobra.Command { + var raw bool 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(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(raw bool) { instance := instance.CreateAndInit() logrus.Info("Executing `arduino-cli monitor`") @@ -93,11 +99,6 @@ func runMonitorCmd(cmd *cobra.Command, args []string) { return } - ttyIn, ttyOut, err := feedback.InteractiveStreams() - if err != nil { - feedback.FatalError(err, feedback.ErrGeneric) - } - configuration := &rpc.MonitorPortConfiguration{} if len(configs) > 0 { for _, config := range configs { @@ -154,7 +155,27 @@ func runMonitorCmd(cmd *cobra.Command, args []string) { feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress)) } - ctx, cancel := context.WithCancel(context.Background()) + 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(ttyOut, portProxy) if err != nil && !errors.Is(err, io.EOF) { @@ -178,6 +199,18 @@ func runMonitorCmd(cmd *cobra.Command, args []string) { <-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"` } From a8e32d9591cdf26ca958d39691bf50097a4084a5 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 1 Sep 2023 17:43:19 +0200 Subject: [PATCH 4/5] Make local the argument/flags of 'monitor' command --- internal/cli/monitor/monitor.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/cli/monitor/monitor.go b/internal/cli/monitor/monitor.go index 446ef10749e..65b5c599a76 100644 --- a/internal/cli/monitor/monitor.go +++ b/internal/cli/monitor/monitor.go @@ -39,18 +39,18 @@ import ( "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 + 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."), @@ -59,7 +59,7 @@ func NewCommand() *cobra.Command { " " + os.Args[0] + " monitor -p /dev/ttyACM0\n" + " " + os.Args[0] + " monitor -p /dev/ttyACM0 --describe", Run: func(cmd *cobra.Command, args []string) { - runMonitorCmd(raw) + runMonitorCmd(&portArgs, &fqbn, configs, describe, quiet, raw) }, } portArgs.AddToCommand(monitorCommand) @@ -72,7 +72,7 @@ func NewCommand() *cobra.Command { return monitorCommand } -func runMonitorCmd(raw bool) { +func runMonitorCmd(portArgs *arguments.Port, fqbn *arguments.Fqbn, configs []string, describe, quiet, raw bool) { instance := instance.CreateAndInit() logrus.Info("Executing `arduino-cli monitor`") From b7e4a394980f073b3433e70ee9e80d2ca858a374 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Wed, 6 Sep 2023 09:40:01 +0200 Subject: [PATCH 5/5] Applied code suggestion --- internal/cli/feedback/terminal.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/cli/feedback/terminal.go b/internal/cli/feedback/terminal.go index 3dc707e1556..11c466bae8b 100644 --- a/internal/cli/feedback/terminal.go +++ b/internal/cli/feedback/terminal.go @@ -22,7 +22,6 @@ import ( "io" "os" - "golang.org/x/crypto/ssh/terminal" "golang.org/x/term" ) @@ -42,7 +41,7 @@ func InteractiveStreams() (io.Reader, io.Writer, error) { return os.Stdin, stdOut, nil } -var oldStateStdin *terminal.State +var oldStateStdin *term.State // SetRawModeStdin sets the stdin stream in RAW mode (no buffering, echo disabled, // no terminal escape codes nor signals interpreted) @@ -50,7 +49,7 @@ func SetRawModeStdin() { if oldStateStdin != nil { panic("terminal already in RAW mode") } - oldStateStdin, _ = terminal.MakeRaw(int(os.Stdin.Fd())) + oldStateStdin, _ = term.MakeRaw(int(os.Stdin.Fd())) } // RestoreModeStdin restore the terminal settings to the normal non-RAW state. This @@ -60,7 +59,7 @@ func RestoreModeStdin() { if oldStateStdin == nil { return } - _ = terminal.Restore(int(os.Stdin.Fd()), oldStateStdin) + _ = term.Restore(int(os.Stdin.Fd()), oldStateStdin) oldStateStdin = nil }