Skip to content
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
5 changes: 3 additions & 2 deletions drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
package main

import (
"github.com/bcmi-labs/orchestrator/cmd/feedback"
"github.com/bcmi-labs/orchestrator/cmd/i18n"
"github.com/spf13/cobra"

"github.com/arduino/arduino-flasher-cli/feedback"
"github.com/arduino/arduino-flasher-cli/i18n"
)

func newInstallDriversCmd() *cobra.Command {
Expand Down
59 changes: 59 additions & 0 deletions feedback/errorcodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// This file is part of arduino-flasher-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-flasher-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 feedback

// ExitCode to be used for Fatal.
type ExitCode int

const (
// Success (0 is the no-error return code in Unix)
Success ExitCode = iota

// ErrGeneric Generic error (1 is the reserved "catchall" code in Unix)
ErrGeneric

_ // (2 Is reserved in Unix)

// ErrNoConfigFile is returned when the config file is not found (3)
ErrNoConfigFile

_ // (4 was ErrBadCall and has been removed)

// ErrNetwork is returned when a network error occurs (5)
ErrNetwork

// ErrCoreConfig represents an error in the cli core config, for example some basic
// files shipped with the installation are missing, or cannot create or get basic
// directories vital for the CLI to work. (6)
ErrCoreConfig

// ErrBadArgument is returned when the arguments are not valid (7)
ErrBadArgument

// ErrFailedToListenToTCPPort is returned if the CLI failed to open a TCP port
// to listen for incoming connections (8)
ErrFailedToListenToTCPPort

// ErrBadTCPPortArgument is returned if the TCP port argument is not valid (9)
ErrBadTCPPortArgument

// ErrInitializingInventory is returned when the inventory cannot be initialized,
// usually depends on a wrong configuration of the data dir (10)
ErrInitializingInventory

// ErrMissingProgrammer is returned when the programmer argument is missing (11)
ErrMissingProgrammer
)
259 changes: 259 additions & 0 deletions feedback/feedback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// This file is part of arduino-flasher-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-flasher-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 feedback

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"

"github.com/sirupsen/logrus"

"github.com/arduino/arduino-flasher-cli/i18n"
)

// OutputFormat is an output format
type OutputFormat int

const (
// Text is the plain text format, suitable for interactive terminals
Text OutputFormat = iota
// JSON format
JSON
// MinifiedJSON format
MinifiedJSON
)

var formats = map[string]OutputFormat{
"json": JSON,
"jsonmini": MinifiedJSON,
"text": Text,
}

func (f OutputFormat) String() string {
for res, format := range formats {
if format == f {
return res
}
}
panic("unknown output format")
}

// ParseOutputFormat parses a string and returns the corresponding OutputFormat.
// The boolean returned is true if the string was a valid OutputFormat.
func ParseOutputFormat(in string) (OutputFormat, bool) {
format, found := formats[in]
return format, found
}

var (
stdOut io.Writer
stdErr io.Writer
feedbackOut io.Writer
feedbackErr io.Writer
bufferOut *bytes.Buffer
bufferErr *bytes.Buffer
bufferWarnings []string
format OutputFormat
formatSelected bool
)

// nolint:gochecknoinits
func init() {
reset()
}

// reset resets the feedback package to its initial state, useful for unit testing
func reset() {
stdOut = os.Stdout
stdErr = os.Stderr
feedbackOut = os.Stdout
feedbackErr = os.Stderr
bufferOut = bytes.NewBuffer(nil)
bufferErr = bytes.NewBuffer(nil)
bufferWarnings = nil
format = Text
formatSelected = false
}

// Result is anything more complex than a sentence that needs to be printed
// for the user.
type Result interface {
fmt.Stringer
Data() interface{}
}

// ErrorResult is a result embedding also an error. In case of textual output
// the error will be printed on stderr.
type ErrorResult interface {
Result
ErrorString() string
}

