github.com/henvic/wedeploycli@v1.7.6-0.20200319005353-3630f582f284/errorhandler/errorhandler.go (about)

     1  /*
     2  Package errorhandler provides a error handling system to be used as
     3  root.Execute() error handler or on watches. It should not be used somewhere else.
     4  */
     5  package errorhandler
     6  
     7  import (
     8  	"fmt"
     9  	"net/url"
    10  	"os"
    11  	"reflect"
    12  	"runtime"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/hashicorp/errwrap"
    17  	"github.com/henvic/wedeploycli/apihelper"
    18  	"github.com/henvic/wedeploycli/color"
    19  	"github.com/henvic/wedeploycli/defaults"
    20  	"github.com/henvic/wedeploycli/templates"
    21  	"github.com/henvic/wedeploycli/verbose"
    22  )
    23  
    24  const panicTemplate = `An unrecoverable error has occurred.
    25  Please report this error at
    26  https://github.com/wedeploy/cli/issues/
    27  
    28  %s
    29  Time: %s
    30  %s`
    31  
    32  // CommandName for the local message repository
    33  var CommandName string
    34  
    35  var afterError func()
    36  
    37  // SetAfterError defines a function to run after global error
    38  func SetAfterError(f func()) {
    39  	afterError = f
    40  }
    41  
    42  // RunAfterError runs code after a global error, just before exiting
    43  func RunAfterError() {
    44  	if afterError != nil {
    45  		afterError()
    46  	}
    47  }
    48  
    49  // Handle error to a more friendly format
    50  func Handle(err error) error {
    51  	if err == nil {
    52  		return nil
    53  	}
    54  
    55  	return (&handler{
    56  		err: err,
    57  	}).handle()
    58  }
    59  
    60  type handler struct {
    61  	err error
    62  }
    63  
    64  type messages map[string]string
    65  
    66  func extractParentCommand(cmd string) string {
    67  	var splitCmd = strings.Split(cmd, " ")
    68  	return strings.Join(splitCmd[:len(splitCmd)-1], " ")
    69  }
    70  
    71  type data struct {
    72  	Err     error
    73  	Context map[string]interface{}
    74  }
    75  
    76  // tryGetPersonalizedMessage tries to get a human-friendly error message from the
    77  // command / local error message lists falling back to the parent command
    78  // and at last instance to the global
    79  func tryGetPersonalizedMessage(cmd, reason string, d data) (string, bool) {
    80  	local := cmd
    81  getMessage:
    82  	if haystack, ok := errorReasonCommandMessageOverrides[local]; ok {
    83  		if msg, has := haystack[reason]; has {
    84  			return msg, true
    85  		}
    86  	}
    87  
    88  	if local = extractParentCommand(local); local != "" {
    89  		goto getMessage
    90  	}
    91  
    92  	msg, ok := errorReasonMessage[reason]
    93  
    94  	if !ok {
    95  		return msg, ok
    96  	}
    97  
    98  	personalizedMsg, err := templates.Execute(msg, d)
    99  
   100  	if err != nil {
   101  		verbose.Debug(errwrap.Wrapf("error getting personalized message: {{err}}", err))
   102  		return msg, ok
   103  	}
   104  
   105  	return personalizedMsg, ok
   106  }
   107  
   108  func (h *handler) handle() error {
   109  	if af := errwrap.GetType(h.err, apihelper.APIFault{}); af != nil {
   110  		return h.handleAPIFaultError()
   111  	}
   112  
   113  	switch nerr, ok := h.err.(*url.Error); {
   114  	case !ok:
   115  		return h.err
   116  	case nerr.Timeout():
   117  		return errwrap.Wrapf("network connection timed out:\n{{err}}", h.err)
   118  	default:
   119  		return errwrap.Wrapf("network connection error:\n{{err}}", h.err)
   120  	}
   121  }
   122  
   123  func (h *handler) handleAPIFaultError() error {
   124  	var af, ok = errwrap.GetType(h.err, apihelper.APIFault{}).(apihelper.APIFault)
   125  
   126  	if !ok {
   127  		return h.err
   128  	}
   129  
   130  	var msgs []string
   131  	// we want to fallback to the default error if no friendly messages are found
   132  	var anyFriendly bool
   133  
   134  	for _, e := range af.Errors {
   135  		d := data{
   136  			Err:     h.err,
   137  			Context: e.Context,
   138  		}
   139  
   140  		rtm, ok := tryGetPersonalizedMessage(CommandName, e.Reason, d)
   141  
   142  		if ok {
   143  			anyFriendly = true
   144  			msgs = append(msgs, rtm)
   145  		} else {
   146  			msgs = append(msgs, e.Reason+": "+e.Context.Message())
   147  		}
   148  
   149  	}
   150  
   151  	if !anyFriendly {
   152  		return h.err
   153  	}
   154  
   155  	var l = strings.Join(msgs, "\n")
   156  
   157  	var msg = strings.Replace(h.err.Error(), af.Error(), l, -1)
   158  
   159  	return errwrap.Wrapf(msg, h.err)
   160  }
   161  
   162  // GetTypes get a list of error types separated by ":"
   163  func GetTypes(err error) string {
   164  	var types []string
   165  
   166  	errwrap.Walk(err, func(err error) {
   167  		r := reflect.TypeOf(err)
   168  		types = append(types, r.String())
   169  	})
   170  
   171  	return strings.Join(types, ":")
   172  }
   173  
   174  // Info prints useful system information for debugging
   175  func Info() {
   176  	var version = fmt.Sprintf("Version: %s %s/%s (runtime: %s)",
   177  		defaults.Version,
   178  		runtime.GOOS,
   179  		runtime.GOARCH,
   180  		runtime.Version())
   181  
   182  	if defaults.Build != "" {
   183  		version += "\nbuild:" + defaults.Build
   184  	}
   185  
   186  	_, _ = fmt.Fprintln(os.Stderr, color.Format(color.FgRed, panicTemplate,
   187  		version,
   188  		time.Now().Format(time.RubyDate), systemInfo()))
   189  }
   190  
   191  func systemInfo() string {
   192  	var m runtime.MemStats
   193  	runtime.ReadMemStats(&m)
   194  
   195  	return fmt.Sprintf(`goroutines: %v | cgo calls: %v
   196  CPUs: %v | Pointer lookups: %v
   197  `, runtime.NumGoroutine(), runtime.NumCgoCall(), runtime.NumCPU(), m.Lookups)
   198  }