From 935a42d3fa86ef2054bf4db127e44a0e5af1d7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= Date: Mon, 7 Aug 2023 16:20:30 -0300 Subject: [PATCH] Catch panic from bbolt open connection --- cmd/run.go | 48 +++++---- cmd/run_internal_test.go | 17 ++-- cmd/run_test.go | 98 ++++++++++++++++++- pkg/api/api_test.go | 13 +++ pkg/api/diagnostic_test.go | 34 +++++-- pkg/api/error.go | 40 ++++++++ pkg/api/testdata/diagnostics_request.json | 9 -- .../diagnostics_request_template.json | 9 ++ pkg/offline/error.go | 41 ++++++++ pkg/offline/offline.go | 79 ++++++++------- pkg/wakaerror/wakaerror.go | 6 ++ 11 files changed, 315 insertions(+), 79 deletions(-) delete mode 100644 pkg/api/testdata/diagnostics_request.json create mode 100644 pkg/api/testdata/diagnostics_request_template.json create mode 100644 pkg/offline/error.go diff --git a/cmd/run.go b/cmd/run.go index 11791114..607a6fc5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -31,6 +31,7 @@ import ( "github.com/wakatime/wakatime-cli/pkg/log" "github.com/wakatime/wakatime-cli/pkg/offline" "github.com/wakatime/wakatime-cli/pkg/vipertools" + "github.com/wakatime/wakatime-cli/pkg/wakaerror" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -259,7 +260,9 @@ func RunCmdWithOfflineSync(v *viper.Viper, verbose bool, sendDiagsOnErrors bool, os.Exit(exitCode) } - os.Exit(runCmd(v, verbose, sendDiagsOnErrors, offlinesync.Run)) + exitCode = runCmd(v, verbose, sendDiagsOnErrors, offlinesync.Run) + + os.Exit(exitCode) } // runCmd contains the main logic of RunCmd. @@ -297,8 +300,17 @@ func runCmd(v *viper.Viper, verbose bool, sendDiagsOnErrors bool, cmd cmdFn) int // run command exitCode, err := cmd(v) - if err != nil && (verbose || canLogError(err)) { - log.Errorf("failed to run command: %s", err) + // nolint:nestif + if err != nil { + if errwaka, ok := err.(wakaerror.Error); ok { + sendDiagsOnErrors = sendDiagsOnErrors || errwaka.SendDiagsOnErrors() + // if verbose is not set, use the value from the error + verbose = verbose || errwaka.ShouldLogError() + } + + if verbose { + log.Errorf("failed to run command: %s", err) + } resetLogs() @@ -330,22 +342,24 @@ func saveHeartbeats(v *viper.Viper) { func sendDiagnostics(v *viper.Viper, d diagnostics) error { paramAPI, err := params.LoadAPIParams(v) + // prevent sending diags for api key errors + if err != nil && !errors.As(err, &api.ErrAuth{}) { + return fmt.Errorf("failed to load API parameters: %s", err) + } + + c, err := cmdapi.NewClient(paramAPI) if err != nil { - // Prevent sending diags for api key errors. - if !errors.As(err, &api.ErrAuth{}) { - return fmt.Errorf("failed to load API parameters: %s", err) - } + log.Warnf("failed to initialize api client: %s", err) - // Prevent sending diags for api connection errors. - if !errors.As(err, &api.ErrBackoff{}) { - return fmt.Errorf("failed to load API parameters: %s", err) + // try without authencation + c, err = cmdapi.NewClientWithoutAuth(paramAPI) + if err != nil { + return fmt.Errorf("failed to initialize api client without auth: %s", err) } } - c, err := cmdapi.NewClientWithoutAuth(paramAPI) - if err != nil { - return fmt.Errorf("failed to initialize api client: %s", err) - } + // foce disable ssl verification + api.WithDisableSSLVerify()(c) diagnostics := []diagnostic.Diagnostic{ diagnostic.Error(d.OriginalError), @@ -353,8 +367,6 @@ func sendDiagnostics(v *viper.Viper, d diagnostics) error { diagnostic.Stack(d.Stack), } - api.WithDisableSSLVerify()(c) - err = c.SendDiagnostics(paramAPI.Plugin, d.Panicked, diagnostics...) if err != nil { return fmt.Errorf("failed to send diagnostics to the API: %s", err) @@ -377,7 +389,3 @@ func captureLogs(dest io.Writer) func() { log.SetOutput(logOutput) } } - -func canLogError(err error) bool { - return !errors.As(err, &api.ErrBackoff{}) -} diff --git a/cmd/run_internal_test.go b/cmd/run_internal_test.go index 142339a1..46447599 100644 --- a/cmd/run_internal_test.go +++ b/cmd/run_internal_test.go @@ -53,7 +53,7 @@ func TestRunCmd_ErrOfflineEnqueue(t *testing.T) { router.HandleFunc("/plugins/errors", func(w http.ResponseWriter, req *http.Request) { // check request assert.Equal(t, http.MethodPost, req.Method) - assert.Nil(t, req.Header["Authorization"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json") @@ -63,12 +63,14 @@ func TestRunCmd_ErrOfflineEnqueue(t *testing.T) { require.NoError(t, err) var diagnostics struct { - Platform string `json:"platform"` - Architecture string `json:"architecture"` - CliVersion string `json:"cli_version"` - Editor string `json:"editor"` - Logs string `json:"logs"` - Stack string `json:"stacktrace"` + Architecture string `json:"architecture"` + CliVersion string `json:"cli_version"` + Editor string `json:"editor"` + Logs string `json:"logs"` + OriginalError string `json:"error_message"` + Platform string `json:"platform"` + Plugin string `json:"plugin"` + Stack string `json:"stacktrace"` } err = json.Unmarshal(body, &diagnostics) @@ -76,6 +78,7 @@ func TestRunCmd_ErrOfflineEnqueue(t *testing.T) { expectedBodyStr := fmt.Sprintf( string(expectedBodyTpl), + jsonEscape(t, diagnostics.OriginalError), jsonEscape(t, diagnostics.Logs), jsonEscape(t, diagnostics.Stack), ) diff --git a/cmd/run_test.go b/cmd/run_test.go index 13e72d43..29de4dfb 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/wakatime/wakatime-cli/cmd" + "github.com/wakatime/wakatime-cli/pkg/offline" "github.com/wakatime/wakatime-cli/pkg/version" "github.com/spf13/viper" @@ -172,7 +173,7 @@ func TestRunCmd_SendDiagnostics_Error(t *testing.T) { // check request assert.Equal(t, http.MethodPost, req.Method) - assert.Nil(t, req.Header["Authorization"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json") @@ -263,7 +264,7 @@ func TestRunCmd_SendDiagnostics_Panic(t *testing.T) { // check request assert.Equal(t, http.MethodPost, req.Method) - assert.Nil(t, req.Header["Authorization"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_panic_template.json") @@ -355,7 +356,7 @@ func TestRunCmd_SendDiagnostics_NoLogs_Panic(t *testing.T) { // check request assert.Equal(t, http.MethodPost, req.Method) - assert.Nil(t, req.Header["Authorization"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_panic_no_logs_template.json") @@ -404,6 +405,97 @@ func TestRunCmd_SendDiagnostics_NoLogs_Panic(t *testing.T) { assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) } +func TestRunCmd_SendDiagnostics_WakaError(t *testing.T) { + // this is exclusively run in subprocess + if os.Getenv("TEST_RUN") == "1" { + version.OS = "some os" + version.Arch = "some architecture" + version.Version = "some version" + + tmpDir := t.TempDir() + + offlineQueueFile, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + logFile, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + v := viper.New() + v.Set("api-url", os.Getenv("TEST_SERVER_URL")) + v.Set("entity", "/path/to/file") + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("log-file", logFile.Name()) + v.Set("log-to-stdout", true) + v.Set("offline-queue-file", offlineQueueFile.Name()) + v.Set("plugin", "vim") + + cmd.RunCmd(v, false, false, func(v *viper.Viper) (int, error) { + return 42, offline.ErrOpenDB{Err: errors.New("fail")} + }) + + return + } + + testServerURL, router, tearDown := setupTestServer() + defer tearDown() + + var numCalls int + + router.HandleFunc("/plugins/errors", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + // check request + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) + + expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json") + require.NoError(t, err) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + var diagnostics struct { + Architecture string `json:"architecture"` + CliVersion string `json:"cli_version"` + Logs string `json:"logs"` + OriginalError string `json:"error_message"` + Platform string `json:"platform"` + Plugin string `json:"plugin"` + Stack string `json:"stacktrace"` + } + + err = json.Unmarshal(body, &diagnostics) + require.NoError(t, err) + + expectedBodyStr := fmt.Sprintf( + string(expectedBodyTpl), + jsonEscape(t, diagnostics.OriginalError), + jsonEscape(t, diagnostics.Logs), + jsonEscape(t, diagnostics.Stack), + ) + + assert.JSONEq(t, expectedBodyStr, string(body)) + + // send response + w.WriteHeader(http.StatusCreated) + }) + + // run command in another runner, to effectively test os.Exit() + cmd := exec.Command(os.Args[0], "-test.run=TestRunCmd_SendDiagnostics_WakaError") // nolint:gosec + cmd.Env = append(os.Environ(), "TEST_RUN=1") + cmd.Env = append(cmd.Env, fmt.Sprintf("TEST_SERVER_URL=%s", testServerURL)) + + err := cmd.Run() + + e, ok := err.(*exec.ExitError) + require.True(t, ok) + + assert.Equal(t, 42, e.ExitCode()) + + assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) +} + func TestRunCmdWithOfflineSync(t *testing.T) { // this is exclusively run in subprocess if os.Getenv("TEST_RUN") == "1" { diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index ca66b2f4..fe28977e 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -1,8 +1,12 @@ package api_test import ( + "encoding/json" "net/http" "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" ) func setupTestServer() (string, *http.ServeMux, func()) { @@ -11,3 +15,12 @@ func setupTestServer() (string, *http.ServeMux, func()) { return srv.URL, router, func() { srv.Close() } } + +func jsonEscape(t *testing.T, i string) string { + b, err := json.Marshal(i) + require.NoError(t, err) + + s := string(b) + + return s[1 : len(s)-1] +} diff --git a/pkg/api/diagnostic_test.go b/pkg/api/diagnostic_test.go index 027c464d..9fb37aeb 100644 --- a/pkg/api/diagnostic_test.go +++ b/pkg/api/diagnostic_test.go @@ -1,6 +1,8 @@ package api_test import ( + "encoding/json" + "fmt" "io" "net/http" "os" @@ -26,25 +28,45 @@ func TestClient_SendDiagnostics(t *testing.T) { // check method and headers assert.Equal(t, http.MethodPost, req.Method) - assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) assert.Nil(t, req.Header["Authorization"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) // check body - expectedBody, err := os.ReadFile("testdata/diagnostics_request.json") + expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json") require.NoError(t, err) body, err := io.ReadAll(req.Body) require.NoError(t, err) - assert.JSONEq(t, string(expectedBody), string(body)) + var diagnostics struct { + Architecture string `json:"architecture"` + CliVersion string `json:"cli_version"` + Logs string `json:"logs"` + OriginalError string `json:"error_message"` + Platform string `json:"platform"` + Plugin string `json:"plugin"` + Stack string `json:"stacktrace"` + } + + err = json.Unmarshal(body, &diagnostics) + require.NoError(t, err) + + expectedBodyStr := fmt.Sprintf( + string(expectedBodyTpl), + jsonEscape(t, diagnostics.OriginalError), + jsonEscape(t, diagnostics.Logs), + jsonEscape(t, diagnostics.Stack), + ) + + assert.JSONEq(t, expectedBodyStr, string(body)) // write response w.WriteHeader(http.StatusCreated) }) - version.OS = "linux" - version.Arch = "amd64" - version.Version = "" + version.OS = "some os" + version.Arch = "some architecture" + version.Version = "some version" diagnostics := []diagnostic.Diagnostic{ diagnostic.Error("some error"), diff --git a/pkg/api/error.go b/pkg/api/error.go index e1c68607..e308edb5 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -29,6 +29,16 @@ func (e Err) Message() string { return fmt.Sprintf("api error: %s", e.Err) } +// SendDiagsOnErrors method to implement wakaerror.SendDiagsOnErrors interface. +func (Err) SendDiagsOnErrors() bool { + return false +} + +// ShouldLogError method to implement wakaerror.ShouldLogError interface. +func (Err) ShouldLogError() bool { + return true +} + // ErrAuth represents an authentication error. type ErrAuth struct { Err error @@ -51,6 +61,16 @@ func (e ErrAuth) Message() string { return fmt.Sprintf("invalid api key... find yours at wakatime.com/api-key. %s", e.Err.Error()) } +// SendDiagsOnErrors method to implement wakaerror.SendDiagsOnErrors interface. +func (ErrAuth) SendDiagsOnErrors() bool { + return false +} + +// ShouldLogError method to implement wakaerror.ShouldLogError interface. +func (ErrAuth) ShouldLogError() bool { + return true +} + // ErrBadRequest represents a 400 response from the API. type ErrBadRequest struct { Err error @@ -73,6 +93,16 @@ func (e ErrBadRequest) Message() string { return fmt.Sprintf("bad request: %s", e.Err) } +// SendDiagsOnErrors method to implement wakaerror.SendDiagsOnErrors interface. +func (ErrBadRequest) SendDiagsOnErrors() bool { + return false +} + +// ShouldLogError method to implement wakaerror.ShouldLogError interface. +func (ErrBadRequest) ShouldLogError() bool { + return true +} + // ErrBackoff means we send later because currently rate limited. type ErrBackoff struct { Err error @@ -94,3 +124,13 @@ func (ErrBackoff) ExitCode() int { func (e ErrBackoff) Message() string { return fmt.Sprintf("rate limited: %s", e.Err) } + +// SendDiagsOnErrors method to implement wakaerror.SendDiagsOnErrors interface. +func (ErrBackoff) SendDiagsOnErrors() bool { + return false +} + +// ShouldLogError method to implement wakaerror.ShouldLogError interface. +func (ErrBackoff) ShouldLogError() bool { + return false +} diff --git a/pkg/api/testdata/diagnostics_request.json b/pkg/api/testdata/diagnostics_request.json deleted file mode 100644 index d97212ac..00000000 --- a/pkg/api/testdata/diagnostics_request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "architecture": "amd64", - "cli_version": "", - "error_message": "some error", - "logs": "some logs", - "platform": "linux", - "plugin": "vim", - "stacktrace": "some stack" -} diff --git a/pkg/api/testdata/diagnostics_request_template.json b/pkg/api/testdata/diagnostics_request_template.json new file mode 100644 index 00000000..5d981a8a --- /dev/null +++ b/pkg/api/testdata/diagnostics_request_template.json @@ -0,0 +1,9 @@ +{ + "architecture": "some architecture", + "cli_version": "some version", + "error_message": "%s", + "logs": "%s", + "platform": "some os", + "plugin": "vim", + "stacktrace": "%s" +} diff --git a/pkg/offline/error.go b/pkg/offline/error.go new file mode 100644 index 00000000..4e568bc2 --- /dev/null +++ b/pkg/offline/error.go @@ -0,0 +1,41 @@ +package offline + +import ( + "fmt" + + "github.com/wakatime/wakatime-cli/pkg/exitcode" + "github.com/wakatime/wakatime-cli/pkg/wakaerror" +) + +// ErrOpenDB is an error returned when the database cannot be opened. +type ErrOpenDB struct { + Err error +} + +var _ wakaerror.Error = ErrOpenDB{} + +// Error method to implement error interface. +func (e ErrOpenDB) Error() string { + return e.Err.Error() +} + +// Message method to implement wakaerror.Error interface. +func (e ErrOpenDB) Message() string { + return fmt.Sprintf("failed to open db file: %s", e.Err) +} + +// ExitCode method to implement wakaerror.Error interface. +func (ErrOpenDB) ExitCode() int { + // Despite the error, we don't want to exit with an error code. + return exitcode.Success +} + +// SendDiagsOnErrors method to implement wakaerror.SendDiagsOnErrors interface. +func (ErrOpenDB) SendDiagsOnErrors() bool { + return true +} + +// ShouldLogError method to implement wakaerror.ShouldLogError interface. +func (ErrOpenDB) ShouldLogError() bool { + return true +} diff --git a/pkg/offline/offline.go b/pkg/offline/offline.go index cef87f0c..18612f09 100644 --- a/pkg/offline/offline.go +++ b/pkg/offline/offline.go @@ -7,6 +7,7 @@ import ( "math" "net/http" "path/filepath" + "runtime/debug" "time" "github.com/wakatime/wakatime-cli/pkg/api" @@ -67,9 +68,10 @@ func WithQueue(filepath string) heartbeat.HandleOption { requeueErr := pushHeartbeatsWithRetry(filepath, hh) if requeueErr != nil { return nil, fmt.Errorf( - "failed to push heartbeats to queue after api error: %s. error: %s", + "failed to push heartbeats to queue after api error: %w. error: %w", requeueErr, - err) + err, + ) } return nil, err @@ -158,7 +160,7 @@ func Sync(filepath string, syncLimit int) func(next heartbeat.Handle) error { if err != nil { requeueErr := pushHeartbeatsWithRetry(filepath, hh) if requeueErr != nil { - log.Warnf("failed to push heatbeats to queue after api error: %s", requeueErr) + log.Warnf("failed to push heartbeats to queue after api error: %s", requeueErr) } return err @@ -166,7 +168,7 @@ func Sync(filepath string, syncLimit int) func(next heartbeat.Handle) error { err = handleResults(filepath, results, hh) if err != nil { - return fmt.Errorf("failed to handle heatbeats api results: %s", err) + return fmt.Errorf("failed to handle heartbeats api results: %s", err) } } @@ -213,7 +215,7 @@ func handleResults(filepath string, results []heartbeat.Result, hh []heartbeat.H err = pushHeartbeatsWithRetry(filepath, withInvalidStatus) if err != nil { - log.Warnf("failed to push heatbeats with invalid status to queue: %s", err) + log.Warnf("failed to push heartbeats with invalid status to queue: %s", err) } } @@ -226,7 +228,7 @@ func handleResults(filepath string, results []heartbeat.Result, hh []heartbeat.H err = pushHeartbeatsWithRetry(filepath, hh[start:]) if err != nil { - log.Warnf("failed to push leftover heatbeats to queue: %s", err) + log.Warnf("failed to push leftover heartbeats to queue: %s", err) } } @@ -234,16 +236,12 @@ func handleResults(filepath string, results []heartbeat.Result, hh []heartbeat.H } func popHeartbeats(filepath string, limit int) ([]heartbeat.Heartbeat, error) { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 10 * time.Minute}) + db, close, err := openDB(filepath) if err != nil { - return nil, fmt.Errorf("failed to open db connection: %s", err) + return nil, err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -308,16 +306,12 @@ func pushHeartbeatsWithRetry(filepath string, hh []heartbeat.Heartbeat) error { } func pushHeartbeats(filepath string, hh []heartbeat.Heartbeat) error { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 10 * time.Minute}) + db, close, err := openDB(filepath) if err != nil { - return fmt.Errorf("failed to open db connection: %s", err) + return err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -340,16 +334,12 @@ func pushHeartbeats(filepath string, hh []heartbeat.Heartbeat) error { // CountHeartbeats returns the total number of heartbeats in the offline db. func CountHeartbeats(filepath string) (int, error) { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 30 * time.Second}) + db, close, err := openDB(filepath) if err != nil { - return 0, fmt.Errorf("failed to open db connection: %s", err) + return 0, err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -375,16 +365,12 @@ func CountHeartbeats(filepath string) (int, error) { // ReadHeartbeats reads the informed heartbeats in the offline db. func ReadHeartbeats(filepath string, limit int) ([]heartbeat.Heartbeat, error) { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 30 * time.Second}) + db, close, err := openDB(filepath) if err != nil { - return nil, fmt.Errorf("failed to open db connection: %s", err) + return nil, err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -410,6 +396,31 @@ func ReadHeartbeats(filepath string, limit int) ([]heartbeat.Heartbeat, error) { return hh, nil } +// openDB opens a connection to the offline db. +// It returns the pointer to bolt.DB, a function to close the connection and an error. +func openDB(filepath string) (*bolt.DB, func(), error) { + var err error + + defer func() { + if r := recover(); r != nil { + err = ErrOpenDB{Err: fmt.Errorf("panicked: %v", r)} + + log.Errorf("%w. Stack: %s", err, string(debug.Stack())) + } + }() + + db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 30 * time.Second}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open db file: %s", err) + } + + return db, func() { + if err := db.Close(); err != nil { + log.Debugf("failed to close db file: %s", err) + } + }, err +} + // Queue is a db client to temporarily store heartbeats in bolt db, in case heartbeat // sending to wakatime api is not possible. Transaction handling is left to the user // via the passed in transaction. diff --git a/pkg/wakaerror/wakaerror.go b/pkg/wakaerror/wakaerror.go index 14467b2b..6211c08b 100644 --- a/pkg/wakaerror/wakaerror.go +++ b/pkg/wakaerror/wakaerror.go @@ -2,7 +2,13 @@ package wakaerror // Error is a custom error interface. type Error interface { + // ExitCode returns the exit code for the error. ExitCode() int + // Message returns the error message. Message() string + // SendDiagsOnErrors returns true when diagnostics should be sent on error. + SendDiagsOnErrors() bool + // ShouldLogError returns true when error should be logged. + ShouldLogError() bool error }