diff options
author | leongross <[email protected]> | 2024-11-07 09:45:47 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2024-11-07 09:45:47 +0100 |
commit | f9f439ad49ef8a21e373568277aff8e448806cf2 (patch) | |
tree | e4798fe9926cd54acabc79ff15f030f7fd79361e /src | |
parent | c02a8141c7974e3e3a3deebd38c9ff64c6511c33 (diff) | |
download | tinygo-f9f439ad49ef8a21e373568277aff8e448806cf2.tar.gz tinygo-f9f439ad49ef8a21e373568277aff8e448806cf2.zip |
os: implement StartProcess
Signed-off-by: leongross <[email protected]>
Diffstat (limited to 'src')
-rw-r--r-- | src/os/exec.go | 14 | ||||
-rw-r--r-- | src/os/exec_linux.go | 103 | ||||
-rw-r--r-- | src/os/exec_linux_test.go | 78 | ||||
-rw-r--r-- | src/os/exec_other.go | 27 | ||||
-rw-r--r-- | src/os/exec_posix.go | 35 | ||||
-rw-r--r-- | src/os/osexec.go | 58 | ||||
-rw-r--r-- | src/syscall/syscall_libc.go | 5 | ||||
-rw-r--r-- | src/syscall/syscall_unix.go | 2 |
8 files changed, 286 insertions, 36 deletions
diff --git a/src/os/exec.go b/src/os/exec.go index 1ea9dcbd8..28406f916 100644 --- a/src/os/exec.go +++ b/src/os/exec.go @@ -5,6 +5,12 @@ import ( "syscall" ) +var ( + ErrNotImplementedDir = errors.New("directory setting not implemented") + ErrNotImplementedSys = errors.New("sys setting not implemented") + ErrNotImplementedFiles = errors.New("files setting not implemented") +) + type Signal interface { String() string Signal() // to distinguish from other Stringers @@ -47,6 +53,10 @@ 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 { @@ -57,8 +67,10 @@ type Process struct { Pid int } +// StartProcess starts a new process with the program, arguments and attributes specified by name, argv and attr. +// Arguments to the process (os.Args) are passed via argv. func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) { - return nil, &PathError{Op: "fork/exec", Path: name, Err: ErrNotImplemented} + return startProcess(name, argv, attr) } func (p *Process) Wait() (*ProcessState, error) { diff --git a/src/os/exec_linux.go b/src/os/exec_linux.go new file mode 100644 index 000000000..58ee79cc8 --- /dev/null +++ b/src/os/exec_linux.go @@ -0,0 +1,103 @@ +// 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 + +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 new file mode 100644 index 000000000..34f1fef98 --- /dev/null +++ b/src/os/exec_linux_test.go @@ -0,0 +1,78 @@ +//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 new file mode 100644 index 000000000..5494f0896 --- /dev/null +++ b/src/os/exec_other.go @@ -0,0 +1,27 @@ +//go:build (!aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm + +package os + +import "syscall" + +var ( + Interrupt Signal = syscall.SIGINT + Kill Signal = syscall.SIGKILL +) + +func findProcess(pid int) (*Process, error) { + return &Process{Pid: pid}, nil +} + +func (p *Process) release() error { + p.Pid = -1 + return nil +} + +func forkExec(_ string, _ []string, _ *ProcAttr) (pid int, err error) { + return 0, ErrNotImplemented +} + +func startProcess(_ string, _ []string, _ *ProcAttr) (proc *Process, err error) { + return &Process{Pid: 0}, ErrNotImplemented +} diff --git a/src/os/exec_posix.go b/src/os/exec_posix.go deleted file mode 100644 index 720368572..000000000 --- a/src/os/exec_posix.go +++ /dev/null @@ -1,35 +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 aix || darwin || dragonfly || freebsd || (js && wasm) || linux || netbsd || openbsd || solaris || wasip1 || wasip2 || windows - -package os - -import ( - "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 -} diff --git a/src/os/osexec.go b/src/os/osexec.go new file mode 100644 index 000000000..65e3ab695 --- /dev/null +++ b/src/os/osexec.go @@ -0,0 +1,58 @@ +//go:build linux && !baremetal && !tinygo.wasm + +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(&argv0[0])), uintptr(unsafe.Pointer(&argv1[0])), uintptr(unsafe.Pointer(&env1[0]))) + 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/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index 67cf6681f..8b032d383 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -233,6 +233,11 @@ func (w WaitStatus) Continued() bool { return false } func (w WaitStatus) StopSignal() Signal { return 0 } func (w WaitStatus) TrapCause() int { return 0 } +// since rusage is quite a big struct and we stub it out anyway no need to define it here +func Wait4(pid int, wstatus *WaitStatus, options int, rusage uintptr) (wpid int, err error) { + return 0, ENOSYS // TODO +} + func Getenv(key string) (value string, found bool) { data := cstring(key) raw := libc_getenv(&data[0]) diff --git a/src/syscall/syscall_unix.go b/src/syscall/syscall_unix.go index 23d81fb89..b5b8f4eb7 100644 --- a/src/syscall/syscall_unix.go +++ b/src/syscall/syscall_unix.go @@ -1,3 +1,5 @@ +//go:build linux || unix + package syscall func Exec(argv0 string, argv []string, envv []string) (err error) |