// SetOut can be used to change the out writer at runtime
func SetOut(out io.Writer) {
if formatSelected {
panic("output format already selected")
}
stdOut = out
}

// SetErr can be used to change the err writer at runtime
func SetErr(err io.Writer) {
if formatSelected {
panic("output format already selected")
}
stdErr = err
}

// SetFormat can be used to change the output format at runtime
func SetFormat(f OutputFormat) {
if formatSelected {
panic("output format already selected")
}
format = f
formatSelected = true

if format == Text {
feedbackOut = io.MultiWriter(bufferOut, stdOut)
feedbackErr = io.MultiWriter(bufferErr, stdErr)
} else {
feedbackOut = bufferOut
feedbackErr = bufferErr
bufferWarnings = nil
}
}

// GetFormat returns the output format currently set
func GetFormat() OutputFormat {
return format
}

// Printf behaves like fmt.Printf but writes on the out writer and adds a newline.
func Printf(format string, v ...interface{}) {
Print(fmt.Sprintf(format, v...))
}

// Print behaves like fmt.Print but writes on the out writer and adds a newline.
func Print(v string) {
fmt.Fprintln(feedbackOut, v)
}

// Warning outputs a warning message.
func Warning(msg string) {
if format == Text {
fmt.Fprintln(feedbackErr, msg)
} else {
bufferWarnings = append(bufferWarnings, msg)
}
logrus.Warning(msg)
}

// FatalError outputs the error and exits with status exitCode.
func FatalError(err error, exitCode ExitCode) {
Fatal(err.Error(), exitCode)
}

// FatalResult outputs the result and exits with status exitCode.
func FatalResult(res ErrorResult, exitCode ExitCode) {
PrintResult(res)
os.Exit(int(exitCode))
}

// Fatal outputs the errorMsg and exits with status exitCode.
func Fatal(errorMsg string, exitCode ExitCode) {
if format == Text {
fmt.Fprintln(stdErr, errorMsg)
os.Exit(int(exitCode))
}

type FatalError struct {
Error string `json:"error"`
Output *OutputStreamsResult `json:"output,omitempty"`
}
res := &FatalError{
Error: errorMsg,
}
if output := getOutputStreamResult(); !output.Empty() {
res.Output = output
}
var d []byte
switch format {
case JSON:
d, _ = json.MarshalIndent(augment(res), "", " ")
case MinifiedJSON:
d, _ = json.Marshal(augment(res))
default:
panic("unknown output format")
}
fmt.Fprintln(stdErr, string(d))
os.Exit(int(exitCode))
}

func augment(data interface{}) interface{} {
if len(bufferWarnings) == 0 {
return data
}
d, err := json.Marshal(data)
if err != nil {
return data
}
var res interface{}
if err := json.Unmarshal(d, &res); err != nil {
return data
}
if m, ok := res.(map[string]interface{}); ok {
m["warnings"] = bufferWarnings
}
return res
}

// PrintResult is a convenient wrapper to provide feedback for complex data,
// where the contents can't be just serialized to JSON but requires more
// structure.
func PrintResult(res Result) {
var data string
var dataErr string
switch format {
case JSON:
d, err := json.MarshalIndent(augment(res.Data()), "", " ")
if err != nil {
Fatal(i18n.Tr("Error during JSON encoding of the output: %v", err), ErrGeneric)
}
data = string(d)
case MinifiedJSON:
d, err := json.Marshal(augment(res.Data()))
if err != nil {
Fatal(i18n.Tr("Error during JSON encoding of the output: %v", err), ErrGeneric)
}
data = string(d)
case Text:
data = res.String()
if resErr, ok := res.(ErrorResult); ok {
dataErr = resErr.ErrorString()
}
default:
panic("unknown output format")
}
if data != "" {
fmt.Fprintln(stdOut, data)
}
if dataErr != "" {
fmt.Fprintln(stdErr, dataErr)
}
}
Loading