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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -227,8 +226,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
} else {
|
} else {
|
||||||
if f, err := os.Open(flagDBusConfigSession); err != nil {
|
if f, err := os.Open(flagDBusConfigSession); err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
} else if err = json.NewDecoder(f).Decode(&config.SessionBus); err != nil {
|
} else {
|
||||||
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
|
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 flagDBusConfigSystem != "nil" {
|
||||||
if f, err := os.Open(flagDBusConfigSystem); err != nil {
|
if f, err := os.Open(flagDBusConfigSystem); err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
} else if err = json.NewDecoder(f).Decode(&config.SystemBus); err != nil {
|
} else {
|
||||||
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
|
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("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("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 })
|
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
||||||
|
|
||||||
return c
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -42,10 +41,7 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) {
|
|||||||
r = os.Stdin
|
r = os.Stdin
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
decodeJSON(log.Fatal, "load configuration", r, &config)
|
||||||
log.Fatalf("cannot load configuration: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -18,6 +17,7 @@ import (
|
|||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// printShowSystem populates and writes a representation of [hst.Info] to output.
|
||||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
@ -26,7 +26,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
|||||||
app.CopyPaths().Copy(&info.Paths, info.User)
|
app.CopyPaths().Copy(&info.Paths, info.User)
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
printJSON(output, short, info)
|
encodeJSON(log.Fatal, output, short, info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +38,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
|||||||
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printShowInstance writes a representation of [state.State] or [hst.Config] to output.
|
||||||
func printShowInstance(
|
func printShowInstance(
|
||||||
output io.Writer, now time.Time,
|
output io.Writer, now time.Time,
|
||||||
instance *state.State, config *hst.Config,
|
instance *state.State, config *hst.Config,
|
||||||
@ -46,9 +47,9 @@ func printShowInstance(
|
|||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
if instance != nil {
|
if instance != nil {
|
||||||
printJSON(output, short, instance)
|
encodeJSON(log.Fatal, output, short, instance)
|
||||||
} else {
|
} else {
|
||||||
printJSON(output, short, config)
|
encodeJSON(log.Fatal, output, short, config)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -170,6 +171,7 @@ func printShowInstance(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printPs writes a representation of active instances to output.
|
||||||
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||||
var entries state.Entries
|
var entries state.Entries
|
||||||
if e, err := state.Join(s); err != nil {
|
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 {
|
for id, instance := range entries {
|
||||||
es[id.String()] = instance
|
es[id.String()] = instance
|
||||||
}
|
}
|
||||||
printJSON(output, short, es)
|
encodeJSON(log.Fatal, output, short, es)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +217,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
|||||||
for i, e := range exp {
|
for i, e := range exp {
|
||||||
v[i] = e.s
|
v[i] = e.s
|
||||||
}
|
}
|
||||||
printJSON(output, short, v)
|
encodeJSON(log.Fatal, output, short, v)
|
||||||
} else {
|
} else {
|
||||||
for _, e := range exp {
|
for _, e := range exp {
|
||||||
mustPrintln(output, e.s[:8])
|
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 {
|
type expandedStateEntry struct {
|
||||||
s string
|
s string
|
||||||
*state.State
|
*state.State
|
||||||
}
|
}
|
||||||
|
|
||||||
func printJSON(output io.Writer, short bool, v any) {
|
// newPrinter returns a configured, wrapped [tabwriter.Writer].
|
||||||
encoder := json.NewEncoder(output)
|
|
||||||
if !short {
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
}
|
|
||||||
if err := encoder.Encode(v); err != nil {
|
|
||||||
log.Fatalf("cannot serialise: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
|
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 }
|
type tp struct{ *tabwriter.Writer }
|
||||||
|
|
||||||
|
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
|
||||||
func (p *tp) Printf(format string, a ...any) {
|
func (p *tp) Printf(format string, a ...any) {
|
||||||
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
||||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
|
||||||
func (p *tp) Println(a ...any) {
|
func (p *tp) Println(a ...any) {
|
||||||
if _, err := fmt.Fprintln(p, a...); err != nil {
|
if _, err := fmt.Fprintln(p, a...); err != nil {
|
||||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
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() {
|
func (p *tp) MustFlush() {
|
||||||
if err := p.Writer.Flush(); err != nil {
|
if err := p.Writer.Flush(); err != nil {
|
||||||
log.Fatalf("cannot flush tabwriter: %v", err)
|
log.Fatalf("cannot flush tabwriter: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustPrint(output io.Writer, a ...any) {
|
func mustPrint(output io.Writer, a ...any) {
|
||||||
if _, err := fmt.Fprint(output, a...); err != nil {
|
if _, err := fmt.Fprint(output, a...); err != nil {
|
||||||
log.Fatalf("cannot print: %v", err)
|
log.Fatalf("cannot print: %v", err)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user