Skip to content

Commit f9f439a

Browse files
authored
os: implement StartProcess
Signed-off-by: leongross <[email protected]>
1 parent c02a814 commit f9f439a

10 files changed

+295
-36
lines changed

GNUmakefile

+1
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,7 @@ endif
941941
@cp -rp lib/musl/src/thread build/release/tinygo/lib/musl/src
942942
@cp -rp lib/musl/src/time build/release/tinygo/lib/musl/src
943943
@cp -rp lib/musl/src/unistd build/release/tinygo/lib/musl/src
944+
@cp -rp lib/musl/src/process build/release/tinygo/lib/musl/src
944945
@cp -rp lib/mingw-w64/mingw-w64-crt/def-include build/release/tinygo/lib/mingw-w64/mingw-w64-crt
945946
@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
946947
@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/kernel32.def.in build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common

builder/musl.go

+8
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,20 @@ var libMusl = Library{
136136
"thread/*.c",
137137
"time/*.c",
138138
"unistd/*.c",
139+
"process/*.c",
139140
}
141+
140142
if arch == "arm" {
141143
// These files need to be added to the start for some reason.
142144
globs = append([]string{"thread/arm/*.c"}, globs...)
143145
}
144146

147+
if arch != "aarch64" && arch != "mips" {
148+
//aarch64 and mips have no architecture specific code, either they
149+
// are not supported or don't need any?
150+
globs = append([]string{"process/" + arch + "/*.s"}, globs...)
151+
}
152+
145153
var sources []string
146154
seenSources := map[string]struct{}{}
147155
basepath := goenv.Get("TINYGOROOT") + "/lib/musl/src/"

src/os/exec.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import (
55
"syscall"
66
)
77

8+
var (
9+
ErrNotImplementedDir = errors.New("directory setting not implemented")
10+
ErrNotImplementedSys = errors.New("sys setting not implemented")
11+
ErrNotImplementedFiles = errors.New("files setting not implemented")
12+
)
13+
814
type Signal interface {
915
String() string
1016
Signal() // to distinguish from other Stringers
@@ -47,6 +53,10 @@ func (p *ProcessState) Sys() interface{} {
4753
return nil // TODO
4854
}
4955

56+
func (p *ProcessState) Exited() bool {
57+
return false // TODO
58+
}
59+
5060
// ExitCode returns the exit code of the exited process, or -1
5161
// if the process hasn't exited or was terminated by a signal.
5262
func (p *ProcessState) ExitCode() int {
@@ -57,8 +67,10 @@ type Process struct {
5767
Pid int
5868
}
5969

70+
// StartProcess starts a new process with the program, arguments and attributes specified by name, argv and attr.
71+
// Arguments to the process (os.Args) are passed via argv.
6072
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) {
61-
return nil, &PathError{Op: "fork/exec", Path: name, Err: ErrNotImplemented}
73+
return startProcess(name, argv, attr)
6274
}
6375

6476
func (p *Process) Wait() (*ProcessState, error) {

src/os/exec_linux.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2009 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux && !baremetal && !tinygo.wasm
6+
7+
package os
8+
9+
import (
10+
"errors"
11+
"runtime"
12+
"syscall"
13+
)
14+
15+
// The only signal values guaranteed to be present in the os package on all
16+
// systems are os.Interrupt (send the process an interrupt) and os.Kill (force
17+
// the process to exit). On Windows, sending os.Interrupt to a process with
18+
// os.Process.Signal is not implemented; it will return an error instead of
19+
// sending a signal.
20+
var (
21+
Interrupt Signal = syscall.SIGINT
22+
Kill Signal = syscall.SIGKILL
23+
)
24+
25+
// Keep compatible with golang and always succeed and return new proc with pid on Linux.
26+
func findProcess(pid int) (*Process, error) {
27+
return &Process{Pid: pid}, nil
28+
}
29+
30+
func (p *Process) release() error {
31+
// NOOP for unix.
32+
p.Pid = -1
33+
// no need for a finalizer anymore
34+
runtime.SetFinalizer(p, nil)
35+
return nil
36+
}
37+
38+
// This function is a wrapper around the forkExec function, which is a wrapper around the fork and execve system calls.
39+
// 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.
40+
// It thereby replaces the newly created process with the specified command and arguments.
41+
// Differences to upstream golang implementation (https://cs.opensource.google/go/go/+/master:src/syscall/exec_unix.go;l=143):
42+
// * No setting of Process Attributes
43+
// * Ignoring Ctty
44+
// * No ForkLocking (might be introduced by #4273)
45+
// * No parent-child communication via pipes (TODO)
46+
// * No waiting for crashes child processes to prohibit zombie process accumulation / Wait status checking (TODO)
47+
func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
48+
if argv == nil {
49+
return 0, errors.New("exec: no argv")
50+
}
51+
52+
if len(argv) == 0 {
53+
return 0, errors.New("exec: no argv")
54+
}
55+
56+
if attr == nil {
57+
attr = new(ProcAttr)
58+
}
59+
60+
p, err := fork()
61+
pid = int(p)
62+
63+
if err != nil {
64+
return 0, err
65+
}
66+
67+
// else code runs in child, which then should exec the new process
68+
err = execve(argv0, argv, attr.Env)
69+
if err != nil {
70+
// exec failed
71+
return 0, err
72+
}
73+
// 3. TODO: use pipes to communicate back child status
74+
return pid, nil
75+
}
76+
77+
// In Golang, the idiomatic way to create a new process is to use the StartProcess function.
78+
// Since the Model of operating system processes in tinygo differs from the one in Golang, we need to implement the StartProcess function differently.
79+
// The startProcess function is a wrapper around the forkExec function, which is a wrapper around the fork and execve system calls.
80+
// 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.
81+
// It thereby replaces the newly created process with the specified command and arguments.
82+
func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) {
83+
if attr != nil {
84+
if attr.Dir != "" {
85+
return nil, ErrNotImplementedDir
86+
}
87+
88+
if attr.Sys != nil {
89+
return nil, ErrNotImplementedSys
90+
}
91+
92+
if len(attr.Files) != 0 {
93+
return nil, ErrNotImplementedFiles
94+
}
95+
}
96+
97+
pid, err := forkExec(name, argv, attr)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
return findProcess(pid)
103+
}

