diff --git a/src/os/exec.go b/src/os/exec.go index 28406f916b..cc00a10e0e 100644 --- a/src/os/exec.go +++ b/src/os/exec.go @@ -5,6 +5,9 @@ import ( "syscall" ) +// Errors StartProcess returns for a ProcAttr that it cannot honour. On a +// hosted OS only ErrNotImplementedSys is reachable. The other two stay because +// they are part of the exported API of this package. var ( ErrNotImplementedDir = errors.New("directory setting not implemented") ErrNotImplementedSys = errors.New("sys setting not implemented") @@ -36,35 +39,12 @@ type ProcAttr struct { // ErrProcessDone indicates a Process has finished. var ErrProcessDone = errors.New("os: process already finished") -type ProcessState struct { -} - -func (p *ProcessState) String() string { - return "" // TODO -} -func (p *ProcessState) Success() bool { - return false // TODO -} - -// Sys returns system-dependent exit information about -// the process. Convert it to the appropriate underlying -// type, such as syscall.WaitStatus on Unix, to access its contents. -func (p *ProcessState) Sys() interface{} { - return nil // TODO -} - -func (p *ProcessState) Exited() bool { - return false // TODO -} - -// ExitCode returns the exit code of the exited process, or -1 -// if the process hasn't exited or was terminated by a signal. -func (p *ProcessState) ExitCode() int { - return -1 // TODO -} - type Process struct { Pid int + + // done reports whether Wait reaped this process. A signal to a reaped pid + // is unsafe, because the number can belong to an unrelated process. + done int32 } // StartProcess starts a new process with the program, arguments and attributes specified by name, argv and attr. @@ -73,21 +53,6 @@ func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) return startProcess(name, argv, attr) } -func (p *Process) Wait() (*ProcessState, error) { - if p.Pid == -1 { - return nil, syscall.EINVAL - } - return nil, ErrNotImplemented -} - -func (p *Process) Kill() error { - return ErrNotImplemented -} - -func (p *Process) Signal(sig Signal) error { - return ErrNotImplemented -} - func Ignore(sig ...Signal) { // leave all the signals unaltered return diff --git a/src/os/exec_linux.go b/src/os/exec_linux.go deleted file mode 100644 index 6914a2c285..0000000000 --- a/src/os/exec_linux.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2009 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 linux && !baremetal && !tinygo.wasm && !nintendoswitch - -package os - -import ( - "errors" - "runtime" - "syscall" -) - -// The only signal values guaranteed to be present in the os package on all -// systems are os.Interrupt (send the process an interrupt) and os.Kill (force -// the process to exit). On Windows, sending os.Interrupt to a process with -// os.Process.Signal is not implemented; it will return an error instead of -// sending a signal. -var ( - Interrupt Signal = syscall.SIGINT - Kill Signal = syscall.SIGKILL -) - -// Keep compatible with golang and always succeed and return new proc with pid on Linux. -func findProcess(pid int) (*Process, error) { - return &Process{Pid: pid}, nil -} - -func (p *Process) release() error { - // NOOP for unix. - p.Pid = -1 - // no need for a finalizer anymore - runtime.SetFinalizer(p, nil) - return nil -} - -// This function is a wrapper around the forkExec function, which is a wrapper around the fork and execve system calls. -// The StartProcess function creates a new process by forking the current process and then calling execve to replace the current process with the new process. -// It thereby replaces the newly created process with the specified command and arguments. -// Differences to upstream golang implementation (https://cs.opensource.google/go/go/+/master:src/syscall/exec_unix.go;l=143): -// * No setting of Process Attributes -// * Ignoring Ctty -// * No ForkLocking (might be introduced by #4273) -// * No parent-child communication via pipes (TODO) -// * No waiting for crashes child processes to prohibit zombie process accumulation / Wait status checking (TODO) -func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) { - if argv == nil { - return 0, errors.New("exec: no argv") - } - - if len(argv) == 0 { - return 0, errors.New("exec: no argv") - } - - if attr == nil { - attr = new(ProcAttr) - } - - p, err := fork() - pid = int(p) - - if err != nil { - return 0, err - } - - // else code runs in child, which then should exec the new process - err = execve(argv0, argv, attr.Env) - if err != nil { - // exec failed - return 0, err - } - // 3. TODO: use pipes to communicate back child status - return pid, nil -} - -// In Golang, the idiomatic way to create a new process is to use the StartProcess function. -// Since the Model of operating system processes in tinygo differs from the one in Golang, we need to implement the StartProcess function differently. -// The startProcess function is a wrapper around the forkExec function, which is a wrapper around the fork and execve system calls. -// The StartProcess function creates a new process by forking the current process and then calling execve to replace the current process with the new process. -// It thereby replaces the newly created process with the specified command and arguments. -func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { - if attr != nil { - if attr.Dir != "" { - return nil, ErrNotImplementedDir - } - - if attr.Sys != nil { - return nil, ErrNotImplementedSys - } - - if len(attr.Files) != 0 { - return nil, ErrNotImplementedFiles - } - } - - pid, err := forkExec(name, argv, attr) - if err != nil { - return nil, err - } - - return findProcess(pid) -} diff --git a/src/os/exec_linux_test.go b/src/os/exec_linux_test.go deleted file mode 100644 index 34f1fef983..0000000000 --- a/src/os/exec_linux_test.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build linux && !baremetal && !tinygo.wasm - -package os_test - -import ( - "errors" - . "os" - "runtime" - "syscall" - "testing" -) - -// Test the functionality of the forkExec function, which is used to fork and exec a new process. -// This test is not run on Windows, as forkExec is not supported on Windows. -// This test is not run on Plan 9, as forkExec is not supported on Plan 9. -func TestForkExec(t *testing.T) { - if runtime.GOOS != "linux" { - t.Logf("skipping test on %s", runtime.GOOS) - return - } - - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{}) - if !errors.Is(err, nil) { - t.Fatalf("forkExec failed: %v", err) - } - - if proc == nil { - t.Fatalf("proc is nil") - } - - if proc.Pid == 0 { - t.Fatalf("forkExec failed: new process has pid 0") - } -} - -func TestForkExecErrNotExist(t *testing.T) { - proc, err := StartProcess("invalid", []string{"invalid"}, &ProcAttr{}) - if !errors.Is(err, ErrNotExist) { - t.Fatalf("wanted ErrNotExist, got %s\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} - -func TestForkExecProcDir(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Dir: "dir"}) - if !errors.Is(err, ErrNotImplementedDir) { - t.Fatalf("wanted ErrNotImplementedDir, got %v\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} - -func TestForkExecProcSys(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Sys: &syscall.SysProcAttr{}}) - if !errors.Is(err, ErrNotImplementedSys) { - t.Fatalf("wanted ErrNotImplementedSys, got %v\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} - -func TestForkExecProcFiles(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Files: []*File{}}) - if !errors.Is(err, ErrNotImplementedFiles) { - t.Fatalf("wanted ErrNotImplementedFiles, got %v\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} diff --git a/src/os/exec_other.go b/src/os/exec_other.go index b05e2830db..05fcc39988 100644 --- a/src/os/exec_other.go +++ b/src/os/exec_other.go @@ -1,4 +1,4 @@ -//go:build (!aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm || nintendoswitch +//go:build (!aix && !android && !darwin && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm || nintendoswitch package os @@ -18,6 +18,49 @@ func (p *Process) release() error { return nil } +// ProcessState is a placeholder on targets that have no process model. +type ProcessState struct { +} + +func (p *ProcessState) String() string { + return "" // TODO +} +func (p *ProcessState) Success() bool { + return false // TODO +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() interface{} { + return nil // TODO +} + +func (p *ProcessState) Exited() bool { + return false // TODO +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + return -1 // TODO +} + +func (p *Process) Wait() (*ProcessState, error) { + if p.Pid == -1 { + return nil, syscall.EINVAL + } + return nil, ErrNotImplemented +} + +func (p *Process) Kill() error { + return ErrNotImplemented +} + +func (p *Process) Signal(sig Signal) error { + return ErrNotImplemented +} + func forkExec(_ string, _ []string, _ *ProcAttr) (pid int, err error) { return 0, ErrNotImplemented } diff --git a/src/os/exec_posix_spawn.go b/src/os/exec_posix_spawn.go new file mode 100644 index 0000000000..485f9a585f --- /dev/null +++ b/src/os/exec_posix_spawn.go @@ -0,0 +1,446 @@ +// Copyright 2009 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 (linux || darwin) && !baremetal && !tinygo.wasm && !nintendoswitch + +package os + +import ( + "errors" + "internal/itoa" + "runtime" + "sync/atomic" + "syscall" + _ "unsafe" // for go:linkname +) + +// Process creation on a hosted OS uses posix_spawn(3) and not fork(2) plus +// execve(2). These targets run the threads scheduler and collect with Boehm, +// so a fork from Go gives the child one thread that holds the locks of the +// other threads, and the stop-the-world signal of the collector can arrive +// between the fork and the exec. posix_spawn does the clone and the exec +// inside libc, where no Go code runs. +// +// posix_spawn takes two POSIX objects whose shape is different for each OS, so +// the types are in exec_posix_spawn_linux.go and exec_posix_spawn_darwin.go. + +// The only signal values guaranteed to be present in the os package on all +// systems are os.Interrupt (send the process an interrupt) and os.Kill (force +// the process to exit). On Windows, sending os.Interrupt to a process with +// os.Process.Signal is not implemented; it will return an error instead of +// sending a signal. +var ( + Interrupt Signal = syscall.SIGINT + Kill Signal = syscall.SIGKILL +) + +// Give the child an empty signal mask. A blocked mask survives an exec, and +// the spawning thread can carry the signal of the collector blocked. +const _POSIX_SPAWN_SETSIGMASK = 0x08 + +// POSIX_SPAWN_SETPGROUP puts the child in the process group of +// posix_spawnattr_setpgroup. The value is 2 in lib/musl/include/spawn.h and in +// the of Darwin. +const _POSIX_SPAWN_SETPGROUP = 0x02 + +// Keep compatible with golang and always succeed and return new proc with pid. +func findProcess(pid int) (*Process, error) { + return &Process{Pid: pid}, nil +} + +func (p *Process) release() error { + // NOOP for unix. + p.Pid = -1 + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status syscall.WaitStatus // System-dependent status info. + rusage *syscall.Rusage +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) String() string { + if p == nil { + return "" + } + status := p.status + res := "" + switch { + case status.Exited(): + res = "exit status " + itoa.Itoa(status.ExitStatus()) + case status.Signaled(): + res = "signal: " + status.Signal().String() + case status.Stopped(): + res = "stop signal: " + status.StopSignal().String() + if status.StopSignal() == syscall.SIGTRAP && status.TrapCause() != 0 { + res += " (trap " + itoa.Itoa(status.TrapCause()) + ")" + } + case status.Continued(): + res = "continued" + } + if status.CoreDump() { + res += " (core dumped)" + } + return res +} + +func (p *ProcessState) Success() bool { + return p.status.ExitStatus() == 0 +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() interface{} { + return p.status +} + +// SysUsage returns system-dependent resource usage information about +// the exited process. Convert it to the appropriate underlying +// type, such as *syscall.Rusage on Unix, to access its contents. +func (p *ProcessState) SysUsage() interface{} { + return p.rusage +} + +func (p *ProcessState) Exited() bool { + return p.status.Exited() +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + // return -1 if the process hasn't started. + if p == nil || !p.status.Exited() { + return -1 + } + return p.status.ExitStatus() +} + +// Wait waits for the Process to exit, and then returns a ProcessState +// describing its status and an error, if any. +func (p *Process) Wait() (*ProcessState, error) { + if p.Pid == -1 { + return nil, syscall.EINVAL + } + var status syscall.WaitStatus + var rusage syscall.Rusage + var wpid int + var err error + for { + wpid, err = syscall.Wait4(p.Pid, &status, 0, &rusage) + // The collector stops the world with a signal, so a thread in wait4 + // gets EINTR as a matter of course. + if err != syscall.EINTR { + break + } + } + if err != nil { + return nil, NewSyscallError("wait", err) + } + atomic.StoreInt32(&p.done, 1) + return &ProcessState{pid: wpid, status: status, rusage: &rusage}, nil +} + +// Signal sends a signal to the Process. Sending Interrupt on Windows is not +// implemented. +func (p *Process) Signal(sig Signal) error { + if p.Pid == -1 { + return errors.New("os: process already released") + } + if p.Pid == 0 { + return errors.New("os: process not initialized") + } + if atomic.LoadInt32(&p.done) != 0 { + return ErrProcessDone + } + s, ok := sig.(syscall.Signal) + if !ok { + return errors.New("os: unsupported signal type") + } + if err := syscall.Kill(p.Pid, s); err != nil { + // Another goroutine can reap the process between the check above and + // the kill. exec.CommandContext expects ErrProcessDone here. + if err == syscall.ESRCH { + return ErrProcessDone + } + return err + } + return nil +} + +// Kill causes the Process to exit immediately. Kill does not wait until the +// Process has actually exited. This only kills the Process itself, not any +// other processes it may have started. +func (p *Process) Kill() error { + return p.Signal(Kill) +} + +// startProcess creates the child with posix_spawn instead of a fork and exec +// pair. +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + if attr == nil { + attr = new(ProcAttr) + } + if attr.Sys != nil { + // Refuse by name every field that posix_spawn cannot express. Only + // Setpgid and Pgid are honoured. + if err := checkSysProcAttr(attr.Sys); err != nil { + return nil, err + } + } + + pid, err := forkExec(name, argv, attr) + if err != nil { + return nil, err + } + + return &Process{Pid: pid}, nil +} + +// forkExec spawns the program at argv0 and returns its pid. It does not fork. +// posix_spawn reports a failed exec as its return value, so no status pipe is +// necessary. +func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) { + if len(argv) == 0 { + return 0, errors.New("exec: no argv") + } + if attr == nil { + attr = new(ProcAttr) + } + + argv0p, err := syscall.BytePtrFromString(argv0) + if err != nil { + return 0, err + } + argvp, err := syscall.SlicePtrFromStrings(argv) + if err != nil { + return 0, err + } + env := attr.Env + if env == nil { + // A nil Env means the environment of the parent. + env = Environ() + } + envp, err := syscall.SlicePtrFromStrings(env) + if err != nil { + return 0, err + } + + var fa spawnFileActions + if errno := posix_spawn_file_actions_init(&fa); errno != 0 { + return 0, syscall.Errno(errno) + } + defer posix_spawn_file_actions_destroy(&fa) + + var sa spawnAttr + if errno := posix_spawnattr_init(&sa); errno != 0 { + return 0, syscall.Errno(errno) + } + defer posix_spawnattr_destroy(&sa) + + var mask sigset + if errno := posix_spawnattr_setsigmask(&sa, &mask); errno != 0 { + return 0, syscall.Errno(errno) + } + + flags := int16(_POSIX_SPAWN_SETSIGMASK) + + // Setpgid is the one SysProcAttr field that posix_spawn can express. A + // Pgid of 0 makes a new group whose id is the pid of the child. + if attr.Sys != nil && attr.Sys.Setpgid { + if errno := posix_spawnattr_setpgroup(&sa, int32(attr.Sys.Pgid)); errno != 0 { + return 0, syscall.Errno(errno) + } + flags |= _POSIX_SPAWN_SETPGROUP + } + + if errno := posix_spawnattr_setflags(&sa, flags); errno != 0 { + return 0, syscall.Errno(errno) + } + + if attr.Dir != "" { + dirp, err := syscall.BytePtrFromString(attr.Dir) + if err != nil { + return 0, err + } + // Darwin stores the pointer and not a copy of the path, and the + // collector cannot see it. Keep the Go bytes alive until the spawn. + defer runtime.KeepAlive(dirp) + if errno := posix_spawn_file_actions_addchdir_np(&fa, dirp); errno != 0 { + return 0, syscall.Errno(errno) + } + } + + defer runtime.KeepAlive(attr.Files) + fds := make([]int, len(attr.Files)) + nextfd := len(fds) + if nextfd < 3 { + nextfd = 3 + } + for i, f := range attr.Files { + fd := ^uintptr(0) + if f != nil { + fd = f.Fd() + } + fds[i] = -1 + if fd != ^uintptr(0) { + if fd >= 1<<31-1 { + return 0, syscall.EBADF + } + fds[i] = int(fd) + if int(fd) >= nextfd { + nextfd = int(fd) + 1 + } + } + } + + // Save sources before an earlier action replaces or closes them. + // See Go src/syscall/exec_linux.go, forkAndExecInChild, Pass 1. + firstTemp := nextfd + for i, fd := range fds { + if fd >= 0 && fd < i { + if nextfd >= 1<<31-1 { + return 0, syscall.EINVAL + } + if errno := posix_spawn_file_actions_adddup2(&fa, int32(fd), int32(nextfd)); errno != 0 { + return 0, syscall.Errno(errno) + } + fds[i] = nextfd + nextfd++ + } + } + + for i, fd := range fds { + if fd == -1 { + if err := addSpawnClose(&fa, int32(i)); err != nil { + return 0, err + } + continue + } + // A dup2 onto the same descriptor clears FD_CLOEXEC and is not a + // no-op, which is what an inherited os.Stdin needs. + if errno := posix_spawn_file_actions_adddup2(&fa, int32(fd), int32(i)); errno != 0 { + return 0, syscall.Errno(errno) + } + } + + // Close the standard descriptors that ProcAttr.Files does not name, which + // is what syscall.forkAndExecInChild does in the standard library. + for i := len(attr.Files); i < 3; i++ { + if err := addSpawnClose(&fa, int32(i)); err != nil { + return 0, err + } + } + for fd := firstTemp; fd < nextfd; fd++ { + if errno := posix_spawn_file_actions_addclose(&fa, int32(fd)); errno != 0 { + return 0, syscall.Errno(errno) + } + } + + var childPid int32 + // ForkLock keeps a descriptor made without O_CLOEXEC out of a child that + // another goroutine spawns at the same time. + syscall.ForkLock.Lock() + errno := posix_spawn(&childPid, argv0p, &fa, &sa, &argvp[0], &envp[0]) + syscall.ForkLock.Unlock() + runtime.KeepAlive(argv0p) + runtime.KeepAlive(argvp) + runtime.KeepAlive(envp) + if errno != 0 { + return 0, syscall.Errno(errno) + } + + return int(childPid), nil +} + +// Bindings for the posix_spawn family. They use //go:linkname and not +// //export, because //export promises that pointer arguments do not escape, +// and the objects here outlive the call. + +//go:linkname posix_spawn posix_spawn +func posix_spawn(pid *int32, path *byte, fa *spawnFileActions, sa *spawnAttr, argv **byte, envp **byte) int32 + +//go:linkname posix_spawn_file_actions_init posix_spawn_file_actions_init +func posix_spawn_file_actions_init(fa *spawnFileActions) int32 + +//go:linkname posix_spawn_file_actions_destroy posix_spawn_file_actions_destroy +func posix_spawn_file_actions_destroy(fa *spawnFileActions) int32 + +//go:linkname posix_spawn_file_actions_adddup2 posix_spawn_file_actions_adddup2 +func posix_spawn_file_actions_adddup2(fa *spawnFileActions, fildes, newfildes int32) int32 + +//go:linkname posix_spawn_file_actions_addclose posix_spawn_file_actions_addclose +func posix_spawn_file_actions_addclose(fa *spawnFileActions, fildes int32) int32 + +// Present in musl since 1.1.24 and in macOS since 10.15. +// +//go:linkname posix_spawn_file_actions_addchdir_np posix_spawn_file_actions_addchdir_np +func posix_spawn_file_actions_addchdir_np(fa *spawnFileActions, path *byte) int32 + +//go:linkname posix_spawnattr_init posix_spawnattr_init +func posix_spawnattr_init(sa *spawnAttr) int32 + +//go:linkname posix_spawnattr_destroy posix_spawnattr_destroy +func posix_spawnattr_destroy(sa *spawnAttr) int32 + +//go:linkname posix_spawnattr_setflags posix_spawnattr_setflags +func posix_spawnattr_setflags(sa *spawnAttr, flags int16) int32 + +//go:linkname posix_spawnattr_setsigmask posix_spawnattr_setsigmask +func posix_spawnattr_setsigmask(sa *spawnAttr, mask *sigset) int32 + +//go:linkname posix_spawnattr_setpgroup posix_spawnattr_setpgroup +func posix_spawnattr_setpgroup(sa *spawnAttr, pgroup int32) int32 + +// unsupportedSysFieldError names the SysProcAttr field that this +// implementation cannot honour. It unwraps to ErrNotImplementedSys. +type unsupportedSysFieldError struct { + field string +} + +func (e *unsupportedSysFieldError) Error() string { + return "os: SysProcAttr." + e.field + ": " + ErrNotImplementedSys.Error() +} + +func (e *unsupportedSysFieldError) Unwrap() error { + return ErrNotImplementedSys +} + +func errUnsupportedSysField(field string) error { + return &unsupportedSysFieldError{field: field} +} + +// checkSysProcAttrCommon rejects every field that both Linux and Darwin +// declare and that posix_spawn cannot express. Setpgid and Pgid are absent, +// because forkExec honours them. +func checkSysProcAttrCommon(sys *syscall.SysProcAttr) error { + switch { + case sys.Chroot != "": + return errUnsupportedSysField("Chroot") + case sys.Credential != nil: + return errUnsupportedSysField("Credential") + case sys.Ptrace: + return errUnsupportedSysField("Ptrace") + case sys.Setsid: + return errUnsupportedSysField("Setsid") + case sys.Setctty: + return errUnsupportedSysField("Setctty") + case sys.Noctty: + return errUnsupportedSysField("Noctty") + case sys.Ctty != 0: + return errUnsupportedSysField("Ctty") + case sys.Foreground: + return errUnsupportedSysField("Foreground") + } + return nil +} diff --git a/src/os/exec_posix_spawn_darwin.go b/src/os/exec_posix_spawn_darwin.go new file mode 100644 index 0000000000..e72fe2164a --- /dev/null +++ b/src/os/exec_posix_spawn_darwin.go @@ -0,0 +1,42 @@ +//go:build darwin + +package os + +import ( + "syscall" + _ "unsafe" // Required by go:linkname. See https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives. +) + +var spawnDevNull = [...]byte{'/', 'd', 'e', 'v', '/', 'n', 'u', 'l', 'l', 0} + +func addSpawnClose(fa *spawnFileActions, fd int32) error { + // Darwin rejects close actions on unopened descriptors. + // See posix_spawn(2), ERRORS, EBADF. + if errno := posix_spawn_file_actions_addopen(fa, fd, &spawnDevNull[0], syscall.O_RDONLY, 0); errno != 0 { + return syscall.Errno(errno) + } + if errno := posix_spawn_file_actions_addclose(fa, fd); errno != 0 { + return syscall.Errno(errno) + } + return nil +} + +//go:linkname posix_spawn_file_actions_addopen posix_spawn_file_actions_addopen +func posix_spawn_file_actions_addopen(fa *spawnFileActions, fd int32, path *byte, flags int32, mode uint16) int32 + +// Darwin declares both POSIX objects as opaque pointers, so the +// object a caller allocates is one pointer wide and libc allocates the rest. +// The type is uintptr because libc stores memory there that is not Go memory. +type spawnFileActions uintptr + +type spawnAttr uintptr + +// The sigset_t of Darwin is a 32-bit mask. The zero value is the empty set. +type sigset uint32 + +// checkSysProcAttr reports whether the SysProcAttr asks for something that +// posix_spawn cannot do. Darwin declares only the common fields plus Setpgid +// and Pgid. +func checkSysProcAttr(sys *syscall.SysProcAttr) error { + return checkSysProcAttrCommon(sys) +} diff --git a/src/os/exec_posix_spawn_linux.go b/src/os/exec_posix_spawn_linux.go new file mode 100644 index 0000000000..f5a0663d5f --- /dev/null +++ b/src/os/exec_posix_spawn_linux.go @@ -0,0 +1,73 @@ +//go:build linux && !baremetal && !tinygo.wasm && !nintendoswitch + +package os + +import "syscall" + +func addSpawnClose(fa *spawnFileActions, fd int32) error { + if errno := posix_spawn_file_actions_addclose(fa, fd); errno != 0 { + return syscall.Errno(errno) + } + return nil +} + +// Storage for the two by-value POSIX objects posix_spawn takes. musl declares +// them in lib/musl/include/spawn.h as +// +// typedef struct { +// int __pad0[2]; +// void *__actions; +// int __pad[16]; +// } posix_spawn_file_actions_t; +// +// typedef struct { +// int __flags; +// pid_t __pgrp; +// sigset_t __def, __mask; +// int __prio, __pol; +// void *__fn; +// char __pad[64-sizeof(void *)]; +// } posix_spawnattr_t; +// +// with a sigset_t of 128 bytes. The file-actions object is then 80 bytes on +// LP64 and 76 bytes on a 32-bit target, and the attribute object is 336 bytes. +// The arrays below are larger than that and uint64 for the alignment. Only +// libc looks inside them. +type spawnFileActions [16]uint64 + +type spawnAttr [48]uint64 + +// The sigset_t of musl, 128 bytes. The zero value is the empty set. +type sigset [16]uint64 + +// checkSysProcAttr reports whether the SysProcAttr asks for something that +// posix_spawn cannot do. Linux declares more fields than POSIX, and each of +// them needs Go code to run in the child between the clone and the exec. +func checkSysProcAttr(sys *syscall.SysProcAttr) error { + if err := checkSysProcAttrCommon(sys); err != nil { + return err + } + switch { + case sys.Pdeathsig != 0: + return errUnsupportedSysField("Pdeathsig") + case sys.Cloneflags != 0: + return errUnsupportedSysField("Cloneflags") + case sys.Unshareflags != 0: + return errUnsupportedSysField("Unshareflags") + case sys.UidMappings != nil: + return errUnsupportedSysField("UidMappings") + case sys.GidMappings != nil: + return errUnsupportedSysField("GidMappings") + case sys.GidMappingsEnableSetgroups: + return errUnsupportedSysField("GidMappingsEnableSetgroups") + case sys.AmbientCaps != nil: + return errUnsupportedSysField("AmbientCaps") + case sys.UseCgroupFD: + return errUnsupportedSysField("UseCgroupFD") + case sys.CgroupFD != 0: + return errUnsupportedSysField("CgroupFD") + case sys.PidFD != nil: + return errUnsupportedSysField("PidFD") + } + return nil +} diff --git a/src/os/exec_remap_test.go b/src/os/exec_remap_test.go new file mode 100644 index 0000000000..a96fafc2a1 --- /dev/null +++ b/src/os/exec_remap_test.go @@ -0,0 +1,138 @@ +//go:build (linux || darwin) && !baremetal && !tinygo.wasm && !nintendoswitch + +package os_test + +import ( + "errors" + "fmt" + . "os" + "syscall" + "testing" +) + +func TestForkExecFileRemapping(t *testing.T) { + if _, err := Stat("/bin/bash"); err != nil { + t.Skip("file remapping checks need /bin/bash for descriptors above 9") + } + for _, name := range []string{"cycle", "repeated", "closed-source", "identity", "sparse"} { + t.Run(name, func(t *testing.T) { + a, err := CreateTemp(t.TempDir(), "source-a") + if err != nil { + t.Fatal(err) + } + defer a.Close() + b, err := CreateTemp(t.TempDir(), "source-b") + if err != nil { + t.Fatal(err) + } + defer b.Close() + out, err := CreateTemp(t.TempDir(), "output") + if err != nil { + t.Fatal(err) + } + defer out.Close() + for i, f := range []*File{a, b} { + if _, err := f.WriteString([]string{"A\n", "B\n"}[i]); err != nil { + t.Fatal(err) + } + if _, err := f.Seek(0, 0); err != nil { + t.Fatal(err) + } + } + x, y := int(a.Fd()), int(b.Fd()) + if x < 3 || y <= x { + t.Fatalf("unexpected source descriptors %d, %d", x, y) + } + files := make([]*File, y+3) + files[1], files[2] = out, Stderr + targets := []int{x, y} + switch name { + case "cycle": + files[x], files[y] = b, a + case "repeated": + files[x], files[y], files[y+1] = b, a, a + targets = []int{x, y, y + 1} + case "closed-source": + files[y] = a + targets = []int{y} + case "identity": + files[x], files[y] = a, b + case "sparse": + files[y+2] = a + targets = []int{y + 2} + } + for _, fd := range targets { + for _, f := range []*File{a, b} { + if _, err := f.Seek(0, 0); err != nil { + t.Fatal(err) + } + } + want := "A" + if files[fd] == b { + want = "B" + } + script := fmt.Sprintf("exec 0<&%d; IFS= read -r value; test \"$value\" = \"$1\"", fd) + checkRemapChild(t, files, script, want, true) + } + nextfd := len(files) + for _, f := range files { + if f != nil && int(f.Fd()) >= nextfd { + nextfd = int(f.Fd()) + 1 + } + } + for i, f := range files { + if f == nil && i >= 3 { + script := fmt.Sprintf("exec 2>/dev/null; exec 0<&%d", i) + checkRemapChild(t, files, script, "", false) + } + if f != nil && int(f.Fd()) < i { + script := fmt.Sprintf("exec 2>/dev/null; exec 0<&%d", nextfd) + checkRemapChild(t, files, script, "", false) + nextfd++ + } + } + for i, f := range []*File{a, b} { + got := make([]byte, 1) + _, err := f.ReadAt(got, 0) + if err != nil || string(got) != []string{"A", "B"}[i] { + t.Fatalf("parent source = %q, %v", got, err) + } + } + }) + } +} + +func checkRemapChild(t *testing.T, files []*File, script, want string, success bool) { + t.Helper() + proc, err := StartProcess("/bin/bash", []string{"bash", "-c", script, "bash", want}, &ProcAttr{Files: files}) + if err != nil { + t.Fatalf("StartProcess for %q = %v", script, err) + } + state, err := proc.Wait() + if err != nil || state.Success() != success { + t.Fatalf("Wait for %q = %v, %v, want success %v", script, state, err, success) + } +} + +func TestForkExecInvalidRemapSource(t *testing.T) { + f, err := Open(DevNull) + if err != nil { + t.Fatal(err) + } + defer f.Close() + files := make([]*File, int(f.Fd())+2) + files[len(files)-1] = f + files[0] = NewFile(1<<31, "invalid") + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 0"}, &ProcAttr{Files: files}) + if proc != nil { + proc.Kill() + proc.Wait() + t.Fatal("StartProcess accepted an invalid descriptor") + } + if !errors.Is(err, syscall.EBADF) { + t.Fatalf("StartProcess = %v, want EBADF", err) + } + if _, err := f.Stat(); err != nil { + t.Fatalf("parent source stat = %v", err) + } +} diff --git a/src/os/exec_spawn_test.go b/src/os/exec_spawn_test.go new file mode 100644 index 0000000000..10e8efd993 --- /dev/null +++ b/src/os/exec_spawn_test.go @@ -0,0 +1,322 @@ +//go:build (linux || darwin) && !baremetal && !tinygo.wasm + +package os_test + +import ( + "errors" + . "os" + "strconv" + "strings" + "syscall" + "testing" +) + +// StartProcess spawns a new process and Wait reports its exit status. An +// empty ProcAttr.Files gives the child no standard descriptors, so the command +// here does not use any. +func TestForkExec(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 0"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + if proc == nil { + t.Fatalf("proc is nil") + } + + if proc.Pid == 0 { + t.Fatalf("StartProcess failed: new process has pid 0") + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if !state.Exited() { + t.Errorf("wanted the process to have exited, got %v", state) + } + if !state.Success() { + t.Errorf("wanted a successful exit, got %v", state) + } + if state.ExitCode() != 0 { + t.Errorf("wanted exit code 0, got %d", state.ExitCode()) + } + if _, ok := state.Sys().(syscall.WaitStatus); !ok { + t.Errorf("wanted Sys() to be a syscall.WaitStatus, got %T", state.Sys()) + } +} + +// A process that exits non-zero must report that status rather than an error. +func TestForkExecExitStatus(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 3"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Success() { + t.Errorf("wanted an unsuccessful exit, got %v", state) + } + if state.ExitCode() != 3 { + t.Errorf("wanted exit code 3, got %d", state.ExitCode()) + } + if state.String() != "exit status 3" { + t.Errorf("wanted %q, got %q", "exit status 3", state.String()) + } +} + +// Killing a process must be reported as a signalled, not an exited, status. +func TestForkExecKill(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "sleep 30"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + if err := proc.Kill(); err != nil { + t.Fatalf("Kill failed: %v", err) + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Exited() { + t.Errorf("wanted the process to have been signalled, got %v", state) + } + + // After the reap, a second signal must report that the process is done and + // must not reach an unrelated process with the same pid. + if err := proc.Kill(); !errors.Is(err, ErrProcessDone) { + t.Errorf("wanted ErrProcessDone, got %v", err) + } +} + +func TestForkExecErrNotExist(t *testing.T) { + proc, err := StartProcess("invalid", []string{"invalid"}, &ProcAttr{}) + if !errors.Is(err, ErrNotExist) { + t.Fatalf("wanted ErrNotExist, got %s\n", err) + } + + if proc != nil { + t.Fatalf("wanted nil, got %v\n", proc) + } +} + +// Dir is honoured through a chdir file action. +func TestForkExecProcDir(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "test \"$(pwd -P)\" = /"}, &ProcAttr{Dir: "/"}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if !state.Success() { + t.Errorf("the child did not start in /, got %v", state) + } +} + +// A SysProcAttr with only zero fields asks for nothing, so it is accepted. +func TestForkExecProcSysEmpty(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 0"}, &ProcAttr{Sys: &syscall.SysProcAttr{}}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if !state.Success() { + t.Errorf("wanted a successful exit, got %v", state) + } +} + +// Every field that posix_spawn cannot express is refused by name, and the +// error unwraps to ErrNotImplementedSys. +func TestForkExecProcSysUnsupported(t *testing.T) { + for _, test := range []struct { + field string + sys *syscall.SysProcAttr + }{ + {"Chroot", &syscall.SysProcAttr{Chroot: "/"}}, + {"Ptrace", &syscall.SysProcAttr{Ptrace: true}}, + {"Setsid", &syscall.SysProcAttr{Setsid: true}}, + {"Setctty", &syscall.SysProcAttr{Setctty: true}}, + {"Noctty", &syscall.SysProcAttr{Noctty: true}}, + {"Ctty", &syscall.SysProcAttr{Ctty: 1}}, + {"Foreground", &syscall.SysProcAttr{Foreground: true}}, + } { + proc, err := StartProcess("/bin/echo", []string{"echo", "hello"}, &ProcAttr{Sys: test.sys}) + if !errors.Is(err, ErrNotImplementedSys) { + t.Errorf("%s: wanted an error wrapping ErrNotImplementedSys, got %v", test.field, err) + } + if err != nil && !strings.Contains(err.Error(), test.field) { + t.Errorf("%s: wanted the error to name the field, got %q", test.field, err.Error()) + } + if proc != nil { + t.Errorf("%s: wanted nil, got %v", test.field, proc) + } + } +} + +// startSleeper spawns a process that stays alive long enough for a check. +func startSleeper(t *testing.T, sys *syscall.SysProcAttr) *Process { + t.Helper() + proc, err := StartProcess("/bin/sleep", []string{"sleep", "30"}, &ProcAttr{Sys: sys}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + return proc +} + +// Setpgid with a Pgid of zero puts the child in a new process group whose id +// is the pid of the child. +func TestForkExecSetpgid(t *testing.T) { + proc := startSleeper(t, &syscall.SysProcAttr{Setpgid: true}) + defer func() { + proc.Kill() + proc.Wait() + }() + + pgid, err := syscall.Getpgid(proc.Pid) + if err != nil { + t.Fatalf("Getpgid(%d) failed: %v", proc.Pid, err) + } + if pgid != proc.Pid { + t.Errorf("wanted the child to lead its own group %d, got group %d", proc.Pid, pgid) + } + if pgid == Getpid() { + t.Errorf("the child stayed in the parent's group %d", pgid) + } +} + +// A non-zero Pgid joins an existing group instead of creating one. +func TestForkExecSetpgidJoin(t *testing.T) { + leader := startSleeper(t, &syscall.SysProcAttr{Setpgid: true}) + defer func() { + leader.Kill() + leader.Wait() + }() + + joiner := startSleeper(t, &syscall.SysProcAttr{Setpgid: true, Pgid: leader.Pid}) + defer func() { + joiner.Kill() + joiner.Wait() + }() + + pgid, err := syscall.Getpgid(joiner.Pid) + if err != nil { + t.Fatalf("Getpgid(%d) failed: %v", joiner.Pid, err) + } + if pgid != leader.Pid { + t.Errorf("wanted the second child in group %d, got group %d", leader.Pid, pgid) + } +} + +// Without Setpgid the child stays in the group that it inherited. +func TestForkExecInheritsProcessGroup(t *testing.T) { + proc := startSleeper(t, nil) + defer func() { + proc.Kill() + proc.Wait() + }() + + pgid, err := syscall.Getpgid(proc.Pid) + if err != nil { + t.Fatalf("Getpgid(%d) failed: %v", proc.Pid, err) + } + parent, err := syscall.Getpgid(Getpid()) + if err != nil { + t.Fatalf("Getpgid(self) failed: %v", err) + } + if pgid != parent { + t.Errorf("wanted the child in the parent's group %d, got group %d", parent, pgid) + } +} + +// A descriptor that the parent did not give to the child must not survive the +// exec. A child that holds a copy of the write end of a pipe keeps that pipe +// from a report of EOF. +func TestForkExecDescriptorsDoNotLeak(t *testing.T) { + r, w, err := Pipe() + if err != nil { + t.Fatalf("Pipe failed: %v", err) + } + defer r.Close() + defer w.Close() + + // The child gets no descriptors, so a successful write to the write end + // shows that the descriptor leaked. + script := "echo leaked >&" + strconv.Itoa(int(w.Fd())) + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", script}, &ProcAttr{ + Files: []*File{nil, nil, nil}, + }) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Success() { + t.Errorf("the child inherited the parent's pipe write end (fd %d)", w.Fd()) + } +} + +// A standard descriptor that ProcAttr.Files does not name is closed in the +// child, which is what syscall.forkAndExecInChild does in the standard +// library. +func TestForkExecClosesUnnamedStdio(t *testing.T) { + // A dup of a closed descriptor is a redirection error in the special + // built-in exec, which makes a non-interactive shell exit. See POSIX + // 2.8.1, Consequences of Shell Errors. + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exec 3>&1"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Success() { + t.Errorf("the child still had a standard output, got %v", state) + } +} + +// Files are handed to the child as its descriptors 0, 1 and 2. +func TestForkExecProcFiles(t *testing.T) { + r, w, err := Pipe() + if err != nil { + t.Fatalf("Pipe failed: %v", err) + } + defer r.Close() + + proc, err := StartProcess("/bin/echo", []string{"echo", "piped"}, &ProcAttr{ + Files: []*File{nil, w, nil}, + }) + if err != nil { + w.Close() + t.Fatalf("StartProcess failed: %v", err) + } + // Close the copy of the write end in the parent, or the read below does + // not see the end of the file. + w.Close() + + buf := make([]byte, 32) + n, err := r.Read(buf) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + if got := string(buf[:n]); got != "piped\n" { + t.Errorf("wanted %q, got %q", "piped\n", got) + } + + if _, err := proc.Wait(); err != nil { + t.Fatalf("Wait failed: %v", err) + } +} diff --git a/src/os/file_darwin.go b/src/os/file_darwin.go index 8d96b7296e..aa0af92753 100644 --- a/src/os/file_darwin.go +++ b/src/os/file_darwin.go @@ -3,5 +3,14 @@ package os import "syscall" func pipe(p []int) error { - return syscall.Pipe(p) + // Darwin has no pipe2, so mark the descriptors close-on-exec afterwards. + // ForkLock keeps a spawn out of the window between the two steps. + syscall.ForkLock.RLock() + defer syscall.ForkLock.RUnlock() + if err := syscall.Pipe(p); err != nil { + return err + } + syscall.CloseOnExec(p[0]) + syscall.CloseOnExec(p[1]) + return nil } diff --git a/src/os/osexec.go b/src/os/osexec.go deleted file mode 100644 index 6b2562a685..0000000000 --- a/src/os/osexec.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build linux && !baremetal && !tinygo.wasm && !nintendoswitch - -package os - -import ( - "syscall" - "unsafe" -) - -func fork() (pid int32, err error) { - pid = libc_fork() - if pid != 0 { - if errno := *libc_errno(); errno != 0 { - err = syscall.Errno(*libc_errno()) - } - } - return -} - -// the golang standard library does not expose interfaces for execve and fork, so we define them here the same way via the libc wrapper -func execve(pathname string, argv []string, envv []string) error { - argv0 := cstring(pathname) - - // transform argv and envv into the format expected by execve - argv1 := make([]*byte, len(argv)+1) - for i, arg := range argv { - argv1[i] = &cstring(arg)[0] - } - argv1[len(argv)] = nil - - env1 := make([]*byte, len(envv)+1) - for i, env := range envv { - env1[i] = &cstring(env)[0] - } - env1[len(envv)] = nil - - ret, _, err := syscall.Syscall(syscall.SYS_EXECVE, uintptr(unsafe.Pointer(unsafe.SliceData(argv0))), uintptr(unsafe.Pointer(unsafe.SliceData(argv1))), uintptr(unsafe.Pointer(unsafe.SliceData(env1)))) - if int(ret) != 0 { - return err - } - - return nil -} - -func cstring(s string) []byte { - data := make([]byte, len(s)+1) - copy(data, s) - // final byte should be zero from the initial allocation - return data -} - -//export fork -func libc_fork() int32 - -// Internal musl function to get the C errno pointer. -// -//export __errno_location -func libc_errno() *int32 diff --git a/testdata/os-exec-waitdelay/README.md b/testdata/os-exec-waitdelay/README.md new file mode 100644 index 0000000000..f1c7cecb36 --- /dev/null +++ b/testdata/os-exec-waitdelay/README.md @@ -0,0 +1,45 @@ +# WaitDelay with a background child + +This manual test uses the Go standard library `os/exec` with TinyGo's `os`. +The shell exits and a background `sleep` keeps the output pipe open for two +seconds. `WaitDelay` is 100 ms. The test returns status 1 if the wait takes +one second or more, or if the error is not `exec.ErrWaitDelay`. + +Build and run from the repository root with the process changes in TINYGOROOT. + +```sh +tinygo build -p 1 -o /tmp/waitdelay ./testdata/os-exec-waitdelay/main.go +timeout 10 /tmp/waitdelay +timeout 10 /tmp/waitdelay pipe +go build -p 1 -o /tmp/waitdelay-go ./testdata/os-exec-waitdelay/main.go +timeout 10 /tmp/waitdelay-go +timeout 10 /tmp/waitdelay-go pipe +``` + +`timeout` is an external limit for Linux. The background child exits after +two seconds. The `pipe` mode closes a reader during a read. It closes the +writer two seconds later so that the test can finish if the read stays blocked. + +Measured on Linux arm64 with the released TinyGo 0.42.0 compiler, Go 1.27.0, +and a TINYGOROOT copy with PR #5634 and the fd remapping fix. + +| Test | TinyGo | Go | +| --- | --- | --- | +| WaitDelay 100 ms | 2.004 s, ErrWaitDelay | 101 ms, ErrWaitDelay | +| Read after reader close | 2.003 s, EOF | 57 us, file already closed | + +`src/os/file_anyos.go` calls `syscall.Read` and `syscall.Close` directly. +Closing the reader does not stop the active blocking read in this test. +Go's `Cmd.awaitGoroutines` closes the pipes when the timer expires, then waits +for the copy goroutines. That wait lasts until the background child closes +the pipe. The timer fires, but it cannot enforce the limit. + +This remains open. A fix needs interruptible pipe I/O and coordination between +close and active I/O. It must also prevent an old operation from using a reused +descriptor. Changes to spawn file actions or Darwin fcntl do not fix this Linux +failure. PR #5630 addresses lock contention, not this blocked read. + +The follow-up must test WaitDelay after normal exit and context cancellation, +blocked pipe reads and writes, prompt close, and descriptor reuse on hosted +Linux and Darwin. A process that keeps its inherited output open must not +keep `Cmd.Wait` blocked after the configured limit. diff --git a/testdata/os-exec-waitdelay/main.go b/testdata/os-exec-waitdelay/main.go new file mode 100644 index 0000000000..9e5d81bc4b --- /dev/null +++ b/testdata/os-exec-waitdelay/main.go @@ -0,0 +1,48 @@ +//go:build linux || darwin + +package main + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "time" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "pipe" { + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + done := make(chan error, 1) + go func() { + var buf [1]byte + _, err := r.Read(buf[:]) + done <- err + }() + time.Sleep(100 * time.Millisecond) + go func() { + time.Sleep(2 * time.Second) + w.Close() + }() + start := time.Now() + closeErr := r.Close() + err = <-done + fmt.Printf("pipe close=%v read=%v elapsed=%v\n", closeErr, err, time.Since(start)) + return + } + cmd := exec.Command("/bin/sh", "-c", "sleep 2 &") + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + cmd.WaitDelay = 100 * time.Millisecond + start := time.Now() + err := cmd.Run() + elapsed := time.Since(start) + fmt.Printf("WaitDelay=%v elapsed=%v error=%v\n", cmd.WaitDelay, elapsed, err) + if !errors.Is(err, exec.ErrWaitDelay) || elapsed >= time.Second { + os.Exit(1) + } +}