elvish/pkg/eval/exception.go
2020-09-04 21:04:58 +01:00

316 lines
8.1 KiB
Go

package eval
import (
"bytes"
"fmt"
"strconv"
"syscall"
"unsafe"
"github.com/elves/elvish/pkg/diag"
"github.com/elves/elvish/pkg/eval/vals"
"github.com/elves/elvish/pkg/parse"
"github.com/xiaq/persistent/hash"
)
// Exception represents an elvish exception. It is both a Value accessible to
// elvishscript, and the type of error returned by public facing evaluation
// methods like (*Evaler)PEval.
type Exception struct {
Reason error
StackTrace *StackTrace
}
// StackTrace represents a stack trace as a linked list of diag.Context. The
// head is the innermost stack.
//
// Since pipelines can call multiple functions in parallel, all the StackTrace
// nodes form a DAG.
type StackTrace struct {
Head *diag.Context
Next *StackTrace
}
// Reason returns the Reason field if err is an *Exception. Otherwise it returns
// err itself.
func Reason(err error) error {
if exc, ok := err.(*Exception); ok {
return exc.Reason
}
return err
}
// OK is a pointer to the zero value of Exception, representing the absence of
// exception.
var OK = &Exception{}
// Error returns the message of the cause of the exception.
func (exc *Exception) Error() string {
return exc.Reason.Error()
}
// Show shows the exception.
func (exc *Exception) Show(indent string) string {
buf := new(bytes.Buffer)
var causeDescription string
if shower, ok := exc.Reason.(diag.Shower); ok {
causeDescription = shower.Show(indent)
} else if exc.Reason == nil {
causeDescription = "ok"
} else {
causeDescription = "\033[31;1m" + exc.Reason.Error() + "\033[m"
}
fmt.Fprintf(buf, "Exception: %s", causeDescription)
if exc.StackTrace != nil {
buf.WriteString("\n")
if exc.StackTrace.Next == nil {
buf.WriteString(exc.StackTrace.Head.ShowCompact(indent))
} else {
buf.WriteString(indent + "Traceback:")
for tb := exc.StackTrace; tb != nil; tb = tb.Next {
buf.WriteString("\n" + indent + " ")
buf.WriteString(tb.Head.Show(indent + " "))
}
}
}
if pipeExcs, ok := exc.Reason.(PipelineError); ok {
buf.WriteString("\n" + indent + "Caused by:")
for _, e := range pipeExcs.Errors {
if e == OK {
continue
}
buf.WriteString("\n" + indent + " " + e.Show(indent+" "))
}
}
return buf.String()
}
// Kind returns "exception".
func (exc *Exception) Kind() string {
return "exception"
}
// Repr returns a representation of the exception. It is lossy in that it does
// not preserve the stacktrace.
func (exc *Exception) Repr(indent int) string {
if exc.Reason == nil {
return "$ok"
}
return "[&reason=" + vals.Repr(exc.Reason, indent+1) + "]"
}
// Equal compares by address.
func (exc *Exception) Equal(rhs interface{}) bool {
return exc == rhs
}
// Hash returns the hash of the address.
func (exc *Exception) Hash() uint32 {
return hash.Pointer(unsafe.Pointer(exc))
}
// Bool returns whether this exception has a nil cause; that is, it is $ok.
func (exc *Exception) Bool() bool {
return exc.Reason == nil
}
func (exc *Exception) Fields() vals.StructMap { return excFields{exc} }
type excFields struct{ e *Exception }
func (excFields) IsStructMap() {}
func (f excFields) Reason() error { return f.e.Reason }
// PipelineError represents the errors of pipelines, in which multiple commands
// may error.
type PipelineError struct {
Errors []*Exception
}
// Error returns a plain text representation of the pipeline error.
func (pe PipelineError) Error() string {
b := new(bytes.Buffer)
b.WriteString("(")
for i, e := range pe.Errors {
if i > 0 {
b.WriteString(" | ")
}
if e == nil || e.Reason == nil {
b.WriteString("<nil>")
} else {
b.WriteString(e.Error())
}
}
b.WriteString(")")
return b.String()
}
// MakePipelineError builds a suitable error from pipeline results:
//
// If all elements are either nil or OK, it returns nil. If there is exactly
// non-nil non-OK Exception, it returns it. Otherwise, it return a PipelineError
// built from the slice, with nil items turned into OK's for easier access from
// Elvish code.
func MakePipelineError(excs []*Exception) error {
newexcs := make([]*Exception, len(excs))
notOK, lastNotOK := 0, 0
for i, e := range excs {
if e == nil {
newexcs[i] = OK
} else {
newexcs[i] = e
if e.Reason != nil {
notOK++
lastNotOK = i
}
}
}
switch notOK {
case 0:
return nil
case 1:
return newexcs[lastNotOK]
default:
return PipelineError{newexcs}
}
}
func (pe PipelineError) Fields() vals.StructMap { return peFields{pe} }
type peFields struct{ pe PipelineError }
func (peFields) IsStructMap() {}
func (f peFields) Type() string { return "pipeline" }
func (f peFields) Exceptions() vals.List {
li := vals.EmptyList
for _, exc := range f.pe.Errors {
li = li.Cons(exc)
}
return li
}
// Flow is a special type of error used for control flows.
type Flow uint
// Control flows.
const (
Return Flow = iota
Break
Continue
)
var flowNames = [...]string{
"return", "break", "continue",
}
func (f Flow) Error() string {
if f >= Flow(len(flowNames)) {
return fmt.Sprintf("!(BAD FLOW: %d)", f)
}
return flowNames[f]
}
// Show shows the flow "error".
func (f Flow) Show(string) string {
return "\033[33;1m" + f.Error() + "\033[m"
}
func (f Flow) Fields() vals.StructMap { return flowFields{f} }
type flowFields struct{ f Flow }
func (flowFields) IsStructMap() {}
func (f flowFields) Type() string { return "flow" }
func (f flowFields) Name() string { return f.f.Error() }
// ExternalCmdExit contains the exit status of external commands.
type ExternalCmdExit struct {
syscall.WaitStatus
CmdName string
Pid int
}
// NewExternalCmdExit constructs an error for representing a non-zero exit from
// an external command.
func NewExternalCmdExit(name string, ws syscall.WaitStatus, pid int) error {
if ws.Exited() && ws.ExitStatus() == 0 {
return nil
}
return ExternalCmdExit{ws, name, pid}
}
func (exit ExternalCmdExit) Error() string {
ws := exit.WaitStatus
quotedName := parse.Quote(exit.CmdName)
switch {
case ws.Exited():
return quotedName + " exited with " + strconv.Itoa(ws.ExitStatus())
case ws.Signaled():
causeDescription := quotedName + " killed by signal " + ws.Signal().String()
if ws.CoreDump() {
causeDescription += " (core dumped)"
}
return causeDescription
case ws.Stopped():
causeDescription := quotedName + " stopped by signal " + fmt.Sprintf("%s (pid=%d)", ws.StopSignal(), exit.Pid)
trap := ws.TrapCause()
if trap != -1 {
causeDescription += fmt.Sprintf(" (trapped %v)", trap)
}
return causeDescription
default:
return fmt.Sprint(quotedName, " has unknown WaitStatus ", ws)
}
}
func (exit ExternalCmdExit) Fields() vals.StructMap {
ws := exit.WaitStatus
f := exitFieldsCommon{exit}
switch {
case ws.Exited():
return exitFieldsExited{f}
case ws.Signaled():
return exitFieldsSignaled{f}
case ws.Stopped():
return exitFieldsStopped{f}
default:
return exitFieldsUnknown{f}
}
}
type exitFieldsCommon struct{ e ExternalCmdExit }
func (exitFieldsCommon) IsStructMap() {}
func (f exitFieldsCommon) CmdName() string { return f.e.CmdName }
func (f exitFieldsCommon) Pid() string { return strconv.Itoa(f.e.Pid) }
type exitFieldsExited struct{ exitFieldsCommon }
func (exitFieldsExited) Type() string { return "external-cmd/exited" }
func (f exitFieldsExited) ExitStatus() string { return strconv.Itoa(f.e.ExitStatus()) }
type exitFieldsSignaled struct{ exitFieldsCommon }
func (f exitFieldsSignaled) Type() string { return "external-cmd/signaled" }
func (f exitFieldsSignaled) SignalName() string { return f.e.Signal().String() }
func (f exitFieldsSignaled) SignalNumber() string { return strconv.Itoa(int(f.e.Signal())) }
func (f exitFieldsSignaled) CoreDumped() bool { return f.e.CoreDump() }
type exitFieldsStopped struct{ exitFieldsCommon }
func (f exitFieldsStopped) Type() string { return "external-cmd/stopped" }
func (f exitFieldsStopped) SignalName() string { return f.e.StopSignal().String() }
func (f exitFieldsStopped) SignalNumber() string { return strconv.Itoa(int(f.e.StopSignal())) }
func (f exitFieldsStopped) TrapCause() int { return f.e.TrapCause() }
type exitFieldsUnknown struct{ exitFieldsCommon }
func (exitFieldsUnknown) Type() string { return "external-cmd/unknown" }