src/os/exec_linux_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//go:build linux && !baremetal && !tinygo.wasm
2+
3+
package os_test
4+
5+
import (
6+
"errors"
7+
. "os"
8+
"runtime"
9+
"syscall"
10+
"testing"
11+
)
12+
13+
// Test the functionality of the forkExec function, which is used to fork and exec a new process.
14+
// This test is not run on Windows, as forkExec is not supported on Windows.
15+
// This test is not run on Plan 9, as forkExec is not supported on Plan 9.
16+
func TestForkExec(t *testing.T) {
17+
if runtime.GOOS != "linux" {
18+
t.Logf("skipping test on %s", runtime.GOOS)
19+
return
20+
}
21+
22+
proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{})
23+
if !errors.Is(err, nil) {
24+
t.Fatalf("forkExec failed: %v", err)
25+
}
26+
27+
if proc == nil {
28+
t.Fatalf("proc is nil")
29+
}
30+
31+
if proc.Pid == 0 {
32+
t.Fatalf("forkExec failed: new process has pid 0")
33+
}
34+
}
35+
36+
func TestForkExecErrNotExist(t *testing.T) {
37+
proc, err := StartProcess("invalid", []string{"invalid"}, &ProcAttr{})
38+
if !errors.Is(err, ErrNotExist) {
39+
t.Fatalf("wanted ErrNotExist, got %s\n", err)
40+
}
41+
42+
if proc != nil {
43+
t.Fatalf("wanted nil, got %v\n", proc)
44+
}
45+
}
46+
47+
func TestForkExecProcDir(t *testing.T) {
48+
proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Dir: "dir"})
49+
if !errors.Is(err, ErrNotImplementedDir) {
50+
t.Fatalf("wanted ErrNotImplementedDir, got %v\n", err)
51+
}
52+
53+
if proc != nil {
54+
t.Fatalf("wanted nil, got %v\n", proc)
55+
}
56+
}
57+
58+
func TestForkExecProcSys(t *testing.T) {
59+
proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Sys: &syscall.SysProcAttr{}})
60+
if !errors.Is(err, ErrNotImplementedSys) {
61+
t.Fatalf("wanted ErrNotImplementedSys, got %v\n", err)
62+
}
63+
64+
if proc != nil {
65+
t.Fatalf("wanted nil, got %v\n", proc)
66+
}
67+
}
68+
69+
func TestForkExecProcFiles(t *testing.T) {
70+
proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Files: []*File{}})
71+
if !errors.Is(err, ErrNotImplementedFiles) {
72+
t.Fatalf("wanted ErrNotImplementedFiles, got %v\n", err)
73+
}
74+
75+
if proc != nil {
76+
t.Fatalf("wanted nil, got %v\n", proc)
77+
}
78+
}

