diff --git a/internal/lib/os/exec/exec.go b/internal/lib/os/exec/exec.go index 20d33d73..d56db4e7 100644 --- a/internal/lib/os/exec/exec.go +++ b/internal/lib/os/exec/exec.go @@ -18,5 +18,1221 @@ package exec // llgo:skipall import ( - _ "unsafe" + "bytes" + "context" + "errors" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" ) + +// Error is returned by LookPath when it fails to classify a file as an +// executable. +type Error struct { + // Name is the file name for which the error occurred. + Name string + // Err is the underlying error. + Err error +} + +func (e *Error) Error() string { + return "exec: " + strconv.Quote(e.Name) + ": " + e.Err.Error() +} + +func (e *Error) Unwrap() error { return e.Err } + +// ErrWaitDelay is returned by (*Cmd).Wait if the process exits with a +// successful status code but its output pipes are not closed before the +// command's WaitDelay expires. +var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete") + +// wrappedError wraps an error without relying on fmt.Errorf. +type wrappedError struct { + prefix string + err error +} + +func (w wrappedError) Error() string { + return w.prefix + ": " + w.err.Error() +} + +func (w wrappedError) Unwrap() error { + return w.err +} + +// Cmd represents an external command being prepared or run. +// +// A Cmd cannot be reused after calling its Run, Output or CombinedOutput +// methods. +type Cmd struct { + // Path is the path of the command to run. + // + // This is the only field that must be set to a non-zero + // value. If Path is relative, it is evaluated relative + // to Dir. + Path string + + // Args holds command line arguments, including the command as Args[0]. + // If the Args field is empty or nil, Run uses {Path}. + // + // In typical use, both Path and Args are set by calling Command. + Args []string + + // Env specifies the environment of the process. + // Each entry is of the form "key=value". + // If Env is nil, the new process uses the current process's + // environment. + // If Env contains duplicate environment keys, only the last + // value in the slice for each duplicate key is used. + // As a special case on Windows, SYSTEMROOT is always added if + // missing and not explicitly set to the empty string. + Env []string + + // Dir specifies the working directory of the command. + // If Dir is the empty string, Run runs the command in the + // calling process's current directory. + Dir string + + // Stdin specifies the process's standard input. + // + // If Stdin is nil, the process reads from the null device (os.DevNull). + // + // If Stdin is an *os.File, the process's standard input is connected + // directly to that file. + // + // Otherwise, during the execution of the command a separate + // goroutine reads from Stdin and delivers that data to the command + // over a pipe. In this case, Wait does not complete until the goroutine + // stops copying, either because it has reached the end of Stdin + // (EOF or a read error), or because writing to the pipe returned an error, + // or because a nonzero WaitDelay was set and expired. + Stdin io.Reader + + // Stdout and Stderr specify the process's standard output and error. + // + // If either is nil, Run connects the corresponding file descriptor + // to the null device (os.DevNull). + // + // If either is an *os.File, the corresponding output from the process + // is connected directly to that file. + // + // Otherwise, during the execution of the command a separate goroutine + // reads from the process over a pipe and delivers that data to the + // corresponding Writer. In this case, Wait does not complete until the + // goroutine reaches EOF or encounters an error or a nonzero WaitDelay + // expires. + // + // If Stdout and Stderr are the same writer, and have a type that can + // be compared with ==, at most one goroutine at a time will call Write. + Stdout io.Writer + Stderr io.Writer + + // ExtraFiles specifies additional open files to be inherited by the + // new process. It does not include standard input, standard output, or + // standard error. If non-nil, entry i becomes file descriptor 3+i. + // + // ExtraFiles is not supported on Windows. + ExtraFiles []*os.File + + // SysProcAttr holds optional, operating system-specific attributes. + // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. + SysProcAttr *syscall.SysProcAttr + + // Process is the underlying process, once started. + Process *os.Process + + // ProcessState contains information about an exited process. + // If the process was started successfully, Wait or Run will + // populate its ProcessState when the command completes. + ProcessState *os.ProcessState + + // ctx is the context passed to CommandContext, if any. + ctx context.Context + + Err error // LookPath error, if any. + + // If Cancel is non-nil, the command must have been created with + // CommandContext and Cancel will be called when the command's + // Context is done. By default, CommandContext sets Cancel to + // call the Kill method on the command's Process. + // + // Typically a custom Cancel will send a signal to the command's + // Process, but it may instead take other actions to initiate cancellation, + // such as closing a stdin or stdout pipe or sending a shutdown request on a + // network socket. + // + // If the command exits with a success status after Cancel is + // called, and Cancel does not return an error equivalent to + // os.ErrProcessDone, then Wait and similar methods will return a non-nil + // error: either an error wrapping the one returned by Cancel, + // or the error from the Context. + // (If the command exits with a non-success status, or Cancel + // returns an error that wraps os.ErrProcessDone, Wait and similar methods + // continue to return the command's usual exit status.) + // + // If Cancel is set to nil, nothing will happen immediately when the command's + // Context is done, but a nonzero WaitDelay will still take effect. That may + // be useful, for example, to work around deadlocks in commands that do not + // support shutdown signals but are expected to always finish quickly. + // + // Cancel will not be called if Start returns a non-nil error. + Cancel func() error + + // If WaitDelay is non-zero, it bounds the time spent waiting on two sources + // of unexpected delay in Wait: a child process that fails to exit after the + // associated Context is canceled, and a child process that exits but leaves + // its I/O pipes unclosed. + // + // The WaitDelay timer starts when either the associated Context is done or a + // call to Wait observes that the child process has exited, whichever occurs + // first. When the delay has elapsed, the command shuts down the child process + // and/or its I/O pipes. + // + // If the child process has failed to exit — perhaps because it ignored or + // failed to receive a shutdown signal from a Cancel function, or because no + // Cancel function was set — then it will be terminated using os.Process.Kill. + // + // Then, if the I/O pipes communicating with the child process are still open, + // those pipes are closed in order to unblock any goroutines currently blocked + // on Read or Write calls. + // + // If pipes are closed due to WaitDelay, no Cancel call has occurred, + // and the command has otherwise exited with a successful status, Wait and + // similar methods will return ErrWaitDelay instead of nil. + // + // If WaitDelay is zero (the default), I/O pipes will be read until EOF, + // which might not occur until orphaned subprocesses of the command have + // also closed their descriptors for the pipes. + WaitDelay time.Duration + + // childIOFiles holds closers for any of the child process's + // stdin, stdout, and/or stderr files that were opened by the Cmd itself + // (not supplied by the caller). These should be closed as soon as they + // are inherited by the child process. + childIOFiles []io.Closer + + // parentIOPipes holds closers for the parent's end of any pipes + // connected to the child's stdin, stdout, and/or stderr streams + // that were opened by the Cmd itself (not supplied by the caller). + // These should be closed after Wait sees the command and copying + // goroutines exit, or after WaitDelay has expired. + parentIOPipes []io.Closer + + // goroutine holds a set of closures to execute to copy data + // to and/or from the command's I/O pipes. + goroutine []func() error + + // If goroutineErr is non-nil, it receives the first error from a copying + // goroutine once all such goroutines have completed. + // goroutineErr is set to nil once its error has been received. + goroutineErr <-chan error + + // If ctxResult is non-nil, it receives the result of watchCtx exactly once. + ctxResult <-chan ctxResult + + // The stack saved when the Command was created, if GODEBUG contains + // execwait=2. Used for debugging leaks. + createdByStack []byte + + // For a security release long ago, we created x/sys/execabs, + // which manipulated the unexported lookPathErr error field + // in this struct. For Go 1.19 we exported the field as Err error, + // above, but we have to keep lookPathErr around for use by + // old programs building against new toolchains. + // The String and Start methods look for an error in lookPathErr + // in preference to Err, to preserve the errors that execabs sets. + // + // In general we don't guarantee misuse of reflect like this, + // but the misuse of reflect was by us, the best of various bad + // options to fix the security problem, and people depend on + // those old copies of execabs continuing to work. + // The result is that we have to leave this variable around for the + // rest of time, a compatibility scar. + // + // See https://go.dev/blog/path-security + // and https://go.dev/issue/43724 for more context. + lookPathErr error +} + +// A ctxResult reports the result of watching the Context associated with a +// running command (and sending corresponding signals if needed). +type ctxResult struct { + err error + + // If timer is non-nil, it expires after WaitDelay has elapsed after + // the Context is done. + // + // (If timer is nil, that means that the Context was not done before the + // command completed, or no WaitDelay was set, or the WaitDelay already + // expired and its effect was already applied.) + timer *time.Timer +} + +/* TODO(xsw): +var execwait = godebug.New("#execwait") +var execerrdot = godebug.New("execerrdot") +*/ + +// Command returns the Cmd struct to execute the named program with +// the given arguments. +// +// It sets only the Path and Args in the returned structure. +// +// If name contains no path separators, Command uses LookPath to +// resolve name to a complete path if possible. Otherwise it uses name +// directly as Path. +// +// The returned Cmd's Args field is constructed from the command name +// followed by the elements of arg, so arg should not include the +// command name itself. For example, Command("echo", "hello"). +// Args[0] is always name, not the possibly resolved Path. +// +// On Windows, processes receive the whole command line as a single string +// and do their own parsing. Command combines and quotes Args into a command +// line string with an algorithm compatible with applications using +// CommandLineToArgvW (which is the most common way). Notable exceptions are +// msiexec.exe and cmd.exe (and thus, all batch files), which have a different +// unquoting algorithm. In these or other similar cases, you can do the +// quoting yourself and provide the full command line in SysProcAttr.CmdLine, +// leaving Args empty. +func Command(name string, arg ...string) *Cmd { + cmd := &Cmd{ + Path: name, + Args: append([]string{name}, arg...), + } + + /* TODO(xsw): + if v := execwait.Value(); v != "" { + if v == "2" { + // Obtain the caller stack. (This is equivalent to runtime/debug.Stack, + // copied to avoid importing the whole package.) + stack := make([]byte, 1024) + for { + n := runtime.Stack(stack, false) + if n < len(stack) { + stack = stack[:n] + break + } + stack = make([]byte, 2*len(stack)) + } + + if i := bytes.Index(stack, []byte("\nos/exec.Command(")); i >= 0 { + stack = stack[i+1:] + } + cmd.createdByStack = stack + } + + runtime.SetFinalizer(cmd, func(c *Cmd) { + if c.Process != nil && c.ProcessState == nil { + debugHint := "" + if c.createdByStack == nil { + debugHint = " (set GODEBUG=execwait=2 to capture stacks for debugging)" + } else { + os.Stderr.WriteString("GODEBUG=execwait=2 detected a leaked exec.Cmd created by:\n") + os.Stderr.Write(c.createdByStack) + os.Stderr.WriteString("\n") + debugHint = "" + } + panic("exec: Cmd started a Process but leaked without a call to Wait" + debugHint) + } + }) + } + */ + + if filepath.Base(name) == name { + lp, err := LookPath(name) + if lp != "" { + // Update cmd.Path even if err is non-nil. + // If err is ErrDot (especially on Windows), lp may include a resolved + // extension (like .exe or .bat) that should be preserved. + cmd.Path = lp + } + if err != nil { + cmd.Err = err + } + } + return cmd +} + +// CommandContext is like Command but includes a context. +// +// The provided context is used to interrupt the process +// (by calling cmd.Cancel or os.Process.Kill) +// if the context becomes done before the command completes on its own. +// +// CommandContext sets the command's Cancel function to invoke the Kill method +// on its Process, and leaves its WaitDelay unset. The caller may change the +// cancellation behavior by modifying those fields before starting the command. +func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { + if ctx == nil { + panic("nil Context") + } + cmd := Command(name, arg...) + cmd.ctx = ctx + cmd.Cancel = func() error { + return cmd.Process.Kill() + } + return cmd +} + +// String returns a human-readable description of c. +// It is intended only for debugging. +// In particular, it is not suitable for use as input to a shell. +// The output of String may vary across Go releases. +func (c *Cmd) String() string { + if c.Err != nil || c.lookPathErr != nil { + // failed to resolve path; report the original requested path (plus args) + return strings.Join(c.Args, " ") + } + // report the exact executable path (plus args) + b := new(strings.Builder) + b.WriteString(c.Path) + for _, a := range c.Args[1:] { + b.WriteByte(' ') + b.WriteString(a) + } + return b.String() +} + +// interfaceEqual protects against panics from doing equality tests on +// two interfaces with non-comparable underlying types. +func interfaceEqual(a, b any) bool { + defer func() { + recover() + }() + return a == b +} + +func (c *Cmd) argv() []string { + if len(c.Args) > 0 { + return c.Args + } + return []string{c.Path} +} + +func (c *Cmd) childStdin() (*os.File, error) { + /* TODO(xsw): + if c.Stdin == nil { + f, err := os.Open(os.DevNull) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := c.Stdin.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(pw, c.Stdin) + if skipStdinCopyError(err) { + err = nil + } + if err1 := pw.Close(); err == nil { + err = err1 + } + return err + }) + return pr, nil + */ + panic("todo: exec.Cmd.childStdin") +} + +func (c *Cmd) childStdout() (*os.File, error) { + return c.writerDescriptor(c.Stdout) +} + +func (c *Cmd) childStderr(childStdout *os.File) (*os.File, error) { + if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { + return childStdout, nil + } + return c.writerDescriptor(c.Stderr) +} + +// writerDescriptor returns an os.File to which the child process +// can write to send data to w. +// +// If w is nil, writerDescriptor returns a File that writes to os.DevNull. +func (c *Cmd) writerDescriptor(w io.Writer) (*os.File, error) { + if w == nil { + f, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := w.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(w, pr) + pr.Close() // in case io.Copy stopped due to write error + return err + }) + return pw, nil +} + +func closeDescriptors(closers []io.Closer) { + for _, fd := range closers { + fd.Close() + } +} + +// Run starts the specified command and waits for it to complete. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command starts but does not complete successfully, the error is of +// type *ExitError. Other error types may be returned for other situations. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +func (c *Cmd) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +// lookExtensions finds windows executable by its dir and path. +// It uses LookPath to try appropriate extensions. +// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`. +func lookExtensions(path, dir string) (string, error) { + if filepath.Base(path) == path { + path = "." + string(filepath.Separator) + path + } + if dir == "" { + return LookPath(path) + } + if filepath.VolumeName(path) != "" { + return LookPath(path) + } + if len(path) > 1 && os.IsPathSeparator(path[0]) { + return LookPath(path) + } + dirandpath := filepath.Join(dir, path) + // We assume that LookPath will only add file extension. + lp, err := LookPath(dirandpath) + if err != nil { + return "", err + } + ext := strings.TrimPrefix(lp, dirandpath) + return path + ext, nil +} + +// Start starts the specified command but does not wait for it to complete. +// +// If Start returns successfully, the c.Process field will be set. +// +// After a successful call to Start the Wait method must be called in +// order to release associated system resources. +func (c *Cmd) Start() error { + // Check for doubled Start calls before we defer failure cleanup. If the prior + // call to Start succeeded, we don't want to spuriously close its pipes. + if c.Process != nil { + return errors.New("exec: already started") + } + + started := false + defer func() { + closeDescriptors(c.childIOFiles) + c.childIOFiles = nil + + if !started { + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + } + }() + + if c.Path == "" && c.Err == nil && c.lookPathErr == nil { + c.Err = errors.New("exec: no command") + } + if c.Err != nil || c.lookPathErr != nil { + if c.lookPathErr != nil { + return c.lookPathErr + } + return c.Err + } + if runtime.GOOS == "windows" { + lp, err := lookExtensions(c.Path, c.Dir) + if err != nil { + return err + } + c.Path = lp + } + if c.Cancel != nil && c.ctx == nil { + return errors.New("exec: command with a non-nil Cancel was not created with CommandContext") + } + if c.ctx != nil { + select { + case <-c.ctx.Done(): + return c.ctx.Err() + default: + } + } + + childFiles := make([]*os.File, 0, 3+len(c.ExtraFiles)) + stdin, err := c.childStdin() + if err != nil { + return err + } + childFiles = append(childFiles, stdin) + stdout, err := c.childStdout() + if err != nil { + return err + } + childFiles = append(childFiles, stdout) + stderr, err := c.childStderr(stdout) + if err != nil { + return err + } + childFiles = append(childFiles, stderr) + childFiles = append(childFiles, c.ExtraFiles...) + + env, err := c.environ() + if err != nil { + return err + } + + c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ + Dir: c.Dir, + Files: childFiles, + Env: env, + Sys: c.SysProcAttr, + }) + if err != nil { + return err + } + started = true + + // Don't allocate the goroutineErr channel unless there are goroutines to start. + if len(c.goroutine) > 0 { + goroutineErr := make(chan error, 1) + c.goroutineErr = goroutineErr + + type goroutineStatus struct { + running int + firstErr error + } + statusc := make(chan goroutineStatus, 1) + statusc <- goroutineStatus{running: len(c.goroutine)} + for _, fn := range c.goroutine { + go func(fn func() error) { + err := fn() + + status := <-statusc + if status.firstErr == nil { + status.firstErr = err + } + status.running-- + if status.running == 0 { + goroutineErr <- status.firstErr + } else { + statusc <- status + } + }(fn) + } + c.goroutine = nil // Allow the goroutines' closures to be GC'd when they complete. + } + + // If we have anything to do when the command's Context expires, + // start a goroutine to watch for cancellation. + // + // (Even if the command was created by CommandContext, a helper library may + // have explicitly set its Cancel field back to nil, indicating that it should + // be allowed to continue running after cancellation after all.) + if (c.Cancel != nil || c.WaitDelay != 0) && c.ctx != nil && c.ctx.Done() != nil { + resultc := make(chan ctxResult) + c.ctxResult = resultc + go c.watchCtx(resultc) + } + + return nil +} + +// watchCtx watches c.ctx until it is able to send a result to resultc. +// +// If c.ctx is done before a result can be sent, watchCtx calls c.Cancel, +// and/or kills cmd.Process it after c.WaitDelay has elapsed. +// +// watchCtx manipulates c.goroutineErr, so its result must be received before +// c.awaitGoroutines is called. +func (c *Cmd) watchCtx(resultc chan<- ctxResult) { + select { + case resultc <- ctxResult{}: + return + case <-c.ctx.Done(): + } + + var err error + if c.Cancel != nil { + if interruptErr := c.Cancel(); interruptErr == nil { + // We appear to have successfully interrupted the command, so any + // program behavior from this point may be due to ctx even if the + // command exits with code 0. + err = c.ctx.Err() + } else if errors.Is(interruptErr, os.ErrProcessDone) { + // The process already finished: we just didn't notice it yet. + // (Perhaps c.Wait hadn't been called, or perhaps it happened to race with + // c.ctx being cancelled.) Don't inject a needless error. + } else { + err = wrappedError{ + prefix: "exec: canceling Cmd", + err: interruptErr, + } + } + } + if c.WaitDelay == 0 { + resultc <- ctxResult{err: err} + return + } + + timer := time.NewTimer(c.WaitDelay) + select { + case resultc <- ctxResult{err: err, timer: timer}: + // c.Process.Wait returned and we've handed the timer off to c.Wait. + // It will take care of goroutine shutdown from here. + return + case <-timer.C: + } + + killed := false + if killErr := c.Process.Kill(); killErr == nil { + // We appear to have killed the process. c.Process.Wait should return a + // non-nil error to c.Wait unless the Kill signal races with a successful + // exit, and if that does happen we shouldn't report a spurious error, + // so don't set err to anything here. + killed = true + } else if !errors.Is(killErr, os.ErrProcessDone) { + err = wrappedError{ + prefix: "exec: killing Cmd", + err: killErr, + } + } + + if c.goroutineErr != nil { + select { + case goroutineErr := <-c.goroutineErr: + // Forward goroutineErr only if we don't have reason to believe it was + // caused by a call to Cancel or Kill above. + if err == nil && !killed { + err = goroutineErr + } + default: + // Close the child process's I/O pipes, in case it abandoned some + // subprocess that inherited them and is still holding them open + // (see https://go.dev/issue/23019). + // + // We close the goroutine pipes only after we have sent any signals we're + // going to send to the process (via Signal or Kill above): if we send + // SIGKILL to the process, we would prefer for it to die of SIGKILL, not + // SIGPIPE. (However, this may still cause any orphaned subprocesses to + // terminate with SIGPIPE.) + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but report ErrWaitDelay for + // the error: any other error here could result from closing the pipes. + _ = <-c.goroutineErr + if err == nil { + err = ErrWaitDelay + } + } + + // Since we have already received the only result from c.goroutineErr, + // set it to nil to prevent awaitGoroutines from blocking on it. + c.goroutineErr = nil + } + + resultc <- ctxResult{err: err} +} + +// An ExitError reports an unsuccessful exit by a command. +type ExitError struct { + *os.ProcessState + + // Stderr holds a subset of the standard error output from the + // Cmd.Output method if standard error was not otherwise being + // collected. + // + // If the error output is long, Stderr may contain only a prefix + // and suffix of the output, with the middle replaced with + // text about the number of omitted bytes. + // + // Stderr is provided for debugging, for inclusion in error messages. + // Users with other needs should redirect Cmd.Stderr as needed. + Stderr []byte +} + +func (e *ExitError) Error() string { + return e.ProcessState.String() +} + +// Wait waits for the command to exit and waits for any copying to +// stdin or copying from stdout or stderr to complete. +// +// The command must have been started by Start. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command fails to run or doesn't complete successfully, the +// error is of type *ExitError. Other error types may be +// returned for I/O problems. +// +// If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits +// for the respective I/O loop copying to or from the process to complete. +// +// Wait releases any resources associated with the Cmd. +func (c *Cmd) Wait() error { + if c.Process == nil { + return errors.New("exec: not started") + } + if c.ProcessState != nil { + return errors.New("exec: Wait was already called") + } + + state, err := c.Process.Wait() + if err == nil && !state.Success() { + err = &ExitError{ProcessState: state} + } + c.ProcessState = state + + var timer *time.Timer + if c.ctxResult != nil { + watch := <-c.ctxResult + timer = watch.timer + // If c.Process.Wait returned an error, prefer that. + // Otherwise, report any error from the watchCtx goroutine, + // such as a Context cancellation or a WaitDelay overrun. + if err == nil && watch.err != nil { + err = watch.err + } + } + + if goroutineErr := c.awaitGoroutines(timer); err == nil { + // Report an error from the copying goroutines only if the program otherwise + // exited normally on its own. Otherwise, the copying error may be due to the + // abnormal termination. + err = goroutineErr + } + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + + return err +} + +// awaitGoroutines waits for the results of the goroutines copying data to or +// from the command's I/O pipes. +// +// If c.WaitDelay elapses before the goroutines complete, awaitGoroutines +// forcibly closes their pipes and returns ErrWaitDelay. +// +// If timer is non-nil, it must send to timer.C at the end of c.WaitDelay. +func (c *Cmd) awaitGoroutines(timer *time.Timer) error { + defer func() { + if timer != nil { + timer.Stop() + } + c.goroutineErr = nil + }() + + if c.goroutineErr == nil { + return nil // No running goroutines to await. + } + + if timer == nil { + if c.WaitDelay == 0 { + return <-c.goroutineErr + } + + select { + case err := <-c.goroutineErr: + // Avoid the overhead of starting a timer. + return err + default: + } + + // No existing timer was started: either there is no Context associated with + // the command, or c.Process.Wait completed before the Context was done. + timer = time.NewTimer(c.WaitDelay) + } + + select { + case <-timer.C: + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but ignore any error + // (since it was probably caused by closing the pipes). + _ = <-c.goroutineErr + return ErrWaitDelay + + case err := <-c.goroutineErr: + return err + } +} + +// Output runs the command and returns its standard output. +// Any returned error will usually be of type *ExitError. +// If c.Stderr was nil, Output populates ExitError.Stderr. +func (c *Cmd) Output() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + var stdout bytes.Buffer + c.Stdout = &stdout + + captureErr := c.Stderr == nil + if captureErr { + c.Stderr = &prefixSuffixSaver{N: 32 << 10} + } + + err := c.Run() + if err != nil && captureErr { + if ee, ok := err.(*ExitError); ok { + ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes() + } + } + return stdout.Bytes(), err +} + +// CombinedOutput runs the command and returns its combined standard +// output and standard error. +func (c *Cmd) CombinedOutput() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + var b bytes.Buffer + c.Stdout = &b + c.Stderr = &b + err := c.Run() + return b.Bytes(), err +} + +// StdinPipe returns a pipe that will be connected to the command's +// standard input when the command starts. +// The pipe will be closed automatically after Wait sees the command exit. +// A caller need only call Close to force the pipe to close sooner. +// For example, if the command being run will not exit until standard input +// is closed, the caller must close the pipe. +func (c *Cmd) StdinPipe() (io.WriteCloser, error) { + if c.Stdin != nil { + return nil, errors.New("exec: Stdin already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdinPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdin = pr + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + return pw, nil +} + +// StdoutPipe returns a pipe that will be connected to the command's +// standard output when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to call Run when using StdoutPipe. +// See the example for idiomatic usage. +func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdoutPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdout = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// StderrPipe returns a pipe that will be connected to the command's +// standard error when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to use Run when using StderrPipe. +// See the StdoutPipe example for idiomatic usage. +func (c *Cmd) StderrPipe() (io.ReadCloser, error) { + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + if c.Process != nil { + return nil, errors.New("exec: StderrPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stderr = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// prefixSuffixSaver is an io.Writer which retains the first N bytes +// and the last N bytes written to it. The Bytes() methods reconstructs +// it with a pretty error message. +type prefixSuffixSaver struct { + N int // max size of prefix or suffix + prefix []byte + suffix []byte // ring buffer once len(suffix) == N + suffixOff int // offset to write into suffix + skipped int64 + + // TODO(bradfitz): we could keep one large []byte and use part of it for + // the prefix, reserve space for the '... Omitting N bytes ...' message, + // then the ring buffer suffix, and just rearrange the ring buffer + // suffix when Bytes() is called, but it doesn't seem worth it for + // now just for error messages. It's only ~64KB anyway. +} + +func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) { + lenp := len(p) + p = w.fill(&w.prefix, p) + + // Only keep the last w.N bytes of suffix data. + if overage := len(p) - w.N; overage > 0 { + p = p[overage:] + w.skipped += int64(overage) + } + p = w.fill(&w.suffix, p) + + // w.suffix is full now if p is non-empty. Overwrite it in a circle. + for len(p) > 0 { // 0, 1, or 2 iterations. + n := copy(w.suffix[w.suffixOff:], p) + p = p[n:] + w.skipped += int64(n) + w.suffixOff += n + if w.suffixOff == w.N { + w.suffixOff = 0 + } + } + return lenp, nil +} + +// fill appends up to len(p) bytes of p to *dst, such that *dst does not +// grow larger than w.N. It returns the un-appended suffix of p. +func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { + if remain := w.N - len(*dst); remain > 0 { + add := minInt(len(p), remain) + *dst = append(*dst, p[:add]...) + p = p[add:] + } + return p +} + +func (w *prefixSuffixSaver) Bytes() []byte { + if w.suffix == nil { + return w.prefix + } + if w.skipped == 0 { + return append(w.prefix, w.suffix...) + } + var buf bytes.Buffer + buf.Grow(len(w.prefix) + len(w.suffix) + 50) + buf.Write(w.prefix) + buf.WriteString("\n... omitting ") + buf.WriteString(strconv.FormatInt(w.skipped, 10)) + buf.WriteString(" bytes ...\n") + buf.Write(w.suffix[w.suffixOff:]) + buf.Write(w.suffix[:w.suffixOff]) + return buf.Bytes() +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// environ returns a best-effort copy of the environment in which the command +// would be run as it is currently configured. If an error occurs in computing +// the environment, it is returned alongside the best-effort copy. +func (c *Cmd) environ() ([]string, error) { + /* TODO(xsw): + var err error + + env := c.Env + if env == nil { + env, err = execenv.Default(c.SysProcAttr) + if err != nil { + env = os.Environ() + // Note that the non-nil err is preserved despite env being overridden. + } + + if c.Dir != "" { + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable, so we don't need to + // keep it accurate. + default: + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory for the command, we should also update PWD to reflect that. + // + // Unfortunately, we didn't always do that, so (as proposed in + // https://go.dev/issue/50599) to avoid unintended collateral damage we + // only implicitly update PWD when Env is nil. That way, we're much + // less likely to override an intentional change to the variable. + if pwd, absErr := filepath.Abs(c.Dir); absErr == nil { + env = append(env, "PWD="+pwd) + } else if err == nil { + err = absErr + } + } + } + } + + env, dedupErr := dedupEnv(env) + if err == nil { + err = dedupErr + } + return addCriticalEnv(env), err + */ + panic("todo: exec.Cmd.environ") +} + +// Environ returns a copy of the environment in which the command would be run +// as it is currently configured. +func (c *Cmd) Environ() []string { + // Intentionally ignore errors: environ returns a best-effort environment no matter what. + env, _ := c.environ() + return env +} + +// dedupEnv returns a copy of env with any duplicates removed, in favor of +// later values. +// Items not of the normal environment "key=value" form are preserved unchanged. +// Except on Plan 9, items containing NUL characters are removed, and +// an error is returned along with the remaining values. +func dedupEnv(env []string) ([]string, error) { + return dedupEnvCase(runtime.GOOS == "windows", runtime.GOOS == "plan9", env) +} + +// dedupEnvCase is dedupEnv with a case option for testing. +// If caseInsensitive is true, the case of keys is ignored. +// If nulOK is false, items containing NUL characters are allowed. +func dedupEnvCase(caseInsensitive, nulOK bool, env []string) ([]string, error) { + // Construct the output in reverse order, to preserve the + // last occurrence of each key. + var err error + out := make([]string, 0, len(env)) + saw := make(map[string]bool, len(env)) + for n := len(env); n > 0; n-- { + kv := env[n-1] + + // Reject NUL in environment variables to prevent security issues (#56284); + // except on Plan 9, which uses NUL as os.PathListSeparator (#56544). + if !nulOK && strings.IndexByte(kv, 0) != -1 { + err = errors.New("exec: environment variable contains NUL") + continue + } + + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-"="? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + if kv != "" { + // The entry is not of the form "key=value" (as it is required to be). + // Leave it as-is for now. + // TODO(#52436): should we strip or reject these bogus entries? + out = append(out, kv) + } + continue + } + k := kv[:i] + if caseInsensitive { + k = strings.ToLower(k) + } + if saw[k] { + continue + } + + saw[k] = true + out = append(out, kv) + } + + // Now reverse the slice to restore the original order. + for i := 0; i < len(out)/2; i++ { + j := len(out) - i - 1 + out[i], out[j] = out[j], out[i] + } + + return out, err +} + +// addCriticalEnv adds any critical environment variables that are required +// (or at least almost always required) on the operating system. +// Currently this is only used for Windows. +func addCriticalEnv(env []string) []string { + if runtime.GOOS != "windows" { + return env + } + for _, kv := range env { + k, _, ok := strings.Cut(kv, "=") + if !ok { + continue + } + if strings.EqualFold(k, "SYSTEMROOT") { + // We already have it. + return env + } + } + return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) +} + +// ErrDot indicates that a path lookup resolved to an executable +// in the current directory due to ‘.’ being in the path, either +// implicitly or explicitly. See the package documentation for details. +// +// Note that functions in this package do not return ErrDot directly. +// Code should use errors.Is(err, ErrDot), not err == ErrDot, +// to test whether a returned error err is due to this condition. +var ErrDot = errors.New("cannot run executable found relative to current directory") diff --git a/internal/lib/os/exec/lp_unix.go b/internal/lib/os/exec/lp_unix.go index 7d6bbc02..757189d2 100644 --- a/internal/lib/os/exec/lp_unix.go +++ b/internal/lib/os/exec/lp_unix.go @@ -8,13 +8,17 @@ package exec import ( "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" ) // ErrNotFound is the error resulting if a path search failed to find an executable file. var ErrNotFound = errors.New("executable file not found in $PATH") func findExecutable(file string) error { - /* TODO(xsw): d, err := os.Stat(file) if err != nil { return err @@ -23,7 +27,7 @@ func findExecutable(file string) error { if m.IsDir() { return syscall.EISDIR } - err = unix.Eaccess(file, unix.X_OK) + err = unixEaccess(file, unix_X_OK) // ENOSYS means Eaccess is not available or not implemented. // EPERM can be returned by Linux containers employing seccomp. // In both cases, fall back to checking the permission bits. @@ -34,8 +38,6 @@ func findExecutable(file string) error { return nil } return fs.ErrPermission - */ - panic("todo: exec.findExecutable") } // LookPath searches for an executable named file in the @@ -47,7 +49,6 @@ func findExecutable(file string) error { // As of Go 1.19, LookPath will instead return that path along with an error satisfying // errors.Is(err, ErrDot). See the package documentation for more details. func LookPath(file string) (string, error) { - /* TODO(xsw): // NOTE(rsc): I wish we could use the Plan 9 behavior here // (only bypass the path if file begins with / or ./ or ../) // but that would not match all the Unix shells. @@ -68,15 +69,16 @@ func LookPath(file string) (string, error) { path := filepath.Join(dir, file) if err := findExecutable(path); err == nil { if !filepath.IsAbs(path) { + /* TODO(xsw): if execerrdot.Value() != "0" { return path, &Error{file, ErrDot} } execerrdot.IncNonDefault() + */ + panic("todo: exec.LookPath: !filepath.IsAbs(path)") } return path, nil } } return "", &Error{file, ErrNotFound} - */ - panic("todo: exec.LookPath") } diff --git a/internal/lib/os/exec/unix_constants.go b/internal/lib/os/exec/unix_constants.go new file mode 100644 index 00000000..8bbbbac8 --- /dev/null +++ b/internal/lib/os/exec/unix_constants.go @@ -0,0 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package exec + +const ( + unix_R_OK = 0x4 + unix_W_OK = 0x2 + unix_X_OK = 0x1 +) diff --git a/internal/lib/os/exec/unix_eaccess_linux.go b/internal/lib/os/exec/unix_eaccess_linux.go new file mode 100644 index 00000000..68d250b9 --- /dev/null +++ b/internal/lib/os/exec/unix_eaccess_linux.go @@ -0,0 +1,11 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import "syscall" + +func unixEaccess(path string, mode uint32) error { + return syscall.Faccessat(AT_FDCWD, path, mode, AT_EACCESS) +} diff --git a/internal/lib/os/exec/unix_eaccess_other.go b/internal/lib/os/exec/unix_eaccess_other.go new file mode 100644 index 00000000..8b99269c --- /dev/null +++ b/internal/lib/os/exec/unix_eaccess_other.go @@ -0,0 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix && !linux + +package exec + +import "syscall" + +func unixEaccess(path string, mode uint32) error { + return syscall.ENOSYS +} diff --git a/internal/lib/syscall/syscall.go b/internal/lib/syscall/syscall.go index a28d8820..045dc87b 100644 --- a/internal/lib/syscall/syscall.go +++ b/internal/lib/syscall/syscall.go @@ -24,6 +24,34 @@ import ( "github.com/goplus/llgo/c/os" ) +// Unix returns the time stored in ts as seconds plus nanoseconds. +func (ts *Timespec) Unix() (sec int64, nsec int64) { + return int64(ts.Sec), int64(ts.Nsec) +} + +// Unix returns the time stored in tv as seconds plus nanoseconds. +func (tv *Timeval) Unix() (sec int64, nsec int64) { + return int64(tv.Sec), int64(tv.Usec) * 1000 +} + +// Nano returns the time stored in ts as nanoseconds. +func (ts *Timespec) Nano() int64 { + return int64(ts.Sec)*1e9 + int64(ts.Nsec) +} + +// Nano returns the time stored in tv as nanoseconds. +func (tv *Timeval) Nano() int64 { + return int64(tv.Sec)*1e9 + int64(tv.Usec)*1000 +} + +func Getpagesize() int { + panic("todo: syscall.Getpagesize") +} + +func Exit(code int) { + os.Exit(c.Int(code)) +} + func Getcwd(buf []byte) (n int, err error) { ptr := unsafe.Pointer(unsafe.SliceData(buf)) ret := os.Getcwd(ptr, uintptr(len(buf)))