src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/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 (*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 var _ vals.PseudoMap = &exception{} 41 42 // StackTrace represents a stack trace as a linked list of diag.Context. The 43 // head is the innermost stack. 44 // 45 // Since pipelines can call multiple functions in parallel, all the StackTrace 46 // nodes form a DAG. 47 type StackTrace struct { 48 Head *diag.Context 49 Next *StackTrace 50 } 51 52 // Reason returns the Reason field if err is an Exception. Otherwise it returns 53 // err itself. 54 func Reason(err error) error { 55 if exc, ok := err.(*exception); ok { 56 return exc.reason 57 } 58 return err 59 } 60 61 // OK is a pointer to a special value of Exception that represents the absence 62 // of exception. 63 var OK = &exception{} 64 65 func (exc *exception) isException() {} 66 67 func (exc *exception) Reason() error { return exc.reason } 68 69 func (exc *exception) StackTrace() *StackTrace { return exc.stackTrace } 70 71 // Error returns the message of the cause of the exception. 72 func (exc *exception) Error() string { return exc.reason.Error() } 73 74 var ( 75 exceptionCauseStartMarker = "\033[31;1m" 76 exceptionCauseEndMarker = "\033[m" 77 ) 78 79 // Show shows the exception. 80 func (exc *exception) Show(indent string) string { 81 buf := new(bytes.Buffer) 82 83 var causeDescription string 84 if shower, ok := exc.reason.(diag.Shower); ok { 85 causeDescription = shower.Show(indent) 86 } else if exc.reason == nil { 87 causeDescription = "ok" 88 } else { 89 causeDescription = exceptionCauseStartMarker + exc.reason.Error() + exceptionCauseEndMarker 90 } 91 fmt.Fprintf(buf, "Exception: %s", causeDescription) 92 93 if exc.stackTrace != nil { 94 for tb := exc.stackTrace; tb != nil; tb = tb.Next { 95 buf.WriteString("\n" + indent + " ") 96 buf.WriteString(tb.Head.Show(indent + " ")) 97 } 98 } 99 100 if pipeExcs, ok := exc.reason.(PipelineError); ok { 101 buf.WriteString("\n" + indent + "Caused by:") 102 for _, e := range pipeExcs.Errors { 103 if e == OK { 104 continue 105 } 106 buf.WriteString("\n" + indent + " " + e.Show(indent+" ")) 107 } 108 } 109 110 return buf.String() 111 } 112 113 // Kind returns "exception". 114 func (exc *exception) Kind() string { 115 return "exception" 116 } 117 118 // Repr returns a representation of the exception. It is lossy in that it does 119 // not preserve the stacktrace. 120 func (exc *exception) Repr(indent int) string { 121 if exc.reason == nil { 122 return "$ok" 123 } 124 return "[^exception &reason=" + vals.Repr(exc.reason, indent+1) + " &stack-trace=<...>]" 125 } 126 127 // Equal compares by address. 128 func (exc *exception) Equal(rhs any) bool { 129 return exc == rhs 130 } 131 132 // Hash returns the hash of the address. 133 func (exc *exception) Hash() uint32 { 134 return hash.Pointer(unsafe.Pointer(exc)) 135 } 136 137 // Bool returns whether this exception has a nil cause; that is, it is $ok. 138 func (exc *exception) Bool() bool { 139 return exc.reason == nil 140 } 141 142 func (exc *exception) Fields() vals.StructMap { return excFields{exc} } 143 144 type excFields struct{ e *exception } 145 146 func (excFields) IsStructMap() {} 147 func (f excFields) Reason() error { return f.e.reason } 148 func (f excFields) StackTrace() *StackTrace { return f.e.stackTrace } 149 150 // PipelineError represents the errors of pipelines, in which multiple commands 151 // may error. 152 type PipelineError struct { 153 Errors []Exception 154 } 155 156 var _ vals.PseudoMap = PipelineError{} 157 158 // Error returns a plain text representation of the pipeline error. 159 func (pe PipelineError) Error() string { 160 b := new(bytes.Buffer) 161 b.WriteString("(") 162 for i, e := range pe.Errors { 163 if i > 0 { 164 b.WriteString(" | ") 165 } 166 if e == nil || e.Reason() == nil { 167 b.WriteString("<nil>") 168 } else { 169 b.WriteString(e.Error()) 170 } 171 } 172 b.WriteString(")") 173 return b.String() 174 } 175 176 // MakePipelineError builds an error from the execution results of multiple 177 // commands in a pipeline. 178 // 179 // If all elements are either nil or OK, it returns nil. If there is exactly 180 // non-nil non-OK Exception, it returns it. Otherwise, it return a PipelineError 181 // built from the slice, with nil items turned into OK's for easier access from 182 // Elvish code. 183 func MakePipelineError(excs []Exception) error { 184 newexcs := make([]Exception, len(excs)) 185 notOK, lastNotOK := 0, 0 186 for i, e := range excs { 187 if e == nil { 188 newexcs[i] = OK 189 } else { 190 newexcs[i] = e 191 if e.Reason() != nil { 192 notOK++ 193 lastNotOK = i 194 } 195 } 196 } 197 switch notOK { 198 case 0: 199 return nil 200 case 1: 201 return newexcs[lastNotOK] 202 default: 203 return PipelineError{newexcs} 204 } 205 } 206 207 func (pe PipelineError) Kind() string { return "pipeline-error" } 208 func (pe PipelineError) Fields() vals.StructMap { return peFields{pe} } 209 210 type peFields struct{ pe PipelineError } 211 212 func (peFields) IsStructMap() {} 213 214 func (f peFields) Type() string { return "pipeline" } 215 216 func (f peFields) Exceptions() vals.List { 217 li := vals.EmptyList 218 for _, exc := range f.pe.Errors { 219 li = li.Conj(exc) 220 } 221 return li 222 } 223 224 // Flow is a special type of error used for control flows. 225 type Flow uint 226 227 var _ vals.PseudoMap = Flow(0) 228 229 // Control flows. 230 const ( 231 Return Flow = iota 232 Break 233 Continue 234 ) 235 236 var flowNames = [...]string{ 237 "return", "break", "continue", 238 } 239 240 func (f Flow) Error() string { 241 if f >= Flow(len(flowNames)) { 242 return fmt.Sprintf("!(BAD FLOW: %d)", f) 243 } 244 return flowNames[f] 245 } 246 247 // Show shows the flow "error". 248 func (f Flow) Show(string) string { 249 return "\033[33;1m" + f.Error() + "\033[m" 250 } 251 252 func (f Flow) Kind() string { return "flow-error" } 253 func (f Flow) Fields() vals.StructMap { return flowFields{f} } 254 255 type flowFields struct{ f Flow } 256 257 func (flowFields) IsStructMap() {} 258 259 func (f flowFields) Type() string { return "flow" } 260 func (f flowFields) Name() string { return f.f.Error() } 261 262 // ExternalCmdExit contains the exit status of external commands. 263 type ExternalCmdExit struct { 264 syscall.WaitStatus 265 CmdName string 266 Pid int 267 } 268 269 var _ vals.PseudoMap = ExternalCmdExit{} 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) Kind() string { 305 return "external-cmd-error" 306 } 307 308 func (exit ExternalCmdExit) Fields() vals.StructMap { 309 ws := exit.WaitStatus 310 f := exitFieldsCommon{exit} 311 switch { 312 case ws.Exited(): 313 return exitFieldsExited{f} 314 case ws.Signaled(): 315 return exitFieldsSignaled{f} 316 case ws.Stopped(): 317 return exitFieldsStopped{f} 318 default: 319 return exitFieldsUnknown{f} 320 } 321 } 322 323 type exitFieldsCommon struct{ e ExternalCmdExit } 324 325 func (exitFieldsCommon) IsStructMap() {} 326 func (f exitFieldsCommon) CmdName() string { return f.e.CmdName } 327 func (f exitFieldsCommon) Pid() string { return strconv.Itoa(f.e.Pid) } 328 329 type exitFieldsExited struct{ exitFieldsCommon } 330 331 func (exitFieldsExited) Type() string { return "external-cmd/exited" } 332 func (f exitFieldsExited) ExitStatus() string { return strconv.Itoa(f.e.ExitStatus()) } 333 334 type exitFieldsSignaled struct{ exitFieldsCommon } 335 336 func (f exitFieldsSignaled) Type() string { return "external-cmd/signaled" } 337 func (f exitFieldsSignaled) SignalName() string { return f.e.Signal().String() } 338 func (f exitFieldsSignaled) SignalNumber() string { return strconv.Itoa(int(f.e.Signal())) } 339 func (f exitFieldsSignaled) CoreDumped() bool { return f.e.CoreDump() } 340 341 type exitFieldsStopped struct{ exitFieldsCommon } 342 343 func (f exitFieldsStopped) Type() string { return "external-cmd/stopped" } 344 func (f exitFieldsStopped) SignalName() string { return f.e.StopSignal().String() } 345 func (f exitFieldsStopped) SignalNumber() string { return strconv.Itoa(int(f.e.StopSignal())) } 346 func (f exitFieldsStopped) TrapCause() int { return f.e.TrapCause() } 347 348 type exitFieldsUnknown struct{ exitFieldsCommon } 349 350 func (exitFieldsUnknown) Type() string { return "external-cmd/unknown" }