Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 13 additions & 41 deletions src/os/exec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down
37 changes: 36 additions & 1 deletion src/os/exec_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package os_test
import (
"errors"
. "os"
"path/filepath"
"runtime"
"syscall"
"testing"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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")
}
}
58 changes: 21 additions & 37 deletions src/os/osexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Loading