diff --git a/src/os/exec_linux.go b/src/os/exec_linux.go index 6914a2c285..8c1bcb8f45 100644 --- a/src/os/exec_linux.go +++ b/src/os/exec_linux.go @@ -35,15 +35,7 @@ func (p *Process) release() error { 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) +// forkExec starts argv0 with posix_spawn. func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) { if argv == nil { return 0, errors.New("exec: no argv") @@ -53,45 +45,25 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) return 0, errors.New("exec: no argv") } + return spawn(argv0, argv, attr.Env) +} + +// startProcess does not support ProcAttr.Dir, Files or Sys yet. +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { if attr == nil { - attr = new(ProcAttr) + return nil, errors.New("exec: no ProcAttr") } - p, err := fork() - pid = int(p) - - if err != nil { - return 0, err + if attr.Dir != "" { + return nil, ErrNotImplementedDir } - // 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 + if attr.Sys != nil { + return nil, ErrNotImplementedSys } - // 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 - } + if len(attr.Files) != 0 { + return nil, ErrNotImplementedFiles } pid, err := forkExec(name, argv, attr) diff --git a/src/os/exec_linux_test.go b/src/os/exec_linux_test.go index 34f1fef983..8be11621c8 100644 --- a/src/os/exec_linux_test.go +++ b/src/os/exec_linux_test.go @@ -5,6 +5,7 @@ package os_test import ( "errors" . "os" + "path/filepath" "runtime" "syscall" "testing" @@ -67,7 +68,7 @@ func TestForkExecProcSys(t *testing.T) { } func TestForkExecProcFiles(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Files: []*File{}}) + proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Files: []*File{Stdin, Stdout, Stderr}}) if !errors.Is(err, ErrNotImplementedFiles) { t.Fatalf("wanted ErrNotImplementedFiles, got %v\n", err) } @@ -76,3 +77,37 @@ func TestForkExecProcFiles(t *testing.T) { t.Fatalf("wanted nil, got %v\n", proc) } } + +func TestForkExecNilAttr(t *testing.T) { + proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, nil) + if err == nil { + t.Fatal("wanted an error for a nil ProcAttr") + } + + if proc != nil { + t.Fatalf("wanted nil, got %v\n", proc) + } +} + +// The caller keeps running and the command runs once. +func TestForkExecParentContinues(t *testing.T) { + out := filepath.Join(t.TempDir(), "out") + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "echo child >> " + out}, &ProcAttr{}) + if err != nil { + t.Fatal(err) + } + var ws syscall.WaitStatus + if _, err := syscall.Wait4(proc.Pid, &ws, 0, nil); err != nil { + t.Fatal(err) + } + if !ws.Exited() || ws.ExitStatus() != 0 { + t.Fatalf("child did not exit cleanly: %v", ws) + } + data, err := ReadFile(out) + if err != nil { + t.Fatal(err) + } + if string(data) != "child\n" { + t.Fatalf("child output = %q, want %q", data, "child\n") + } +} diff --git a/src/os/osexec.go b/src/os/osexec.go index 6b2562a685..58e9ef8d87 100644 --- a/src/os/osexec.go +++ b/src/os/osexec.go @@ -7,39 +7,16 @@ import ( "unsafe" ) -func fork() (pid int32, err error) { - pid = libc_fork() - if pid != 0 { - if errno := *libc_errno(); errno != 0 { - err = syscall.Errno(*libc_errno()) - } +// spawn starts path with argv and envv and returns the pid of the new process. +func spawn(path string, argv, envv []string) (int, error) { + pathp := cstring(path) + argvp := cstrings(argv) + envp := cstrings(envv) + var pid int32 + if errno := libc_posix_spawn(&pid, &pathp[0], nil, nil, &argvp[0], &envp[0]); errno != 0 { + return 0, syscall.Errno(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 + return int(pid), nil } func cstring(s string) []byte { @@ -49,10 +26,17 @@ func cstring(s string) []byte { return data } -//export fork -func libc_fork() int32 +// cstrings returns ss as a nil terminated array of C strings. +func cstrings(ss []string) []*byte { + p := make([]*byte, len(ss)+1) + for i, s := range ss { + p[i] = &cstring(s)[0] + } + return p +} -// Internal musl function to get the C errno pointer. +// posix_spawn returns an error number instead of setting errno, see +// https://pubs.opengroup.org/onlinepubs/9799919799/functions/posix_spawn.html // -//export __errno_location -func libc_errno() *int32 +//export posix_spawn +func libc_posix_spawn(pid *int32, path *byte, fileActions, attrp unsafe.Pointer, argv, envp **byte) int32