Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os/StartProcess #4377

Open
wants to merge 24 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4cecb75
add StartProcess functionality (fix)
leongross Jul 31, 2024
53bedeb
renaming
leongross Aug 2, 2024
8fd1ba4
os rework
leongross Aug 2, 2024
1faa457
adjust build tags, un-export fork and execve, add signals for windows…
leongross Aug 5, 2024
8494457
update wrapper comment
leongross Aug 6, 2024
c9703df
exclude baremetal targets
leongross Aug 8, 2024
b33e04f
test: remove os.Getpid in seed generation on baremetal devices
leongross Aug 8, 2024
44bc700
remove os/signal from baremetal
leongross Aug 8, 2024
c037cea
adjust baremetal build tags
leongross Aug 9, 2024
e30e603
remove old comments
leongross Sep 9, 2024
f975bbf
os/StartProcess: cleanup
leongross Sep 11, 2024
7f7559c
os/exec: exclude aarch64
leongross Oct 4, 2024
fb71295
add fallback stub for aarch64
leongross Oct 8, 2024
6c6f254
add syscall error handling
leongross Oct 8, 2024
fdf44d8
change build tags aarch64 -> arm64
leongross Oct 8, 2024
be48fa5
add signal implementations for linux arm64
leongross Oct 8, 2024
9b8dd29
wip
leongross Oct 8, 2024
6d74b9d
update TestForkExec with invalid testcase
leongross Oct 27, 2024
61267ad
remove comments, remove unused/obsolete files
leongross Oct 27, 2024
70bfa96
replace raw syscall implementation with musl wrapper
leongross Oct 28, 2024
5878a4b
remove arm64 handling
leongross Oct 29, 2024
706477a
update musl builder to include arch specific process files
leongross Oct 30, 2024
664c581
add musl sources to release files
leongross Nov 1, 2024
94e8652
remove redundant else
leongross Nov 3, 2024
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
1 change: 1 addition & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions builder/musl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
14 changes: 13 additions & 1 deletion src/os/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,6 +53,10 @@ func (p *ProcessState) Sys() interface{} {
return nil // TODO
}

func (p *ProcessState) Exited() bool {
return false // TODO
}
leongross marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
Expand All @@ -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) {
Expand Down
103 changes: 103 additions & 0 deletions src/os/exec_linux.go
Original file line number Diff line number Diff line change
@@ -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)
leongross marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: with this check, the previous check is unnecessary. len(argv) of a nil slice is always 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)
}
78 changes: 78 additions & 0 deletions src/os/exec_linux_test.go
Original file line number Diff line number Diff line change
@@ -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{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a negative test, to make sure that running /bin/non-existing-program for example returns the expected error message?

Can you also add some tests that verify that unsupported features like ProcAttr.Dir, ProcAttr.Sys, etc will return an error as they should? (These checks probably also need to be implemented). If a program sets Dir for example but then the program executes in another directory without error, that's going to be weird and difficult to debug. I'd rather not have users deal with such bugs.

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)
}
}
27 changes: 27 additions & 0 deletions src/os/exec_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build (!aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm

package os
leongross marked this conversation as resolved.
Show resolved Hide resolved

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
}
35 changes: 0 additions & 35 deletions src/os/exec_posix.go

This file was deleted.

58 changes: 58 additions & 0 deletions src/os/osexec.go
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions src/syscall/syscall_libc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 2 additions & 0 deletions src/syscall/syscall_unix.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux || unix
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//go:build linux || unix
//go:build unix


package syscall

func Exec(argv0 string, argv []string, envv []string) (err error)
Expand Down
Loading