github.com/haraldrudell/parl@v0.4.176/pexec/exit-error-data.go (about) 1 /* 2 © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package pexec 7 8 import ( 9 "errors" 10 "fmt" 11 "os/exec" 12 "strings" 13 14 "github.com/haraldrudell/parl/pbytes" 15 "github.com/haraldrudell/parl/perrors" 16 "golang.org/x/sys/unix" 17 ) 18 19 const ( 20 // [ExitErrorData.ExitErrorString] should include standard error output 21 ExitErrorIncludeStderr = true 22 // status code 1, which in POSIX means a general error 23 StatusCode1 = 1 24 addStderrStack = 1 25 ) 26 27 // ExitErrorData provides additional detail on pexec.ExitError values 28 type ExitErrorData struct { 29 // the original error 30 Err error 31 // Err interpreted as ExitError, possibly nil 32 ExitErr *exec.ExitError 33 // status code in ExitError, possibly 0 34 StatusCode int 35 // signal in ExitError, possibly 0 36 Signal unix.Signal 37 // stderr in ExitError or from argument, possibly nil 38 Stderr []byte 39 } 40 41 // ExitErrorData implements error 42 var _ error = &ExitErrorData{} 43 44 // NewExitErrorData returns parse-once data on a possible ExitError 45 // - if ExitErr field is nil or IsExitError method returns false, err does not contain an ExitError 46 // - the returned value is an error implementation 47 func NewExitErrorData(err error, stderr ...[]byte) (exitErrorData *ExitErrorData) { 48 var e = ExitErrorData{Err: err} 49 if errors.As(err, &e.ExitErr); e.ExitErr != nil { 50 _, e.StatusCode, e.Signal, e.Stderr = ExitError(err) 51 } 52 if len(stderr) > 0 { 53 e.Stderr = stderr[0] 54 } 55 return &e 56 } 57 58 // IsExitError returns true if an pexec.ExitError is present 59 // - false if Err was nil or some other type of error 60 func (e *ExitErrorData) IsExitError() (isExitError bool) { 61 return e.ExitErr != nil 62 } 63 64 // IsStatusCode1 returns if the err error chain contains an ExitError 65 // that indicates status code 1 66 // - Status code 1 indicates an unspecified failure of a process 67 // - Success has no ExitError and status code 0 68 // - Terminated by signal is status code -1 69 // - Input syntax error is status code 2 70 func (e *ExitErrorData) IsStatusCode1() (is1 bool) { 71 return e.StatusCode == StatusCode1 72 } 73 74 // IsSignalKill returns true if the err error chain contains an 75 // ExitError with signal kill 76 // - signal kill is the response to a command’s context being 77 // canceled. This should be checked together with [context.Context.Err] 78 // - SIGKILL can also be sent to the process by the operating system 79 // trying to reclaim memory or by other processes 80 func (e *ExitErrorData) IsSignalKill() (isSignalKill bool) { 81 return e.StatusCode == TerminatedBySignal && 82 e.Signal == unix.SIGKILL 83 } 84 85 // the Error method returns the message from any ExitError, 86 // otherwise empty string 87 // - Error also makes ExitErrorData implementing the error 88 // interface 89 func (e *ExitErrorData) Error() (exitErrorMessage string) { 90 if e.ExitErr != nil { 91 exitErrorMessage = e.ExitErr.Error() 92 } 93 return 94 } 95 96 // AddStderr adds standard error output at the end of the error message 97 // for err. Also ensures stack trace. 98 // - ExitError has standard error if the Output method was used 99 // - NewExitErrorData can also have been provided stderr 100 func (e *ExitErrorData) AddStderr(err error) (err2 error) { 101 if stderr := e.Stderr; len(stderr) > 0 { 102 if serr := pbytes.TrimNewline(stderr); len(serr) > 0 { 103 err2 = perrors.Errorf("%w stderr: ‘%s’", err, string(serr)) 104 return // standard error appended to message 105 } 106 } 107 if perrors.HasStack(err) { 108 err2 = err 109 return // error already has stack trace: no change return 110 } 111 err2 = perrors.Stackn(err, addStderrStack) 112 return // stackk added Stderr return 113 } 114 115 // ExitErrorString returns the ExitError error message and data from 116 // Err and stderr, not an error value 117 // - for non-signal: “status code: 1 ‘read error’” 118 // - for signal: “signal: "abort trap" ‘signal: abort trap’” 119 // - the error message for err: “message: ‘failure’” 120 // - stderr if non-empty from ExitErr or stderr argument and 121 // includeStderr is ExitErrorIncludeStderr: 122 // - “stderr: ‘I/O error’” 123 // - returned value is never empty 124 func (e *ExitErrorData) ExitErrorString(includeStderr ...bool) (errS string) { 125 var s []string 126 var stderr []byte 127 if e.ExitErr != nil { 128 if stderr = e.ExitErr.Stderr; len(stderr) == 0 { 129 stderr = e.Stderr 130 } 131 132 // it’s either status code or signal 133 if e.StatusCode == TerminatedBySignal { 134 s = append(s, fmt.Sprintf("signal: %q", e.Signal.String())) 135 } else { 136 s = append(s, fmt.Sprintf("status code: %d", e.StatusCode)) 137 } 138 } 139 140 // original error message 141 s = append(s, fmt.Sprintf("message: ‘%s’", perrors.Short(e.Err))) 142 143 // stderr 144 if len(includeStderr) > 0 && includeStderr[0] && 145 len(stderr) > 0 { 146 if serr := pbytes.TrimNewline(stderr); len(serr) > 0 { 147 s = append(s, fmt.Sprintf("stderr: ‘%s’", string(serr))) 148 } 149 } 150 151 errS = strings.Join(s, "\x20") 152 return 153 }