aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorleongross <[email protected]>2024-11-07 09:45:47 +0100
committerGitHub <[email protected]>2024-11-07 09:45:47 +0100
commitf9f439ad49ef8a21e373568277aff8e448806cf2 (patch)
treee4798fe9926cd54acabc79ff15f030f7fd79361e
parentc02a8141c7974e3e3a3deebd38c9ff64c6511c33 (diff)
downloadtinygo-f9f439ad49ef8a21e373568277aff8e448806cf2.tar.gz
tinygo-f9f439ad49ef8a21e373568277aff8e448806cf2.zip
os: implement StartProcess
Signed-off-by: leongross <[email protected]>
-rw-r--r--GNUmakefile1
-rw-r--r--builder/musl.go8
-rw-r--r--src/os/exec.go14
-rw-r--r--src/os/exec_linux.go103
-rw-r--r--src/os/exec_linux_test.go78
-rw-r--r--src/os/exec_other.go27
-rw-r--r--src/os/exec_posix.go35
-rw-r--r--src/os/osexec.go58
-rw-r--r--src/syscall/syscall_libc.go5
-rw-r--r--src/syscall/syscall_unix.go2
10 files changed, 295 insertions, 36 deletions
diff --git a/GNUmakefile b/GNUmakefile
index fd2d282c4..155f5c5d5 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -941,6 +941,7 @@ endif
@cp -rp lib/musl/src/thread build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/time build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/unistd build/release/tinygo/lib/musl/src
+ @cp -rp lib/musl/src/process build/release/tinygo/lib/musl/src
@cp -rp lib/mingw-w64/mingw-w64-crt/def-include build/release/tinygo/lib/mingw-w64/mingw-w64-crt
@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/api-ms-win-crt-* build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/kernel32.def.in build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
diff --git a/builder/musl.go b/builder/musl.go
index ecae118e4..a6699ad8d 100644
--- a/builder/musl.go
+++ b/builder/musl.go
@@ -136,12 +136,20 @@ var libMusl = Library{
"thread/*.c",
"time/*.c",
"unistd/*.c",
+ "process/*.c",
}
+
if arch == "arm" {
// These files need to be added to the start for some reason.
globs = append([]string{"thread/arm/*.c"}, globs...)
}
+ if arch != "aarch64" && arch != "mips" {
+ //aarch64 and mips have no architecture specific code, either they
+ // are not supported or don't need any?
+ globs = append([]string{"process/" + arch + "/*.s"}, globs...)
+ }
+
var sources []string
seenSources := map[string]struct{}{}
basepath := goenv.Get("TINYGOROOT") + "/lib/musl/src/"
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)