github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/eval/exception.go (about)

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