cmd/hakurei/json: friendly error messages
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m23s
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m23s
This change handles errors returned by encoding/json and prints significantly cleaner messages. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
b9459a80c7
commit
d42067df7c
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@ -227,8 +226,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
} else {
|
||||
if f, err := os.Open(flagDBusConfigSession); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
} else if err = json.NewDecoder(f).Decode(&config.SessionBus); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
|
||||
} else {
|
||||
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
|
||||
if err = f.Close(); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,8 +238,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
if flagDBusConfigSystem != "nil" {
|
||||
if f, err := os.Open(flagDBusConfigSystem); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
} else if err = json.NewDecoder(f).Decode(&config.SystemBus); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
|
||||
} else {
|
||||
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
|
||||
if err = f.Close(); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,7 +328,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
|
||||
c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess })
|
||||
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
||||
c.Command("template", "Produce a config template", func(args []string) error { printJSON(os.Stdout, false, hst.Template()); return errSuccess })
|
||||
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
||||
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
||||
|
||||
return c
|
||||
|
||||
60
cmd/hakurei/json.go
Normal file
60
cmd/hakurei/json.go
Normal file
@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
|
||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
|
||||
err := json.NewDecoder(r).Decode(v)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
syntaxError *json.SyntaxError
|
||||
unmarshalTypeError *json.UnmarshalTypeError
|
||||
|
||||
msg string
|
||||
)
|
||||
|
||||
switch {
|
||||
case errors.As(err, &syntaxError) && syntaxError != nil:
|
||||
msg = syntaxError.Error() +
|
||||
" at byte " + strconv.FormatInt(syntaxError.Offset, 10)
|
||||
|
||||
case errors.As(err, &unmarshalTypeError) && unmarshalTypeError != nil:
|
||||
msg = "inappropriate " + unmarshalTypeError.Value +
|
||||
" at byte " + strconv.FormatInt(unmarshalTypeError.Offset, 10)
|
||||
|
||||
default:
|
||||
// InvalidUnmarshalError: incorrect usage, does not need to be handled
|
||||
// io.ErrUnexpectedEOF: no additional error information available
|
||||
msg = err.Error()
|
||||
}
|
||||
|
||||
fatal("cannot " + op + ": " + msg)
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to output. A non-nil error results in a call to fatal.
|
||||
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
|
||||
encoder := json.NewEncoder(output)
|
||||
if !short {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
var marshalerError *json.MarshalerError
|
||||
if errors.As(err, &marshalerError) && marshalerError != nil {
|
||||
// this likely indicates an implementation error in hst
|
||||
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
|
||||
fatal("cannot write json: " + err.Error())
|
||||
}
|
||||
}
|
||||
107
cmd/hakurei/json_test.go
Normal file
107
cmd/hakurei/json_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
_ "unsafe"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
//go:linkname decodeJSON hakurei.app/cmd/hakurei.decodeJSON
|
||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any)
|
||||
|
||||
func TestDecodeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
t reflect.Type
|
||||
data string
|
||||
want any
|
||||
msg string
|
||||
}{
|
||||
{"success", reflect.TypeFor[uintptr](), "3735928559\n", uintptr(0xdeadbeef), ""},
|
||||
|
||||
{"syntax", reflect.TypeFor[*int](), "\x00", nil,
|
||||
`cannot load sample: invalid character '\x00' looking for beginning of value at byte 1`},
|
||||
{"type", reflect.TypeFor[uintptr](), "-1", nil,
|
||||
`cannot load sample: inappropriate number -1 at byte 2`},
|
||||
{"default", reflect.TypeFor[*int](), "{", nil,
|
||||
"cannot load sample: unexpected EOF"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
gotP = reflect.New(tc.t)
|
||||
gotMsg *string
|
||||
)
|
||||
decodeJSON(func(v ...any) {
|
||||
if gotMsg != nil {
|
||||
t.Fatal("fatal called twice")
|
||||
}
|
||||
msg := v[0].(string)
|
||||
gotMsg = &msg
|
||||
}, "load sample", strings.NewReader(tc.data), gotP.Interface())
|
||||
if tc.msg != "" {
|
||||
if gotMsg == nil {
|
||||
t.Errorf("decodeJSON: success, want fatal %q", tc.msg)
|
||||
} else if *gotMsg != tc.msg {
|
||||
t.Errorf("decodeJSON: fatal = %q, want %q", *gotMsg, tc.msg)
|
||||
}
|
||||
} else if gotMsg != nil {
|
||||
t.Errorf("decodeJSON: fatal = %q", *gotMsg)
|
||||
} else if !reflect.DeepEqual(gotP.Elem().Interface(), tc.want) {
|
||||
t.Errorf("decodeJSON: %#v, want %#v", gotP.Elem().Interface(), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//go:linkname encodeJSON hakurei.app/cmd/hakurei.encodeJSON
|
||||
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any)
|
||||
|
||||
func TestEncodeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
v any
|
||||
want string
|
||||
}{
|
||||
{"marshaler", errorJSONMarshaler{},
|
||||
`cannot encode json for main_test.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
||||
{"default", func() {},
|
||||
`cannot write json: json: unsupported type: func()`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var called bool
|
||||
encodeJSON(func(v ...any) {
|
||||
if called {
|
||||
t.Fatal("fatal called twice")
|
||||
}
|
||||
called = true
|
||||
|
||||
if v[0].(string) != tc.want {
|
||||
t.Errorf("encodeJSON: fatal = %q, want %q", v[0].(string), tc.want)
|
||||
}
|
||||
}, nil, false, tc.v)
|
||||
|
||||
if !called {
|
||||
t.Errorf("encodeJSON: success, want fatal %q", tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// errorJSONMarshaler implements json.Marshaler.
|
||||
type errorJSONMarshaler struct{}
|
||||
|
||||
func (errorJSONMarshaler) MarshalJSON() ([]byte, error) { return nil, stub.UniqueError(0xdeadbeef) }
|
||||
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@ -42,10 +41,7 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) {
|
||||
r = os.Stdin
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
||||
log.Fatalf("cannot load configuration: %v", err)
|
||||
}
|
||||
|
||||
decodeJSON(log.Fatal, "load configuration", r, &config)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@ -18,6 +17,7 @@ import (
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// printShowSystem populates and writes a representation of [hst.Info] to output.
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
@ -26,7 +26,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
app.CopyPaths().Copy(&info.Paths, info.User)
|
||||
|
||||
if flagJSON {
|
||||
printJSON(output, short, info)
|
||||
encodeJSON(log.Fatal, output, short, info)
|
||||
return
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
||||
}
|
||||
|
||||
// printShowInstance writes a representation of [state.State] or [hst.Config] to output.
|
||||
func printShowInstance(
|
||||
output io.Writer, now time.Time,
|
||||
instance *state.State, config *hst.Config,
|
||||
@ -46,9 +47,9 @@ func printShowInstance(
|
||||
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
printJSON(output, short, instance)
|
||||
encodeJSON(log.Fatal, output, short, instance)
|
||||
} else {
|
||||
printJSON(output, short, config)
|
||||
encodeJSON(log.Fatal, output, short, config)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -170,6 +171,7 @@ func printShowInstance(
|
||||
return
|
||||
}
|
||||
|
||||
// printPs writes a representation of active instances to output.
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||
var entries state.Entries
|
||||
if e, err := state.Join(s); err != nil {
|
||||
@ -186,7 +188,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
||||
for id, instance := range entries {
|
||||
es[id.String()] = instance
|
||||
}
|
||||
printJSON(output, short, es)
|
||||
encodeJSON(log.Fatal, output, short, es)
|
||||
return
|
||||
}
|
||||
|
||||
@ -215,7 +217,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
||||
for i, e := range exp {
|
||||
v[i] = e.s
|
||||
}
|
||||
printJSON(output, short, v)
|
||||
encodeJSON(log.Fatal, output, short, v)
|
||||
} else {
|
||||
for _, e := range exp {
|
||||
mustPrintln(output, e.s[:8])
|
||||
@ -249,40 +251,39 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
||||
}
|
||||
}
|
||||
|
||||
// expandedStateEntry stores [state.State] alongside a string representation of its [state.ID].
|
||||
type expandedStateEntry struct {
|
||||
s string
|
||||
*state.State
|
||||
}
|
||||
|
||||
func printJSON(output io.Writer, short bool, v any) {
|
||||
encoder := json.NewEncoder(output)
|
||||
if !short {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
log.Fatalf("cannot serialise: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// newPrinter returns a configured, wrapped [tabwriter.Writer].
|
||||
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
|
||||
|
||||
// tp wraps [tabwriter.Writer] to provide additional formatting methods.
|
||||
type tp struct{ *tabwriter.Writer }
|
||||
|
||||
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
|
||||
func (p *tp) Printf(format string, a ...any) {
|
||||
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
|
||||
func (p *tp) Println(a ...any) {
|
||||
if _, err := fmt.Fprintln(p, a...); err != nil {
|
||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MustFlush calls the Flush method of [tabwriter.Writer] and calls [log.Fatalf] on a non-nil error.
|
||||
func (p *tp) MustFlush() {
|
||||
if err := p.Writer.Flush(); err != nil {
|
||||
log.Fatalf("cannot flush tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustPrint(output io.Writer, a ...any) {
|
||||
if _, err := fmt.Fprint(output, a...); err != nil {
|
||||
log.Fatalf("cannot print: %v", err)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user