github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/eval/exception.go (about)

     1  package eval
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strconv"
     7  	"syscall"
     8  	"unsafe"
     9  
    10  	"src.elv.sh/pkg/diag"
    11  	"src.elv.sh/pkg/eval/vals"
    12  	"src.elv.sh/pkg/parse"
    13  	"src.elv.sh/pkg/persistent/hash"
    14  )
    15  
    16  // Exception represents exceptions. It is both a Value accessible to Elvish
    17  // code, and can be returned by methods like like (*Evaler).Eval.
    18  type Exception interface {
    19  	error
    20  	diag.Shower
    21  	Reason() error
    22  	StackTrace() *StackTrace
    23  	// This is not strictly necessary, but it makes sure that there is only one
    24  	// implementation of Exception, so that the compiler may de-virtualize this
    25  	// interface.
    26  	isException()
    27  }
    28  
    29  // NewException creates a new Exception.
    30  func NewException(reason error, stackTrace *StackTrace) Exception {
    31  	return &exception{reason, stackTrace}
    32  }
    33  
    34  // Implementation of the Exception interface.
    35  type exception struct {
    36  	reason     error
    37  	stackTrace *StackTrace
    38  }
    39  
    40  // StackTrace represents a stack trace as a linked list of diag.Context. The
    41  // head is the innermost stack.
    42  //
    43  // Since pipelines can call multiple functions in parallel, all the StackTrace
    44  // nodes form a DAG.
    45  type StackTrace struct {
    46  	Head *diag.Context
    47  	Next *StackTrace
    48  }
    49  
    50  // MakeStackTrace creates a new StackTrace from the given Context entries, using
    51  // the first entry as the head.
    52  func MakeStackTrace(entries ...*diag.Context) *StackTrace {
    53  	var s *StackTrace
    54  	for i := len(entries) - 1; i >= 0; i-- {
    55  		s = &StackTrace{Head: entries[i], Next: s}
    56  	}
    57  	return s
    58  }
    59  
    60  // Reason returns the Reason field if err is an Exception. Otherwise it returns
    61  // err itself.
    62  func Reason(err error) error {
    63  	if exc, ok := err.(*exception); ok {
    64  		return exc.reason
    65  	}
    66  	return err
    67  }
    68  
    69  // OK is a pointer to a special value of Exception that represents the absence
    70  // of exception.
    71  var OK = &exception{}
    72  
    73  func (exc *exception) isException() {}
    74  
    75  func (exc *exception) Reason() error { return exc.reason }
    76  
    77  func (exc *exception) StackTrace() *StackTrace { return exc.stackTrace }
    78  
    79  // Error returns the message of the cause of the exception.
    80  func (exc *exception) Error() string { return exc.reason.Error() }
    81  
    82  // Show shows the exception.
    83  func (exc *exception) Show(indent string) string {
    84  	buf := new(bytes.Buffer)
    85  
    86  	var causeDescription string
    87  	if shower, ok := exc.reason.(diag.Shower); ok {
    88  		causeDescription = shower.Show(indent)
    89  	} else if exc.reason == nil {
    90  		causeDescription = "ok"
    91  	} else {
    92  		causeDescription = "\033[31;1m" + exc.reason.Error() + "\033[m"
    93  	}
    94  	fmt.Fprintf(buf, "Exception: %s", causeDescription)
    95  
    96  	if exc.stackTrace != nil {
    97  		buf.WriteString("\n")
    98  		if exc.stackTrace.Next == nil {
    99  			buf.WriteString(exc.stackTrace.Head.ShowCompact(indent))
   100  		} else {
   101  			buf.WriteString(indent + "Traceback:")
   102  			for tb := exc.stackTrace; tb != nil; tb = tb.Next {
   103  				buf.WriteString("\n" + indent + "  ")
   104  				buf.WriteString(tb.Head.Show(indent + "    "))
   105  			}
   106  		}
   107  	}
   108  
   109  	if pipeExcs, ok := exc.reason.(PipelineError); ok {
   110  		buf.WriteString("\n" + indent + "Caused by:")
   111  		for _, e := range pipeExcs.Errors {
   112  			if e == OK {
   113  				continue
   114  			}
   115  			buf.WriteString("\n" + indent + "  " + e.Show(indent+"  "))
   116  		}
   117  	}
   118  
   119  	return buf.String()
   120  }
   121  
   122  // Kind returns "exception".
   123  func (exc *exception) Kind() string {
   124  	return "exception"
   125  }
   126  
   127  // Repr returns a representation of the exception. It is lossy in that it does
   128  // not preserve the stacktrace.
   129  func (exc *exception) Repr(indent int) string {
   130  	if exc.reason == nil {
   131  		return "$ok"
   132  	}
   133  	return "[&reason=" + vals.Repr(exc.reason, indent+1) + "]"
   134  }
   135  
   136  // Equal compares by address.
   137  func (exc *exception) Equal(rhs interface{}) bool {
   138  	return exc == rhs
   139  }
   140  
   141  // Hash returns the hash of the address.
   142  func (exc *exception) Hash() uint32 {
   143  	return hash.Pointer(unsafe.Pointer(exc))
   144  }
   145  
   146  // Bool returns whether this exception has a nil cause; that is, it is $ok.
   147  func (exc *exception) Bool() bool {
   148  	return exc.reason == nil
   149  }
   150  
   151  func (exc *exception) Fields() vals.StructMap { return excFields{exc} }
   152  
   153  type excFields struct{ e *exception }
   154  
   155  func (excFields) IsStructMap()    {}
   156  func (f excFields) Reason() error { return f.e.reason }
   157  
   158  // PipelineError represents the errors of pipelines, in which multiple commands
   159  // may error.
   160  type PipelineError struct {
   161  	Errors []Exception
   162  }
   163  
   164  // Error returns a plain text representation of the pipeline error.
   165  func (pe PipelineError) Error() string {
   166  	b := new(bytes.Buffer)
   167  	b.WriteString("(")
   168  	for i, e := range pe.Errors {
   169  		if i > 0 {
   170  			b.WriteString(" | ")
   171  		}
   172  		if e == nil || e.Reason() == nil {
   173  			b.WriteString("<nil>")
   174  		} else {
   175  			b.WriteString(e.Error())
   176  		}
   177  	}
   178  	b.WriteString(")")
   179  	return b.String()
   180  }
   181  
   182  // MakePipelineError builds an error from the execution results of multiple
   183  // commands in a pipeline.
   184  //
   185  // If all elements are either nil or OK, it returns nil. If there is exactly
   186  // non-nil non-OK Exception, it returns it. Otherwise, it return a PipelineError
   187  // built from the slice, with nil items turned into OK's for easier access from
   188  // Elvish code.
   189  func MakePipelineError(excs []Exception) error {
   190  	newexcs := make([]Exception, len(excs))
   191  	notOK, lastNotOK := 0, 0
   192  	for i, e := range excs {
   193  		if e == nil {
   194  			newexcs[i] = OK
   195  		} else {
   196  			newexcs[i] = e
   197  			if e.Reason() != nil {
   198  				notOK++
   199  				lastNotOK = i
   200  			}
   201  		}
   202  	}
   203  	switch notOK {
   204  	case 0:
   205  		return nil
   206  	case 1:
   207  		return newexcs[lastNotOK]
   208  	default:
   209  		return PipelineError{newexcs}
   210  	}
   211  }
   212  
   213  func (pe PipelineError) Fields() vals.StructMap { return peFields{pe} }
   214  
   215  type peFields struct{ pe PipelineError }
   216  
   217  func (peFields) IsStructMap() {}
   218  
   219  func (f peFields) Type() string { return "pipeline" }
   220  
   221  func (f peFields) Exceptions() vals.List {
   222  	li := vals.EmptyList
   223  	for _, exc := range f.pe.Errors {
   224  		li = li.Cons(exc)
   225  	}
   226  	return li
   227  }
   228  
   229  // Flow is a special type of error used for control flows.
   230  type Flow uint
   231  
   232  // Control flows.
   233  const (
   234  	Return Flow = iota
   235  	Break
   236  	Continue
   237  )
   238  
   239  var flowNames = [...]string{
   240  	"return", "break", "continue",
   241  }
   242  
   243  func (f Flow) Error() string {
   244  	if f >= Flow(len(flowNames)) {
   245  		return fmt.Sprintf("!(BAD FLOW: %d)", f)
   246  	}
   247  	return flowNames[f]
   248  }
   249  
   250  // Show shows the flow "error".
   251  func (f Flow) Show(string) string {
   252  	return "\033[33;1m" + f.Error() + "\033[m"
   253  }
   254  
   255  func (f Flow) Fields() vals.StructMap { return flowFields{f} }
   256  
   257  type flowFields struct{ f Flow }
   258  
   259  func (flowFields) IsStructMap() {}
   260  
   261  func (f flowFields) Type() string { return "flow" }
   262  func (f flowFields) Name() string { return f.f.Error() }
   263  
   264  // ExternalCmdExit contains the exit status of external commands.
   265  type ExternalCmdExit struct {
   266  	syscall.WaitStatus
   267  	CmdName string
   268  	Pid     int
   269  }
   270  
   271  // NewExternalCmdExit constructs an error for representing a non-zero exit from
   272  // an external command.
   273  func NewExternalCmdExit(name string, ws syscall.WaitStatus, pid int) error {
   274  	if ws.Exited() && ws.ExitStatus() == 0 {
   275  		return nil
   276  	}
   277  	return ExternalCmdExit{ws, name, pid}
   278  }
   279  
   280  func (exit ExternalCmdExit) Error() string {
   281  	ws := exit.WaitStatus
   282  	quotedName := parse.Quote(exit.CmdName)
   283  	switch {
   284  	case ws.Exited():
   285  		return quotedName + " exited with " + strconv.Itoa(ws.ExitStatus())
   286  	case ws.Signaled():
   287  		causeDescription := quotedName + " killed by signal " + ws.Signal().String()
   288  		if ws.CoreDump() {
   289  			causeDescription += " (core dumped)"
   290  		}
   291  		return causeDescription
   292  	case ws.Stopped():
   293  		causeDescription := quotedName + " stopped by signal " + fmt.Sprintf("%s (pid=%d)", ws.StopSignal(), exit.Pid)
   294  		trap := ws.TrapCause()
   295  		if trap != -1 {
   296  			causeDescription += fmt.Sprintf(" (trapped %v)", trap)
   297  		}
   298  		return causeDescription
   299  	default:
   300  		return fmt.Sprint(quotedName, " has unknown WaitStatus ", ws)
   301  	}
   302  }
   303  
   304  func (exit ExternalCmdExit) Fields() vals.StructMap {
   305  	ws := exit.WaitStatus
   306  	f := exitFieldsCommon{exit}
   307  	switch {
   308  	case ws.Exited():
   309  		return exitFieldsExited{f}
   310  	case ws.Signaled():
   311  		return exitFieldsSignaled{f}
   312  	case ws.Stopped():
   313  		return exitFieldsStopped{f}
   314  	default:
   315  		return exitFieldsUnknown{f}
   316  	}
   317  }
   318  
   319  type exitFieldsCommon struct{ e ExternalCmdExit }
   320  
   321  func (exitFieldsCommon) IsStructMap()      {}
   322  func (f exitFieldsCommon) CmdName() string { return f.e.CmdName }
   323  func (f exitFieldsCommon) Pid() string     { return strconv.Itoa(f.e.Pid) }
   324  
   325  type exitFieldsExited struct{ exitFieldsCommon }
   326  
   327  func (exitFieldsExited) Type() string         { return "external-cmd/exited" }
   328  func (f exitFieldsExited) ExitStatus() string { return strconv.Itoa(f.e.ExitStatus()) }
   329  
   330  type exitFieldsSignaled struct{ exitFieldsCommon }
   331  
   332  func (f exitFieldsSignaled) Type() string         { return "external-cmd/signaled" }
   333  func (f exitFieldsSignaled) SignalName() string   { return f.e.Signal().String() }
   334  func (f exitFieldsSignaled) SignalNumber() string { return strconv.Itoa(int(f.e.Signal())) }
   335  func (f exitFieldsSignaled) CoreDumped() bool     { return f.e.CoreDump() }
   336  
   337  type exitFieldsStopped struct{ exitFieldsCommon }
   338  
   339  func (f exitFieldsStopped) Type() string         { return "external-cmd/stopped" }
   340  func (f exitFieldsStopped) SignalName() string   { return f.e.StopSignal().String() }
   341  func (f exitFieldsStopped) SignalNumber() string { return strconv.Itoa(int(f.e.StopSignal())) }
   342  func (f exitFieldsStopped) TrapCause() int       { return f.e.TrapCause() }
   343  
   344  type exitFieldsUnknown struct{ exitFieldsCommon }
   345  
   346  func (exitFieldsUnknown) Type() string { return "external-cmd/unknown" }