diff --git a/acceptance/panic/output.txt b/acceptance/panic/output.txt new file mode 100644 index 000000000..9dca41c23 --- /dev/null +++ b/acceptance/panic/output.txt @@ -0,0 +1,15 @@ + +>>> [CLI] selftest panic +The Databricks CLI unexpectedly had a fatal error. +Please report this issue to Databricks in the form of a GitHub issue at: +https://github.com/databricks/cli + +CLI Version: [DEV_VERSION] + +Panic Payload: the databricks selftest panic command always panics + +Stack Trace: +goroutine 1 [running]: +runtime/debug.Stack() + +Exit code: 1 diff --git a/acceptance/panic/script b/acceptance/panic/script new file mode 100644 index 000000000..a02466923 --- /dev/null +++ b/acceptance/panic/script @@ -0,0 +1,5 @@ +# We filter anything after runtime/debug.Stack() in the output because the stack +# trace itself is hard to perform replacements on, since it can depend upon the +# exact setup of where the modules are installed in your Go setup, memory addresses +# at runtime etc. +trace $CLI selftest panic 2>&1 | sed '/runtime\/debug\.Stack()/q' diff --git a/acceptance/panic/test.toml b/acceptance/panic/test.toml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/cmd.go b/cmd/cmd.go index 5d835409f..4f5337fd3 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -12,6 +12,7 @@ import ( "github.com/databricks/cli/cmd/fs" "github.com/databricks/cli/cmd/labs" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/cmd/selftest" "github.com/databricks/cli/cmd/sync" "github.com/databricks/cli/cmd/version" "github.com/databricks/cli/cmd/workspace" @@ -74,6 +75,7 @@ func New(ctx context.Context) *cobra.Command { cli.AddCommand(labs.New(ctx)) cli.AddCommand(sync.New()) cli.AddCommand(version.New()) + cli.AddCommand(selftest.New()) return cli } diff --git a/cmd/root/root.go b/cmd/root/root.go index d7adf47f4..04815f48b 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "runtime/debug" "strings" "github.com/databricks/cli/internal/build" @@ -96,11 +97,35 @@ func flagErrorFunc(c *cobra.Command, err error) error { // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute(ctx context.Context, cmd *cobra.Command) error { - // TODO: deferred panic recovery +func Execute(ctx context.Context, cmd *cobra.Command) (err error) { + defer func() { + r := recover() + + // No panic. Return normally. + if r == nil { + return + } + + version := build.GetInfo().Version + trace := debug.Stack() + + // Set the error so that the CLI exits with a non-zero exit code. + err = fmt.Errorf("panic: %v", r) + + fmt.Fprintf(cmd.ErrOrStderr(), `The Databricks CLI unexpectedly had a fatal error. +Please report this issue to Databricks in the form of a GitHub issue at: +https://github.com/databricks/cli + +CLI Version: %s + +Panic Payload: %v + +Stack Trace: +%s`, version, r, string(trace)) + }() // Run the command - cmd, err := cmd.ExecuteContextC(ctx) + cmd, err = cmd.ExecuteContextC(ctx) if err != nil && !errors.Is(err, ErrAlreadyPrinted) { // If cmdio logger initialization succeeds, then this function logs with the // initialized cmdio logger, otherwise with the default cmdio logger diff --git a/cmd/selftest/panic.go b/cmd/selftest/panic.go new file mode 100644 index 000000000..58d8b24e5 --- /dev/null +++ b/cmd/selftest/panic.go @@ -0,0 +1,12 @@ +package selftest + +import "github.com/spf13/cobra" + +func newPanic() *cobra.Command { + return &cobra.Command{ + Use: "panic", + Run: func(cmd *cobra.Command, args []string) { + panic("the databricks selftest panic command always panics") + }, + } +} diff --git a/cmd/selftest/selftest.go b/cmd/selftest/selftest.go new file mode 100644 index 000000000..7d8cfcb76 --- /dev/null +++ b/cmd/selftest/selftest.go @@ -0,0 +1,16 @@ +package selftest + +import ( + "github.com/spf13/cobra" +) + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: "selftest", + Short: "Non functional CLI commands that are useful for testing", + Hidden: true, + } + + cmd.AddCommand(newPanic()) + return cmd +}