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