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" }