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  }