diff --git a/GNUmakefile b/GNUmakefile index 94d9c357d2..22e31a17bd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -335,6 +335,7 @@ TEST_PACKAGES_FAST = \ net/http/internal/ascii \ net/mail \ os \ + os/signal \ path \ reflect \ sync \ diff --git a/src/os/exec.go b/src/os/exec.go index 1ea9dcbd8c..c348eebeba 100644 --- a/src/os/exec.go +++ b/src/os/exec.go @@ -47,6 +47,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 { @@ -58,7 +62,7 @@ type Process struct { } 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 0000000000..d8a7fe168e --- /dev/null +++ b/src/os/exec_linux.go @@ -0,0 +1,110 @@ +// 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 posix || aix || linux || dragonfly || freebsd || (js && wasm) || netbsd || openbsd || solaris || wasip1 +// +build posix aix linux dragonfly freebsd js,wasm netbsd openbsd solaris wasip1 + +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) { + var ( + ret uintptr + ) + + 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 + } + envp, err := syscall.SlicePtrFromStrings(attr.Env) + if err != nil { + return 0, err + } + + if (runtime.GOOS == "freebsd" || runtime.GOOS == "dragonfly") && len(argv) > 0 && len(argv[0]) > len(argv0) { + argvp[0] = argv0p + } + + ret = syscall.Fork() + if int(ret) < 0 { + return 0, errors.New("fork failed") + } + + if int(ret) != 0 { + // if fd == 0 code runs in parent + return int(ret), nil + } else { + // else code runs in child, which then should exec the new process + ret = syscall.Execve(argv0, argv, envp) + if int(ret) != 0 { + // exec failed + return int(ret), errors.New("exec failed") + } + // 3. TODO: use pipes to communicate back child status + return int(ret), 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) { + 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 0000000000..655eab427e --- /dev/null +++ b/src/os/exec_linux_test.go @@ -0,0 +1,29 @@ +package os_test + +import ( + . "os" + "runtime" + "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("echo", []string{"echo", "hello", "world"}, &ProcAttr{}) + if err != nil { + t.Fatalf("forkExec failed: %v", err) + return + } + + if proc.Pid == 0 { + t.Fatalf("forkExec failed: new process has pid 0") + } + + t.Logf("forkExec succeeded: new process has pid %d", proc) +} diff --git a/src/os/exec_other.go b/src/os/exec_other.go new file mode 100644 index 0000000000..5e16773a3c --- /dev/null +++ b/src/os/exec_other.go @@ -0,0 +1,21 @@ +//go:build !aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris +// +build !aix,!android,!freebsd,!linux,!netbsd,!openbsd,!plan9,!solaris + +package os + +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 720368572b..0000000000 --- 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/signal/signal_linux.go b/src/os/signal/signal_linux.go new file mode 100644 index 0000000000..391fb5c7d9 --- /dev/null +++ b/src/os/signal/signal_linux.go @@ -0,0 +1,54 @@ +// Copyright 2012 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 !plan9 && !windows && !js && !wasm && !wasip1 && !wasip2 +// +build !plan9,!windows,!js,!wasm,!wasip1,!wasip2 + +package signal + +import ( + "os" + "syscall" +) + +const ( + numSig = 65 // max across all systems +) + +type Signal interface { + String() string + Signal() // to distinguish from other Stringers +} + +// Defined by the runtime package and used by syscall/sigqueue.go. +func signal_disable(uint32) +func signal_enable(uint32) +func signal_ignore(uint32) +func signal_ignored(uint32) bool +func signal_recv() uint32 + +func signum(sig os.Signal) int { + switch sig := sig.(type) { + case syscall.Signal: + i := int(sig) + if i < 0 || i >= numSig { + return -1 + } + return i + default: + return -1 + } +} + +func enableSignal(sig int) { + signal_enable(uint32(sig)) +} + +func disableSignal(sig int) { + signal_disable(uint32(sig)) +} + +func ignoreSignal(sig int) { + signal_ignore(uint32(sig)) +} diff --git a/src/os/signal/signal_linux_test.go b/src/os/signal/signal_linux_test.go new file mode 100644 index 0000000000..9c58cafd35 --- /dev/null +++ b/src/os/signal/signal_linux_test.go @@ -0,0 +1,30 @@ +//go:build !plan9 && !windows && !js && !wasm && !wasip1 && !wasip2 +// +build !plan9,!windows,!js,!wasm,!wasip1,!wasip2 + +package signal + +import ( + "runtime" + "testing" +) + +// The file sigqueue.go contains preliminary stubs for signal handling. +// Since tinygo ultimately lacks support for signals, these stubs are +// placeholders for future work. +// There might be userland applications that rely on these functions +// from the upstream go package os/signal that we want to enable +// building and linking. + +func TestSignalHandling(t *testing.T) { + if runtime.GOOS == "wasip1" || runtime.GOOS == "wasip2" { + t.Skip() + } + + // This test is a placeholder for future work. + // It is here to ensure that the stubs in sigqueue.go + // are correctly linked and can be called from userland + // applications. + enableSignal(0) + disableSignal(0) + ignoreSignal(0) +} diff --git a/src/runtime/sigqueue.go b/src/runtime/sigqueue.go new file mode 100644 index 0000000000..ff789a605f --- /dev/null +++ b/src/runtime/sigqueue.go @@ -0,0 +1,26 @@ +// 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 !plan9 +// +build !plan9 + +package runtime + +// Stub definitions copied from upstream golang +// TODO: implement a minimal functional version of these functions + +// go: linkname os/signal.signal_disable signal_disable +func signal_disable(_ uint32) {} + +// go: linkname os/signal.signal_enable signal_enable +func signal_enable(_ uint32) {} + +// go: linkname os/signal.signal_ignore signal_ignore +func signal_ignore(_ uint32) {} + +// go: linkname os/signal.signal_ignored signal_ignored +func signal_ignored(_ uint32) bool { return true } + +// go: linkname os/signal.signal_recv signal_recv +func signal_recv() uint32 { return 0 } diff --git a/src/syscall/exec_unix.go b/src/syscall/exec_unix.go new file mode 100644 index 0000000000..2d92d5061e --- /dev/null +++ b/src/syscall/exec_unix.go @@ -0,0 +1,7 @@ +//go:build unix + +package syscall + +// Implemented in runtime package. +func runtime_BeforeExec() +func runtime_AfterExec() diff --git a/src/syscall/syscall_unix.go b/src/syscall/syscall_unix.go index 23d81fb891..e16e7ea387 100644 --- a/src/syscall/syscall_unix.go +++ b/src/syscall/syscall_unix.go @@ -1,5 +1,10 @@ +//go:build linux || unix +// +build linux unix + package syscall +import "errors" + func Exec(argv0 string, argv []string, envv []string) (err error) // The two SockaddrInet* structs have been copied from the Go source tree. @@ -16,3 +21,54 @@ type SockaddrInet6 struct { Addr [16]byte raw RawSockaddrInet6 } + +func Fork() (err error) { + fail := int(libc_fork()) + if fail < 0 { + // TODO: parse the syscall return codes + return errors.New("fork failed") + } + 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) (err 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 + + fail := int(libc_execve(&argv0[0], &argv1[0], &env1[0])) + if fail < 0 { + // TODO: parse the syscall return codes + return errors.New("fork failed") + } + return +} + +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 +} + +// pid_t fork(void); +// +//export fork +func libc_fork() int32 + +// int execve(const char *filename, char *const argv[], char *const envp[]); +// +//export execve +func libc_execve(filename *byte, argv **byte, envp **byte) int