src/os/exec_other.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//go:build (!aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm
2+
3+
package os
4+
5+
import "syscall"
6+
7+
var (
8+
Interrupt Signal = syscall.SIGINT
9+
Kill Signal = syscall.SIGKILL
10+
)
11+
12+
func findProcess(pid int) (*Process, error) {
13+
return &Process{Pid: pid}, nil
14+
}
15+
16+
func (p *Process) release() error {
17+
p.Pid = -1
18+
return nil
19+
}
20+
21+
func forkExec(_ string, _ []string, _ *ProcAttr) (pid int, err error) {
22+
return 0, ErrNotImplemented
23+
}
24+
25+
func startProcess(_ string, _ []string, _ *ProcAttr) (proc *Process, err error) {
26+
return &Process{Pid: 0}, ErrNotImplemented
27+
}

src/os/exec_posix.go

-35
This file was deleted.

src/os/osexec.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//go:build linux && !baremetal && !tinygo.wasm
2+
3+
package os
4+
5+
import (
6+
"syscall"
7+
"unsafe"
8+
)
9+
10+
func fork() (pid int32, err error) {
11+
pid = libc_fork()
12+
if pid != 0 {
13+
if errno := *libc_errno(); errno != 0 {
14+
err = syscall.Errno(*libc_errno())
15+
}
16+
}
17+
return
18+
}
19+
20+
// the golang standard library does not expose interfaces for execve and fork, so we define them here the same way via the libc wrapper
21+
func execve(pathname string, argv []string, envv []string) error {
22+
argv0 := cstring(pathname)
23+
24+
// transform argv and envv into the format expected by execve
25+
argv1 := make([]*byte, len(argv)+1)
26+
for i, arg := range argv {
27+
argv1[i] = &cstring(arg)[0]
28+
}
29+
argv1[len(argv)] = nil
30+
31+
env1 := make([]*byte, len(envv)+1)
32+
for i, env := range envv {
33+
env1[i] = &cstring(env)[0]
34+
}
35+
env1[len(envv)] = nil
36+
37+
ret, _, err := syscall.Syscall(syscall.SYS_EXECVE, uintptr(unsafe.Pointer(&argv0[0])), uintptr(unsafe.Pointer(&argv1[0])), uintptr(unsafe.Pointer(&env1[0])))
38+
if int(ret) != 0 {
39+
return err
40+
}
41+
42+
return nil
43+
}
44+
45+
func cstring(s string) []byte {
46+
data := make([]byte, len(s)+1)
47+
copy(data, s)
48+
// final byte should be zero from the initial allocation
49+
return data
50+
}
51+
52+
//export fork
53+
func libc_fork() int32
54+
55+
// Internal musl function to get the C errno pointer.
56+
//
57+
//export __errno_location
58+
func libc_errno() *int32

src/syscall/syscall_libc.go

+5
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ func (w WaitStatus) Continued() bool { return false }
233233
func (w WaitStatus) StopSignal() Signal { return 0 }
234234
func (w WaitStatus) TrapCause() int { return 0 }
235235

236+
// since rusage is quite a big struct and we stub it out anyway no need to define it here
237+
func Wait4(pid int, wstatus *WaitStatus, options int, rusage uintptr) (wpid int, err error) {
238+
return 0, ENOSYS // TODO
239+
}
240+
236241
func Getenv(key string) (value string, found bool) {
237242
data := cstring(key)
238243
raw := libc_getenv(&data[0])

src/syscall/syscall_unix.go

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build linux || unix
2+
13
package syscall
24

35
func Exec(argv0 string, argv []string, envv []string) (err error)

0 commit comments

Comments
 (0)