diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:25:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:25:22 +0000 |
commit | f6ad4dcef54c5ce997a4bad5a6d86de229015700 (patch) | |
tree | 7cfa4e31ace5c2bd95c72b154d15af494b2bcbef /src/os/exec | |
parent | Initial commit. (diff) | |
download | golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.tar.xz golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.zip |
Adding upstream version 1.22.1.upstream/1.22.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
47 files changed, 6907 insertions, 0 deletions
diff --git a/src/os/exec.go b/src/os/exec.go new file mode 100644 index 0000000..ed5a75c --- /dev/null +++ b/src/os/exec.go @@ -0,0 +1,180 @@ +// 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. + +package os + +import ( + "errors" + "internal/testlog" + "runtime" + "sync" + "sync/atomic" + "syscall" + "time" +) + +// ErrProcessDone indicates a Process has finished. +var ErrProcessDone = errors.New("os: process already finished") + +// Process stores the information about a process created by StartProcess. +type Process struct { + Pid int + handle uintptr // handle is accessed atomically on Windows + isdone atomic.Bool // process has been successfully waited on + sigMu sync.RWMutex // avoid race between wait and signal +} + +func newProcess(pid int, handle uintptr) *Process { + p := &Process{Pid: pid, handle: handle} + runtime.SetFinalizer(p, (*Process).Release) + return p +} + +func (p *Process) setDone() { + p.isdone.Store(true) +} + +func (p *Process) done() bool { + return p.isdone.Load() +} + +// ProcAttr holds the attributes that will be applied to a new process +// started by StartProcess. +type ProcAttr struct { + // If Dir is non-empty, the child changes into the directory before + // creating the process. + Dir string + // If Env is non-nil, it gives the environment variables for the + // new process in the form returned by Environ. + // If it is nil, the result of Environ will be used. + Env []string + // Files specifies the open files inherited by the new process. The + // first three entries correspond to standard input, standard output, and + // standard error. An implementation may support additional entries, + // depending on the underlying operating system. A nil entry corresponds + // to that file being closed when the process starts. + // On Unix systems, StartProcess will change these File values + // to blocking mode, which means that SetDeadline will stop working + // and calling Close will not interrupt a Read or Write. + Files []*File + + // Operating system-specific process creation attributes. + // Note that setting this field means that your program + // may not execute properly or even compile on some + // operating systems. + Sys *syscall.SysProcAttr +} + +// A Signal represents an operating system signal. +// The usual underlying implementation is operating system-dependent: +// on Unix it is syscall.Signal. +type Signal interface { + String() string + Signal() // to distinguish from other Stringers +} + +// Getpid returns the process id of the caller. +func Getpid() int { return syscall.Getpid() } + +// Getppid returns the process id of the caller's parent. +func Getppid() int { return syscall.Getppid() } + +// FindProcess looks for a running process by its pid. +// +// The Process it returns can be used to obtain information +// about the underlying operating system process. +// +// On Unix systems, FindProcess always succeeds and returns a Process +// for the given pid, regardless of whether the process exists. To test whether +// the process actually exists, see whether p.Signal(syscall.Signal(0)) reports +// an error. +func FindProcess(pid int) (*Process, error) { + return findProcess(pid) +} + +// StartProcess starts a new process with the program, arguments and attributes +// specified by name, argv and attr. The argv slice will become os.Args in the +// new process, so it normally starts with the program name. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +// +// StartProcess is a low-level interface. The os/exec package provides +// higher-level interfaces. +// +// If there is an error, it will be of type *PathError. +func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) { + testlog.Open(name) + return startProcess(name, argv, attr) +} + +// Release releases any resources associated with the Process p, +// rendering it unusable in the future. +// Release only needs to be called if Wait is not. +func (p *Process) Release() error { + return p.release() +} + +// Kill causes the Process to exit immediately. Kill does not wait until +// the Process has actually exited. This only kills the Process itself, +// not any other processes it may have started. +func (p *Process) Kill() error { + return p.kill() +} + +// Wait waits for the Process to exit, and then returns a +// ProcessState describing its status and an error, if any. +// Wait releases any resources associated with the Process. +// On most operating systems, the Process must be a child +// of the current process or an error will be returned. +func (p *Process) Wait() (*ProcessState, error) { + return p.wait() +} + +// Signal sends a signal to the Process. +// Sending Interrupt on Windows is not implemented. +func (p *Process) Signal(sig Signal) error { + return p.signal(sig) +} + +// UserTime returns the user CPU time of the exited process and its children. +func (p *ProcessState) UserTime() time.Duration { + return p.userTime() +} + +// SystemTime returns the system CPU time of the exited process and its children. +func (p *ProcessState) SystemTime() time.Duration { + return p.systemTime() +} + +// Exited reports whether the program has exited. +// On Unix systems this reports true if the program exited due to calling exit, +// but false if the program terminated due to a signal. +func (p *ProcessState) Exited() bool { + return p.exited() +} + +// Success reports whether the program exited successfully, +// such as with exit status 0 on Unix. +func (p *ProcessState) Success() bool { + return p.success() +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() any { + return p.sys() +} + +// SysUsage returns system-dependent resource usage information about +// the exited process. Convert it to the appropriate underlying +// type, such as *syscall.Rusage on Unix, to access its contents. +// (On Unix, *syscall.Rusage matches struct rusage as defined in the +// getrusage(2) manual page.) +func (p *ProcessState) SysUsage() any { + return p.sysUsage() +} diff --git a/src/os/exec/bench_test.go b/src/os/exec/bench_test.go new file mode 100644 index 0000000..9a94001 --- /dev/null +++ b/src/os/exec/bench_test.go @@ -0,0 +1,23 @@ +// Copyright 2019 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. + +package exec + +import ( + "testing" +) + +func BenchmarkExecHostname(b *testing.B) { + b.ReportAllocs() + path, err := LookPath("hostname") + if err != nil { + b.Fatalf("could not find hostname: %v", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := Command(path).Run(); err != nil { + b.Fatalf("hostname: %v", err) + } + } +} diff --git a/src/os/exec/dot_test.go b/src/os/exec/dot_test.go new file mode 100644 index 0000000..ed4bad2 --- /dev/null +++ b/src/os/exec/dot_test.go @@ -0,0 +1,181 @@ +// Copyright 2022 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. + +package exec_test + +import ( + "errors" + "internal/testenv" + "os" + . "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +var pathVar string = func() string { + if runtime.GOOS == "plan9" { + return "path" + } + return "PATH" +}() + +func TestLookPath(t *testing.T) { + testenv.MustHaveExec(t) + // Not parallel: uses Chdir and Setenv. + + tmpDir := filepath.Join(t.TempDir(), "testdir") + if err := os.Mkdir(tmpDir, 0777); err != nil { + t.Fatal(err) + } + + executable := "execabs-test" + if runtime.GOOS == "windows" { + executable += ".exe" + } + if err := os.WriteFile(filepath.Join(tmpDir, executable), []byte{1, 2, 3}, 0777); err != nil { + t.Fatal(err) + } + chdir(t, tmpDir) + t.Setenv("PWD", tmpDir) + t.Logf(". is %#q", tmpDir) + + origPath := os.Getenv(pathVar) + + // Add "." to PATH so that exec.LookPath looks in the current directory on all systems. + // And try to trick it with "../testdir" too. + for _, errdot := range []string{"1", "0"} { + t.Run("GODEBUG=execerrdot="+errdot, func(t *testing.T) { + t.Setenv("GODEBUG", "execerrdot="+errdot+",execwait=2") + for _, dir := range []string{".", "../testdir"} { + t.Run(pathVar+"="+dir, func(t *testing.T) { + t.Setenv(pathVar, dir+string(filepath.ListSeparator)+origPath) + good := dir + "/execabs-test" + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want "%s...", nil`, good, found, err, good) + } + if runtime.GOOS == "windows" { + good = dir + `\execabs-test` + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want "%s...", nil`, good, found, err, good) + } + } + + _, err := LookPath("execabs-test") + if errdot == "1" { + if err == nil { + t.Fatalf("LookPath didn't fail when finding a non-relative path") + } else if !errors.Is(err, ErrDot) { + t.Fatalf("LookPath returned unexpected error: want Is ErrDot, got %q", err) + } + } else { + if err != nil { + t.Fatalf("LookPath failed unexpectedly: %v", err) + } + } + + cmd := Command("execabs-test") + if errdot == "1" { + if cmd.Err == nil { + t.Fatalf("Command didn't fail when finding a non-relative path") + } else if !errors.Is(cmd.Err, ErrDot) { + t.Fatalf("Command returned unexpected error: want Is ErrDot, got %q", cmd.Err) + } + cmd.Err = nil + } else { + if cmd.Err != nil { + t.Fatalf("Command failed unexpectedly: %v", err) + } + } + + // Clearing cmd.Err should let the execution proceed, + // and it should fail because it's not a valid binary. + if err := cmd.Run(); err == nil { + t.Fatalf("Run did not fail: expected exec error") + } else if errors.Is(err, ErrDot) { + t.Fatalf("Run returned unexpected error ErrDot: want error like ENOEXEC: %q", err) + } + }) + } + }) + } + + // Test the behavior when the first entry in PATH is an absolute name for the + // current directory. + // + // On Windows, "." may or may not be implicitly included before the explicit + // %PATH%, depending on the process environment; + // see https://go.dev/issue/4394. + // + // If the relative entry from "." resolves to the same executable as what + // would be resolved from an absolute entry in %PATH% alone, LookPath should + // return the absolute version of the path instead of ErrDot. + // (See https://go.dev/issue/53536.) + // + // If PATH does not implicitly include "." (such as on Unix platforms, or on + // Windows configured with NoDefaultCurrentDirectoryInExePath), then this + // lookup should succeed regardless of the behavior for ".", so it may be + // useful to run as a control case even on those platforms. + t.Run(pathVar+"=$PWD", func(t *testing.T) { + t.Setenv(pathVar, tmpDir+string(filepath.ListSeparator)+origPath) + good := filepath.Join(tmpDir, "execabs-test") + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want \"%s...\", nil`, good, found, err, good) + } + + if found, err := LookPath("execabs-test"); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want \"%s...\", nil`, "execabs-test", found, err, good) + } + + cmd := Command("execabs-test") + if cmd.Err != nil { + t.Fatalf("Command(%#q).Err = %v; want nil", "execabs-test", cmd.Err) + } + }) + + t.Run(pathVar+"=$OTHER", func(t *testing.T) { + // Control case: if the lookup returns ErrDot when PATH is empty, then we + // know that PATH implicitly includes ".". If it does not, then we don't + // expect to see ErrDot at all in this test (because the path will be + // unambiguously absolute). + wantErrDot := false + t.Setenv(pathVar, "") + if found, err := LookPath("execabs-test"); errors.Is(err, ErrDot) { + wantErrDot = true + } else if err == nil { + t.Fatalf(`with PATH='', LookPath(%#q) = %#q; want non-nil error`, "execabs-test", found) + } + + // Set PATH to include an explicit directory that contains a completely + // independent executable that happens to have the same name as an + // executable in ".". If "." is included implicitly, looking up the + // (unqualified) executable name will return ErrDot; otherwise, the + // executable in "." should have no effect and the lookup should + // unambiguously resolve to the directory in PATH. + + dir := t.TempDir() + executable := "execabs-test" + if runtime.GOOS == "windows" { + executable += ".exe" + } + if err := os.WriteFile(filepath.Join(dir, executable), []byte{1, 2, 3}, 0777); err != nil { + t.Fatal(err) + } + t.Setenv(pathVar, dir+string(filepath.ListSeparator)+origPath) + + found, err := LookPath("execabs-test") + if wantErrDot { + wantFound := filepath.Join(".", executable) + if found != wantFound || !errors.Is(err, ErrDot) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, Is ErrDot`, "execabs-test", found, err, wantFound) + } + } else { + wantFound := filepath.Join(dir, executable) + if found != wantFound || err != nil { + t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, nil`, "execabs-test", found, err, wantFound) + } + } + }) +} diff --git a/src/os/exec/env_test.go b/src/os/exec/env_test.go new file mode 100644 index 0000000..ea06af3 --- /dev/null +++ b/src/os/exec/env_test.go @@ -0,0 +1,67 @@ +// Copyright 2017 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. + +package exec + +import ( + "reflect" + "testing" +) + +func TestDedupEnv(t *testing.T) { + t.Parallel() + + tests := []struct { + noCase bool + nulOK bool + in []string + want []string + wantErr bool + }{ + { + noCase: true, + in: []string{"k1=v1", "k2=v2", "K1=v3"}, + want: []string{"k2=v2", "K1=v3"}, + }, + { + noCase: false, + in: []string{"k1=v1", "K1=V2", "k1=v3"}, + want: []string{"K1=V2", "k1=v3"}, + }, + { + in: []string{"=a", "=b", "foo", "bar"}, + want: []string{"=b", "foo", "bar"}, + }, + { + // #49886: preserve weird Windows keys with leading "=" signs. + noCase: true, + in: []string{`=C:=C:\golang`, `=D:=D:\tmp`, `=D:=D:\`}, + want: []string{`=C:=C:\golang`, `=D:=D:\`}, + }, + { + // #52436: preserve invalid key-value entries (for now). + // (Maybe filter them out or error out on them at some point.) + in: []string{"dodgy", "entries"}, + want: []string{"dodgy", "entries"}, + }, + { + // Filter out entries containing NULs. + in: []string{"A=a\x00b", "B=b", "C\x00C=c"}, + want: []string{"B=b"}, + wantErr: true, + }, + { + // Plan 9 needs to preserve environment variables with NUL (#56544). + nulOK: true, + in: []string{"path=one\x00two"}, + want: []string{"path=one\x00two"}, + }, + } + for _, tt := range tests { + got, err := dedupEnvCase(tt.noCase, tt.nulOK, tt.in) + if !reflect.DeepEqual(got, tt.want) || (err != nil) != tt.wantErr { + t.Errorf("Dedup(%v, %q) = %q, %v; want %q, error:%v", tt.noCase, tt.in, got, err, tt.want, tt.wantErr) + } + } +} diff --git a/src/os/exec/example_test.go b/src/os/exec/example_test.go new file mode 100644 index 0000000..150f5cf --- /dev/null +++ b/src/os/exec/example_test.go @@ -0,0 +1,169 @@ +// 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. + +package exec_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "time" +) + +func ExampleLookPath() { + path, err := exec.LookPath("fortune") + if err != nil { + log.Fatal("installing fortune is in your future") + } + fmt.Printf("fortune is available at %s\n", path) +} + +func ExampleCommand() { + cmd := exec.Command("tr", "a-z", "A-Z") + cmd.Stdin = strings.NewReader("some input") + var out strings.Builder + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + fmt.Printf("in all caps: %q\n", out.String()) +} + +func ExampleCommand_environment() { + cmd := exec.Command("prog") + cmd.Env = append(os.Environ(), + "FOO=duplicate_value", // ignored + "FOO=actual_value", // this value is used + ) + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} + +func ExampleCmd_Output() { + out, err := exec.Command("date").Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("The date is %s\n", out) +} + +func ExampleCmd_Run() { + cmd := exec.Command("sleep", "1") + log.Printf("Running command and waiting for it to finish...") + err := cmd.Run() + log.Printf("Command finished with error: %v", err) +} + +func ExampleCmd_Start() { + cmd := exec.Command("sleep", "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) +} + +func ExampleCmd_StdoutPipe() { + cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`) + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + var person struct { + Name string + Age int + } + if err := json.NewDecoder(stdout).Decode(&person); err != nil { + log.Fatal(err) + } + if err := cmd.Wait(); err != nil { + log.Fatal(err) + } + fmt.Printf("%s is %d years old\n", person.Name, person.Age) +} + +func ExampleCmd_StdinPipe() { + cmd := exec.Command("cat") + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) + } + + go func() { + defer stdin.Close() + io.WriteString(stdin, "values written to stdin are passed to cmd's standard input") + }() + + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s\n", out) +} + +func ExampleCmd_StderrPipe() { + cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") + stderr, err := cmd.StderrPipe() + if err != nil { + log.Fatal(err) + } + + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + + slurp, _ := io.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + + if err := cmd.Wait(); err != nil { + log.Fatal(err) + } +} + +func ExampleCmd_CombinedOutput() { + cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", stdoutStderr) +} + +func ExampleCmd_Environ() { + cmd := exec.Command("pwd") + + // Set Dir before calling cmd.Environ so that it will include an + // updated PWD variable (on platforms where that is used). + cmd.Dir = ".." + cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1") + + out, err := cmd.Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", out) +} + +func ExampleCommandContext() { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil { + // This will fail after 100 milliseconds. The 5 second sleep + // will be interrupted. + } +} diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go new file mode 100644 index 0000000..c88ee7f --- /dev/null +++ b/src/os/exec/exec.go @@ -0,0 +1,1303 @@ +// 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. + +// Package exec runs external commands. It wraps os.StartProcess to make it +// easier to remap stdin and stdout, connect I/O with pipes, and do other +// adjustments. +// +// Unlike the "system" library call from C and other languages, the +// os/exec package intentionally does not invoke the system shell and +// does not expand any glob patterns or handle other expansions, +// pipelines, or redirections typically done by shells. The package +// behaves more like C's "exec" family of functions. To expand glob +// patterns, either call the shell directly, taking care to escape any +// dangerous input, or use the path/filepath package's Glob function. +// To expand environment variables, use package os's ExpandEnv. +// +// Note that the examples in this package assume a Unix system. +// They may not run on Windows, and they do not run in the Go Playground +// used by golang.org and godoc.org. +// +// # Executables in the current directory +// +// The functions Command and LookPath look for a program +// in the directories listed in the current path, following the +// conventions of the host operating system. +// Operating systems have for decades included the current +// directory in this search, sometimes implicitly and sometimes +// configured explicitly that way by default. +// Modern practice is that including the current directory +// is usually unexpected and often leads to security problems. +// +// To avoid those security problems, as of Go 1.19, this package will not resolve a program +// using an implicit or explicit path entry relative to the current directory. +// That is, if you run exec.LookPath("go"), it will not successfully return +// ./go on Unix nor .\go.exe on Windows, no matter how the path is configured. +// Instead, if the usual path algorithms would result in that answer, +// these functions return an error err satisfying errors.Is(err, ErrDot). +// +// For example, consider these two program snippets: +// +// path, err := exec.LookPath("prog") +// if err != nil { +// log.Fatal(err) +// } +// use(path) +// +// and +// +// cmd := exec.Command("prog") +// if err := cmd.Run(); err != nil { +// log.Fatal(err) +// } +// +// These will not find and run ./prog or .\prog.exe, +// no matter how the current path is configured. +// +// Code that always wants to run a program from the current directory +// can be rewritten to say "./prog" instead of "prog". +// +// Code that insists on including results from relative path entries +// can instead override the error using an errors.Is check: +// +// path, err := exec.LookPath("prog") +// if errors.Is(err, exec.ErrDot) { +// err = nil +// } +// if err != nil { +// log.Fatal(err) +// } +// use(path) +// +// and +// +// cmd := exec.Command("prog") +// if errors.Is(cmd.Err, exec.ErrDot) { +// cmd.Err = nil +// } +// if err := cmd.Run(); err != nil { +// log.Fatal(err) +// } +// +// Setting the environment variable GODEBUG=execerrdot=0 +// disables generation of ErrDot entirely, temporarily restoring the pre-Go 1.19 +// behavior for programs that are unable to apply more targeted fixes. +// A future version of Go may remove support for this variable. +// +// Before adding such overrides, make sure you understand the +// security implications of doing so. +// See https://go.dev/blog/path-security for more information. +package exec + +import ( + "bytes" + "context" + "errors" + "internal/godebug" + "internal/syscall/execenv" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" +) + +// Error is returned by LookPath when it fails to classify a file as an +// executable. +type Error struct { + // Name is the file name for which the error occurred. + Name string + // Err is the underlying error. + Err error +} + +func (e *Error) Error() string { + return "exec: " + strconv.Quote(e.Name) + ": " + e.Err.Error() +} + +func (e *Error) Unwrap() error { return e.Err } + +// ErrWaitDelay is returned by (*Cmd).Wait if the process exits with a +// successful status code but its output pipes are not closed before the +// command's WaitDelay expires. +var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete") + +// wrappedError wraps an error without relying on fmt.Errorf. +type wrappedError struct { + prefix string + err error +} + +func (w wrappedError) Error() string { + return w.prefix + ": " + w.err.Error() +} + +func (w wrappedError) Unwrap() error { + return w.err +} + +// Cmd represents an external command being prepared or run. +// +// A Cmd cannot be reused after calling its Run, Output or CombinedOutput +// methods. +type Cmd struct { + // Path is the path of the command to run. + // + // This is the only field that must be set to a non-zero + // value. If Path is relative, it is evaluated relative + // to Dir. + Path string + + // Args holds command line arguments, including the command as Args[0]. + // If the Args field is empty or nil, Run uses {Path}. + // + // In typical use, both Path and Args are set by calling Command. + Args []string + + // Env specifies the environment of the process. + // Each entry is of the form "key=value". + // If Env is nil, the new process uses the current process's + // environment. + // If Env contains duplicate environment keys, only the last + // value in the slice for each duplicate key is used. + // As a special case on Windows, SYSTEMROOT is always added if + // missing and not explicitly set to the empty string. + Env []string + + // Dir specifies the working directory of the command. + // If Dir is the empty string, Run runs the command in the + // calling process's current directory. + Dir string + + // Stdin specifies the process's standard input. + // + // If Stdin is nil, the process reads from the null device (os.DevNull). + // + // If Stdin is an *os.File, the process's standard input is connected + // directly to that file. + // + // Otherwise, during the execution of the command a separate + // goroutine reads from Stdin and delivers that data to the command + // over a pipe. In this case, Wait does not complete until the goroutine + // stops copying, either because it has reached the end of Stdin + // (EOF or a read error), or because writing to the pipe returned an error, + // or because a nonzero WaitDelay was set and expired. + Stdin io.Reader + + // Stdout and Stderr specify the process's standard output and error. + // + // If either is nil, Run connects the corresponding file descriptor + // to the null device (os.DevNull). + // + // If either is an *os.File, the corresponding output from the process + // is connected directly to that file. + // + // Otherwise, during the execution of the command a separate goroutine + // reads from the process over a pipe and delivers that data to the + // corresponding Writer. In this case, Wait does not complete until the + // goroutine reaches EOF or encounters an error or a nonzero WaitDelay + // expires. + // + // If Stdout and Stderr are the same writer, and have a type that can + // be compared with ==, at most one goroutine at a time will call Write. + Stdout io.Writer + Stderr io.Writer + + // ExtraFiles specifies additional open files to be inherited by the + // new process. It does not include standard input, standard output, or + // standard error. If non-nil, entry i becomes file descriptor 3+i. + // + // ExtraFiles is not supported on Windows. + ExtraFiles []*os.File + + // SysProcAttr holds optional, operating system-specific attributes. + // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. + SysProcAttr *syscall.SysProcAttr + + // Process is the underlying process, once started. + Process *os.Process + + // ProcessState contains information about an exited process. + // If the process was started successfully, Wait or Run will + // populate its ProcessState when the command completes. + ProcessState *os.ProcessState + + // ctx is the context passed to CommandContext, if any. + ctx context.Context + + Err error // LookPath error, if any. + + // If Cancel is non-nil, the command must have been created with + // CommandContext and Cancel will be called when the command's + // Context is done. By default, CommandContext sets Cancel to + // call the Kill method on the command's Process. + // + // Typically a custom Cancel will send a signal to the command's + // Process, but it may instead take other actions to initiate cancellation, + // such as closing a stdin or stdout pipe or sending a shutdown request on a + // network socket. + // + // If the command exits with a success status after Cancel is + // called, and Cancel does not return an error equivalent to + // os.ErrProcessDone, then Wait and similar methods will return a non-nil + // error: either an error wrapping the one returned by Cancel, + // or the error from the Context. + // (If the command exits with a non-success status, or Cancel + // returns an error that wraps os.ErrProcessDone, Wait and similar methods + // continue to return the command's usual exit status.) + // + // If Cancel is set to nil, nothing will happen immediately when the command's + // Context is done, but a nonzero WaitDelay will still take effect. That may + // be useful, for example, to work around deadlocks in commands that do not + // support shutdown signals but are expected to always finish quickly. + // + // Cancel will not be called if Start returns a non-nil error. + Cancel func() error + + // If WaitDelay is non-zero, it bounds the time spent waiting on two sources + // of unexpected delay in Wait: a child process that fails to exit after the + // associated Context is canceled, and a child process that exits but leaves + // its I/O pipes unclosed. + // + // The WaitDelay timer starts when either the associated Context is done or a + // call to Wait observes that the child process has exited, whichever occurs + // first. When the delay has elapsed, the command shuts down the child process + // and/or its I/O pipes. + // + // If the child process has failed to exit — perhaps because it ignored or + // failed to receive a shutdown signal from a Cancel function, or because no + // Cancel function was set — then it will be terminated using os.Process.Kill. + // + // Then, if the I/O pipes communicating with the child process are still open, + // those pipes are closed in order to unblock any goroutines currently blocked + // on Read or Write calls. + // + // If pipes are closed due to WaitDelay, no Cancel call has occurred, + // and the command has otherwise exited with a successful status, Wait and + // similar methods will return ErrWaitDelay instead of nil. + // + // If WaitDelay is zero (the default), I/O pipes will be read until EOF, + // which might not occur until orphaned subprocesses of the command have + // also closed their descriptors for the pipes. + WaitDelay time.Duration + + // childIOFiles holds closers for any of the child process's + // stdin, stdout, and/or stderr files that were opened by the Cmd itself + // (not supplied by the caller). These should be closed as soon as they + // are inherited by the child process. + childIOFiles []io.Closer + + // parentIOPipes holds closers for the parent's end of any pipes + // connected to the child's stdin, stdout, and/or stderr streams + // that were opened by the Cmd itself (not supplied by the caller). + // These should be closed after Wait sees the command and copying + // goroutines exit, or after WaitDelay has expired. + parentIOPipes []io.Closer + + // goroutine holds a set of closures to execute to copy data + // to and/or from the command's I/O pipes. + goroutine []func() error + + // If goroutineErr is non-nil, it receives the first error from a copying + // goroutine once all such goroutines have completed. + // goroutineErr is set to nil once its error has been received. + goroutineErr <-chan error + + // If ctxResult is non-nil, it receives the result of watchCtx exactly once. + ctxResult <-chan ctxResult + + // The stack saved when the Command was created, if GODEBUG contains + // execwait=2. Used for debugging leaks. + createdByStack []byte + + // For a security release long ago, we created x/sys/execabs, + // which manipulated the unexported lookPathErr error field + // in this struct. For Go 1.19 we exported the field as Err error, + // above, but we have to keep lookPathErr around for use by + // old programs building against new toolchains. + // The String and Start methods look for an error in lookPathErr + // in preference to Err, to preserve the errors that execabs sets. + // + // In general we don't guarantee misuse of reflect like this, + // but the misuse of reflect was by us, the best of various bad + // options to fix the security problem, and people depend on + // those old copies of execabs continuing to work. + // The result is that we have to leave this variable around for the + // rest of time, a compatibility scar. + // + // See https://go.dev/blog/path-security + // and https://go.dev/issue/43724 for more context. + lookPathErr error +} + +// A ctxResult reports the result of watching the Context associated with a +// running command (and sending corresponding signals if needed). +type ctxResult struct { + err error + + // If timer is non-nil, it expires after WaitDelay has elapsed after + // the Context is done. + // + // (If timer is nil, that means that the Context was not done before the + // command completed, or no WaitDelay was set, or the WaitDelay already + // expired and its effect was already applied.) + timer *time.Timer +} + +var execwait = godebug.New("#execwait") +var execerrdot = godebug.New("execerrdot") + +// Command returns the Cmd struct to execute the named program with +// the given arguments. +// +// It sets only the Path and Args in the returned structure. +// +// If name contains no path separators, Command uses LookPath to +// resolve name to a complete path if possible. Otherwise it uses name +// directly as Path. +// +// The returned Cmd's Args field is constructed from the command name +// followed by the elements of arg, so arg should not include the +// command name itself. For example, Command("echo", "hello"). +// Args[0] is always name, not the possibly resolved Path. +// +// On Windows, processes receive the whole command line as a single string +// and do their own parsing. Command combines and quotes Args into a command +// line string with an algorithm compatible with applications using +// CommandLineToArgvW (which is the most common way). Notable exceptions are +// msiexec.exe and cmd.exe (and thus, all batch files), which have a different +// unquoting algorithm. In these or other similar cases, you can do the +// quoting yourself and provide the full command line in SysProcAttr.CmdLine, +// leaving Args empty. +func Command(name string, arg ...string) *Cmd { + cmd := &Cmd{ + Path: name, + Args: append([]string{name}, arg...), + } + + if v := execwait.Value(); v != "" { + if v == "2" { + // Obtain the caller stack. (This is equivalent to runtime/debug.Stack, + // copied to avoid importing the whole package.) + stack := make([]byte, 1024) + for { + n := runtime.Stack(stack, false) + if n < len(stack) { + stack = stack[:n] + break + } + stack = make([]byte, 2*len(stack)) + } + + if i := bytes.Index(stack, []byte("\nos/exec.Command(")); i >= 0 { + stack = stack[i+1:] + } + cmd.createdByStack = stack + } + + runtime.SetFinalizer(cmd, func(c *Cmd) { + if c.Process != nil && c.ProcessState == nil { + debugHint := "" + if c.createdByStack == nil { + debugHint = " (set GODEBUG=execwait=2 to capture stacks for debugging)" + } else { + os.Stderr.WriteString("GODEBUG=execwait=2 detected a leaked exec.Cmd created by:\n") + os.Stderr.Write(c.createdByStack) + os.Stderr.WriteString("\n") + debugHint = "" + } + panic("exec: Cmd started a Process but leaked without a call to Wait" + debugHint) + } + }) + } + + if filepath.Base(name) == name { + lp, err := LookPath(name) + if lp != "" { + // Update cmd.Path even if err is non-nil. + // If err is ErrDot (especially on Windows), lp may include a resolved + // extension (like .exe or .bat) that should be preserved. + cmd.Path = lp + } + if err != nil { + cmd.Err = err + } + } else if runtime.GOOS == "windows" && filepath.IsAbs(name) { + // We may need to add a filename extension from PATHEXT + // or verify an extension that is already present. + // Since the path is absolute, its extension should be unambiguous + // and independent of cmd.Dir, and we can go ahead and update cmd.Path to + // reflect it. + // + // Note that we cannot add an extension here for relative paths, because + // cmd.Dir may be set after we return from this function and that may cause + // the command to resolve to a different extension. + lp, err := lookExtensions(name, "") + if lp != "" { + cmd.Path = lp + } + if err != nil { + cmd.Err = err + } + } + return cmd +} + +// CommandContext is like Command but includes a context. +// +// The provided context is used to interrupt the process +// (by calling cmd.Cancel or os.Process.Kill) +// if the context becomes done before the command completes on its own. +// +// CommandContext sets the command's Cancel function to invoke the Kill method +// on its Process, and leaves its WaitDelay unset. The caller may change the +// cancellation behavior by modifying those fields before starting the command. +func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { + if ctx == nil { + panic("nil Context") + } + cmd := Command(name, arg...) + cmd.ctx = ctx + cmd.Cancel = func() error { + return cmd.Process.Kill() + } + return cmd +} + +// String returns a human-readable description of c. +// It is intended only for debugging. +// In particular, it is not suitable for use as input to a shell. +// The output of String may vary across Go releases. +func (c *Cmd) String() string { + if c.Err != nil || c.lookPathErr != nil { + // failed to resolve path; report the original requested path (plus args) + return strings.Join(c.Args, " ") + } + // report the exact executable path (plus args) + b := new(strings.Builder) + b.WriteString(c.Path) + for _, a := range c.Args[1:] { + b.WriteByte(' ') + b.WriteString(a) + } + return b.String() +} + +// interfaceEqual protects against panics from doing equality tests on +// two interfaces with non-comparable underlying types. +func interfaceEqual(a, b any) bool { + defer func() { + recover() + }() + return a == b +} + +func (c *Cmd) argv() []string { + if len(c.Args) > 0 { + return c.Args + } + return []string{c.Path} +} + +func (c *Cmd) childStdin() (*os.File, error) { + if c.Stdin == nil { + f, err := os.Open(os.DevNull) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := c.Stdin.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(pw, c.Stdin) + if skipStdinCopyError(err) { + err = nil + } + if err1 := pw.Close(); err == nil { + err = err1 + } + return err + }) + return pr, nil +} + +func (c *Cmd) childStdout() (*os.File, error) { + return c.writerDescriptor(c.Stdout) +} + +func (c *Cmd) childStderr(childStdout *os.File) (*os.File, error) { + if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { + return childStdout, nil + } + return c.writerDescriptor(c.Stderr) +} + +// writerDescriptor returns an os.File to which the child process +// can write to send data to w. +// +// If w is nil, writerDescriptor returns a File that writes to os.DevNull. +func (c *Cmd) writerDescriptor(w io.Writer) (*os.File, error) { + if w == nil { + f, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := w.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(w, pr) + pr.Close() // in case io.Copy stopped due to write error + return err + }) + return pw, nil +} + +func closeDescriptors(closers []io.Closer) { + for _, fd := range closers { + fd.Close() + } +} + +// Run starts the specified command and waits for it to complete. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command starts but does not complete successfully, the error is of +// type *ExitError. Other error types may be returned for other situations. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +func (c *Cmd) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +// Start starts the specified command but does not wait for it to complete. +// +// If Start returns successfully, the c.Process field will be set. +// +// After a successful call to Start the Wait method must be called in +// order to release associated system resources. +func (c *Cmd) Start() error { + // Check for doubled Start calls before we defer failure cleanup. If the prior + // call to Start succeeded, we don't want to spuriously close its pipes. + if c.Process != nil { + return errors.New("exec: already started") + } + + started := false + defer func() { + closeDescriptors(c.childIOFiles) + c.childIOFiles = nil + + if !started { + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + } + }() + + if c.Path == "" && c.Err == nil && c.lookPathErr == nil { + c.Err = errors.New("exec: no command") + } + if c.Err != nil || c.lookPathErr != nil { + if c.lookPathErr != nil { + return c.lookPathErr + } + return c.Err + } + lp := c.Path + if runtime.GOOS == "windows" && !filepath.IsAbs(c.Path) { + // If c.Path is relative, we had to wait until now + // to resolve it in case c.Dir was changed. + // (If it is absolute, we already resolved its extension in Command + // and shouldn't need to do so again.) + // + // Unfortunately, we cannot write the result back to c.Path because programs + // may assume that they can call Start concurrently with reading the path. + // (It is safe and non-racy to do so on Unix platforms, and users might not + // test with the race detector on all platforms; + // see https://go.dev/issue/62596.) + // + // So we will pass the fully resolved path to os.StartProcess, but leave + // c.Path as is: missing a bit of logging information seems less harmful + // than triggering a surprising data race, and if the user really cares + // about that bit of logging they can always use LookPath to resolve it. + var err error + lp, err = lookExtensions(c.Path, c.Dir) + if err != nil { + return err + } + } + if c.Cancel != nil && c.ctx == nil { + return errors.New("exec: command with a non-nil Cancel was not created with CommandContext") + } + if c.ctx != nil { + select { + case <-c.ctx.Done(): + return c.ctx.Err() + default: + } + } + + childFiles := make([]*os.File, 0, 3+len(c.ExtraFiles)) + stdin, err := c.childStdin() + if err != nil { + return err + } + childFiles = append(childFiles, stdin) + stdout, err := c.childStdout() + if err != nil { + return err + } + childFiles = append(childFiles, stdout) + stderr, err := c.childStderr(stdout) + if err != nil { + return err + } + childFiles = append(childFiles, stderr) + childFiles = append(childFiles, c.ExtraFiles...) + + env, err := c.environ() + if err != nil { + return err + } + + c.Process, err = os.StartProcess(lp, c.argv(), &os.ProcAttr{ + Dir: c.Dir, + Files: childFiles, + Env: env, + Sys: c.SysProcAttr, + }) + if err != nil { + return err + } + started = true + + // Don't allocate the goroutineErr channel unless there are goroutines to start. + if len(c.goroutine) > 0 { + goroutineErr := make(chan error, 1) + c.goroutineErr = goroutineErr + + type goroutineStatus struct { + running int + firstErr error + } + statusc := make(chan goroutineStatus, 1) + statusc <- goroutineStatus{running: len(c.goroutine)} + for _, fn := range c.goroutine { + go func(fn func() error) { + err := fn() + + status := <-statusc + if status.firstErr == nil { + status.firstErr = err + } + status.running-- + if status.running == 0 { + goroutineErr <- status.firstErr + } else { + statusc <- status + } + }(fn) + } + c.goroutine = nil // Allow the goroutines' closures to be GC'd when they complete. + } + + // If we have anything to do when the command's Context expires, + // start a goroutine to watch for cancellation. + // + // (Even if the command was created by CommandContext, a helper library may + // have explicitly set its Cancel field back to nil, indicating that it should + // be allowed to continue running after cancellation after all.) + if (c.Cancel != nil || c.WaitDelay != 0) && c.ctx != nil && c.ctx.Done() != nil { + resultc := make(chan ctxResult) + c.ctxResult = resultc + go c.watchCtx(resultc) + } + + return nil +} + +// watchCtx watches c.ctx until it is able to send a result to resultc. +// +// If c.ctx is done before a result can be sent, watchCtx calls c.Cancel, +// and/or kills cmd.Process it after c.WaitDelay has elapsed. +// +// watchCtx manipulates c.goroutineErr, so its result must be received before +// c.awaitGoroutines is called. +func (c *Cmd) watchCtx(resultc chan<- ctxResult) { + select { + case resultc <- ctxResult{}: + return + case <-c.ctx.Done(): + } + + var err error + if c.Cancel != nil { + if interruptErr := c.Cancel(); interruptErr == nil { + // We appear to have successfully interrupted the command, so any + // program behavior from this point may be due to ctx even if the + // command exits with code 0. + err = c.ctx.Err() + } else if errors.Is(interruptErr, os.ErrProcessDone) { + // The process already finished: we just didn't notice it yet. + // (Perhaps c.Wait hadn't been called, or perhaps it happened to race with + // c.ctx being cancelled.) Don't inject a needless error. + } else { + err = wrappedError{ + prefix: "exec: canceling Cmd", + err: interruptErr, + } + } + } + if c.WaitDelay == 0 { + resultc <- ctxResult{err: err} + return + } + + timer := time.NewTimer(c.WaitDelay) + select { + case resultc <- ctxResult{err: err, timer: timer}: + // c.Process.Wait returned and we've handed the timer off to c.Wait. + // It will take care of goroutine shutdown from here. + return + case <-timer.C: + } + + killed := false + if killErr := c.Process.Kill(); killErr == nil { + // We appear to have killed the process. c.Process.Wait should return a + // non-nil error to c.Wait unless the Kill signal races with a successful + // exit, and if that does happen we shouldn't report a spurious error, + // so don't set err to anything here. + killed = true + } else if !errors.Is(killErr, os.ErrProcessDone) { + err = wrappedError{ + prefix: "exec: killing Cmd", + err: killErr, + } + } + + if c.goroutineErr != nil { + select { + case goroutineErr := <-c.goroutineErr: + // Forward goroutineErr only if we don't have reason to believe it was + // caused by a call to Cancel or Kill above. + if err == nil && !killed { + err = goroutineErr + } + default: + // Close the child process's I/O pipes, in case it abandoned some + // subprocess that inherited them and is still holding them open + // (see https://go.dev/issue/23019). + // + // We close the goroutine pipes only after we have sent any signals we're + // going to send to the process (via Signal or Kill above): if we send + // SIGKILL to the process, we would prefer for it to die of SIGKILL, not + // SIGPIPE. (However, this may still cause any orphaned subprocesses to + // terminate with SIGPIPE.) + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but report ErrWaitDelay for + // the error: any other error here could result from closing the pipes. + _ = <-c.goroutineErr + if err == nil { + err = ErrWaitDelay + } + } + + // Since we have already received the only result from c.goroutineErr, + // set it to nil to prevent awaitGoroutines from blocking on it. + c.goroutineErr = nil + } + + resultc <- ctxResult{err: err} +} + +// An ExitError reports an unsuccessful exit by a command. +type ExitError struct { + *os.ProcessState + + // Stderr holds a subset of the standard error output from the + // Cmd.Output method if standard error was not otherwise being + // collected. + // + // If the error output is long, Stderr may contain only a prefix + // and suffix of the output, with the middle replaced with + // text about the number of omitted bytes. + // + // Stderr is provided for debugging, for inclusion in error messages. + // Users with other needs should redirect Cmd.Stderr as needed. + Stderr []byte +} + +func (e *ExitError) Error() string { + return e.ProcessState.String() +} + +// Wait waits for the command to exit and waits for any copying to +// stdin or copying from stdout or stderr to complete. +// +// The command must have been started by Start. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command fails to run or doesn't complete successfully, the +// error is of type *ExitError. Other error types may be +// returned for I/O problems. +// +// If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits +// for the respective I/O loop copying to or from the process to complete. +// +// Wait releases any resources associated with the Cmd. +func (c *Cmd) Wait() error { + if c.Process == nil { + return errors.New("exec: not started") + } + if c.ProcessState != nil { + return errors.New("exec: Wait was already called") + } + + state, err := c.Process.Wait() + if err == nil && !state.Success() { + err = &ExitError{ProcessState: state} + } + c.ProcessState = state + + var timer *time.Timer + if c.ctxResult != nil { + watch := <-c.ctxResult + timer = watch.timer + // If c.Process.Wait returned an error, prefer that. + // Otherwise, report any error from the watchCtx goroutine, + // such as a Context cancellation or a WaitDelay overrun. + if err == nil && watch.err != nil { + err = watch.err + } + } + + if goroutineErr := c.awaitGoroutines(timer); err == nil { + // Report an error from the copying goroutines only if the program otherwise + // exited normally on its own. Otherwise, the copying error may be due to the + // abnormal termination. + err = goroutineErr + } + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + + return err +} + +// awaitGoroutines waits for the results of the goroutines copying data to or +// from the command's I/O pipes. +// +// If c.WaitDelay elapses before the goroutines complete, awaitGoroutines +// forcibly closes their pipes and returns ErrWaitDelay. +// +// If timer is non-nil, it must send to timer.C at the end of c.WaitDelay. +func (c *Cmd) awaitGoroutines(timer *time.Timer) error { + defer func() { + if timer != nil { + timer.Stop() + } + c.goroutineErr = nil + }() + + if c.goroutineErr == nil { + return nil // No running goroutines to await. + } + + if timer == nil { + if c.WaitDelay == 0 { + return <-c.goroutineErr + } + + select { + case err := <-c.goroutineErr: + // Avoid the overhead of starting a timer. + return err + default: + } + + // No existing timer was started: either there is no Context associated with + // the command, or c.Process.Wait completed before the Context was done. + timer = time.NewTimer(c.WaitDelay) + } + + select { + case <-timer.C: + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but ignore any error + // (since it was probably caused by closing the pipes). + _ = <-c.goroutineErr + return ErrWaitDelay + + case err := <-c.goroutineErr: + return err + } +} + +// Output runs the command and returns its standard output. +// Any returned error will usually be of type *ExitError. +// If c.Stderr was nil, Output populates ExitError.Stderr. +func (c *Cmd) Output() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + var stdout bytes.Buffer + c.Stdout = &stdout + + captureErr := c.Stderr == nil + if captureErr { + c.Stderr = &prefixSuffixSaver{N: 32 << 10} + } + + err := c.Run() + if err != nil && captureErr { + if ee, ok := err.(*ExitError); ok { + ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes() + } + } + return stdout.Bytes(), err +} + +// CombinedOutput runs the command and returns its combined standard +// output and standard error. +func (c *Cmd) CombinedOutput() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + var b bytes.Buffer + c.Stdout = &b + c.Stderr = &b + err := c.Run() + return b.Bytes(), err +} + +// StdinPipe returns a pipe that will be connected to the command's +// standard input when the command starts. +// The pipe will be closed automatically after Wait sees the command exit. +// A caller need only call Close to force the pipe to close sooner. +// For example, if the command being run will not exit until standard input +// is closed, the caller must close the pipe. +func (c *Cmd) StdinPipe() (io.WriteCloser, error) { + if c.Stdin != nil { + return nil, errors.New("exec: Stdin already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdinPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdin = pr + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + return pw, nil +} + +// StdoutPipe returns a pipe that will be connected to the command's +// standard output when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to call Run when using StdoutPipe. +// See the example for idiomatic usage. +func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdoutPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdout = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// StderrPipe returns a pipe that will be connected to the command's +// standard error when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to use Run when using StderrPipe. +// See the StdoutPipe example for idiomatic usage. +func (c *Cmd) StderrPipe() (io.ReadCloser, error) { + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + if c.Process != nil { + return nil, errors.New("exec: StderrPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stderr = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// prefixSuffixSaver is an io.Writer which retains the first N bytes +// and the last N bytes written to it. The Bytes() methods reconstructs +// it with a pretty error message. +type prefixSuffixSaver struct { + N int // max size of prefix or suffix + prefix []byte + suffix []byte // ring buffer once len(suffix) == N + suffixOff int // offset to write into suffix + skipped int64 + + // TODO(bradfitz): we could keep one large []byte and use part of it for + // the prefix, reserve space for the '... Omitting N bytes ...' message, + // then the ring buffer suffix, and just rearrange the ring buffer + // suffix when Bytes() is called, but it doesn't seem worth it for + // now just for error messages. It's only ~64KB anyway. +} + +func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) { + lenp := len(p) + p = w.fill(&w.prefix, p) + + // Only keep the last w.N bytes of suffix data. + if overage := len(p) - w.N; overage > 0 { + p = p[overage:] + w.skipped += int64(overage) + } + p = w.fill(&w.suffix, p) + + // w.suffix is full now if p is non-empty. Overwrite it in a circle. + for len(p) > 0 { // 0, 1, or 2 iterations. + n := copy(w.suffix[w.suffixOff:], p) + p = p[n:] + w.skipped += int64(n) + w.suffixOff += n + if w.suffixOff == w.N { + w.suffixOff = 0 + } + } + return lenp, nil +} + +// fill appends up to len(p) bytes of p to *dst, such that *dst does not +// grow larger than w.N. It returns the un-appended suffix of p. +func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { + if remain := w.N - len(*dst); remain > 0 { + add := min(len(p), remain) + *dst = append(*dst, p[:add]...) + p = p[add:] + } + return p +} + +func (w *prefixSuffixSaver) Bytes() []byte { + if w.suffix == nil { + return w.prefix + } + if w.skipped == 0 { + return append(w.prefix, w.suffix...) + } + var buf bytes.Buffer + buf.Grow(len(w.prefix) + len(w.suffix) + 50) + buf.Write(w.prefix) + buf.WriteString("\n... omitting ") + buf.WriteString(strconv.FormatInt(w.skipped, 10)) + buf.WriteString(" bytes ...\n") + buf.Write(w.suffix[w.suffixOff:]) + buf.Write(w.suffix[:w.suffixOff]) + return buf.Bytes() +} + +// environ returns a best-effort copy of the environment in which the command +// would be run as it is currently configured. If an error occurs in computing +// the environment, it is returned alongside the best-effort copy. +func (c *Cmd) environ() ([]string, error) { + var err error + + env := c.Env + if env == nil { + env, err = execenv.Default(c.SysProcAttr) + if err != nil { + env = os.Environ() + // Note that the non-nil err is preserved despite env being overridden. + } + + if c.Dir != "" { + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable, so we don't need to + // keep it accurate. + default: + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory for the command, we should also update PWD to reflect that. + // + // Unfortunately, we didn't always do that, so (as proposed in + // https://go.dev/issue/50599) to avoid unintended collateral damage we + // only implicitly update PWD when Env is nil. That way, we're much + // less likely to override an intentional change to the variable. + if pwd, absErr := filepath.Abs(c.Dir); absErr == nil { + env = append(env, "PWD="+pwd) + } else if err == nil { + err = absErr + } + } + } + } + + env, dedupErr := dedupEnv(env) + if err == nil { + err = dedupErr + } + return addCriticalEnv(env), err +} + +// Environ returns a copy of the environment in which the command would be run +// as it is currently configured. +func (c *Cmd) Environ() []string { + // Intentionally ignore errors: environ returns a best-effort environment no matter what. + env, _ := c.environ() + return env +} + +// dedupEnv returns a copy of env with any duplicates removed, in favor of +// later values. +// Items not of the normal environment "key=value" form are preserved unchanged. +// Except on Plan 9, items containing NUL characters are removed, and +// an error is returned along with the remaining values. +func dedupEnv(env []string) ([]string, error) { + return dedupEnvCase(runtime.GOOS == "windows", runtime.GOOS == "plan9", env) +} + +// dedupEnvCase is dedupEnv with a case option for testing. +// If caseInsensitive is true, the case of keys is ignored. +// If nulOK is false, items containing NUL characters are allowed. +func dedupEnvCase(caseInsensitive, nulOK bool, env []string) ([]string, error) { + // Construct the output in reverse order, to preserve the + // last occurrence of each key. + var err error + out := make([]string, 0, len(env)) + saw := make(map[string]bool, len(env)) + for n := len(env); n > 0; n-- { + kv := env[n-1] + + // Reject NUL in environment variables to prevent security issues (#56284); + // except on Plan 9, which uses NUL as os.PathListSeparator (#56544). + if !nulOK && strings.IndexByte(kv, 0) != -1 { + err = errors.New("exec: environment variable contains NUL") + continue + } + + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-"="? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + if kv != "" { + // The entry is not of the form "key=value" (as it is required to be). + // Leave it as-is for now. + // TODO(#52436): should we strip or reject these bogus entries? + out = append(out, kv) + } + continue + } + k := kv[:i] + if caseInsensitive { + k = strings.ToLower(k) + } + if saw[k] { + continue + } + + saw[k] = true + out = append(out, kv) + } + + // Now reverse the slice to restore the original order. + for i := 0; i < len(out)/2; i++ { + j := len(out) - i - 1 + out[i], out[j] = out[j], out[i] + } + + return out, err +} + +// addCriticalEnv adds any critical environment variables that are required +// (or at least almost always required) on the operating system. +// Currently this is only used for Windows. +func addCriticalEnv(env []string) []string { + if runtime.GOOS != "windows" { + return env + } + for _, kv := range env { + k, _, ok := strings.Cut(kv, "=") + if !ok { + continue + } + if strings.EqualFold(k, "SYSTEMROOT") { + // We already have it. + return env + } + } + return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) +} + +// ErrDot indicates that a path lookup resolved to an executable +// in the current directory due to ‘.’ being in the path, either +// implicitly or explicitly. See the package documentation for details. +// +// Note that functions in this package do not return ErrDot directly. +// Code should use errors.Is(err, ErrDot), not err == ErrDot, +// to test whether a returned error err is due to this condition. +var ErrDot = errors.New("cannot run executable found relative to current directory") diff --git a/src/os/exec/exec_linux_test.go b/src/os/exec/exec_linux_test.go new file mode 100644 index 0000000..b9f6b7b --- /dev/null +++ b/src/os/exec/exec_linux_test.go @@ -0,0 +1,45 @@ +// Copyright 2020 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 && cgo + +// On systems that use glibc, calling malloc can create a new arena, +// and creating a new arena can read /sys/devices/system/cpu/online. +// If we are using cgo, we will call malloc when creating a new thread. +// That can break TestExtraFiles if we create a new thread that creates +// a new arena and opens the /sys file while we are checking for open +// file descriptors. Work around the problem by creating threads up front. +// See issue 25628. + +package exec_test + +import ( + "os" + "sync" + "syscall" + "time" +) + +func init() { + if os.Getenv("GO_EXEC_TEST_PID") == "" { + return + } + + // Start some threads. 10 is arbitrary but intended to be enough + // to ensure that the code won't have to create any threads itself. + // In particular this should be more than the number of threads + // the garbage collector might create. + const threads = 10 + + var wg sync.WaitGroup + wg.Add(threads) + ts := syscall.NsecToTimespec((100 * time.Microsecond).Nanoseconds()) + for i := 0; i < threads; i++ { + go func() { + defer wg.Done() + syscall.Nanosleep(&ts, nil) + }() + } + wg.Wait() +} diff --git a/src/os/exec/exec_other_test.go b/src/os/exec/exec_other_test.go new file mode 100644 index 0000000..64c819c --- /dev/null +++ b/src/os/exec/exec_other_test.go @@ -0,0 +1,14 @@ +// Copyright 2021 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 !unix && !windows + +package exec_test + +import "os" + +var ( + quitSignal os.Signal = nil + pipeSignal os.Signal = nil +) diff --git a/src/os/exec/exec_plan9.go b/src/os/exec/exec_plan9.go new file mode 100644 index 0000000..8920bec --- /dev/null +++ b/src/os/exec/exec_plan9.go @@ -0,0 +1,19 @@ +// Copyright 2019 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. + +package exec + +import "io/fs" + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore hungup errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 35753. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err.Error() == "i/o on hungup channel" +} diff --git a/src/os/exec/exec_posix_test.go b/src/os/exec/exec_posix_test.go new file mode 100644 index 0000000..5d828b3 --- /dev/null +++ b/src/os/exec/exec_posix_test.go @@ -0,0 +1,276 @@ +// Copyright 2017 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 unix + +package exec_test + +import ( + "fmt" + "internal/testenv" + "io" + "os" + "os/user" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "syscall" + "testing" + "time" +) + +func init() { + registerHelperCommand("pwd", cmdPwd) +} + +func cmdPwd(...string) { + pwd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(pwd) +} + +func TestCredentialNoSetGroups(t *testing.T) { + if runtime.GOOS == "android" { + maySkipHelperCommand("echo") + t.Skip("unsupported on Android") + } + t.Parallel() + + u, err := user.Current() + if err != nil { + t.Fatalf("error getting current user: %v", err) + } + + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("error converting Uid=%s to integer: %v", u.Uid, err) + } + + gid, err := strconv.Atoi(u.Gid) + if err != nil { + t.Fatalf("error converting Gid=%s to integer: %v", u.Gid, err) + } + + // If NoSetGroups is true, setgroups isn't called and cmd.Run should succeed + cmd := helperCommand(t, "echo", "foo") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + NoSetGroups: true, + }, + } + + if err = cmd.Run(); err != nil { + t.Errorf("Failed to run command: %v", err) + } +} + +// For issue #19314: make sure that SIGSTOP does not cause the process +// to appear done. +func TestWaitid(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // Wait for the child process to come up and register any signal handlers. + const msg = "O:ping\n" + if _, err := io.WriteString(stdin, msg); err != nil { + t.Fatal(err) + } + buf := make([]byte, len(msg)) + if _, err := io.ReadFull(stdout, buf); err != nil { + t.Fatal(err) + } + // Now leave the pipes open so that the process will hang until we close stdin. + + if err := cmd.Process.Signal(syscall.SIGSTOP); err != nil { + cmd.Process.Kill() + t.Fatal(err) + } + + ch := make(chan error) + go func() { + ch <- cmd.Wait() + }() + + // Give a little time for Wait to block on waiting for the process. + // (This is just to give some time to trigger the bug; it should not be + // necessary for the test to pass.) + if testing.Short() { + time.Sleep(1 * time.Millisecond) + } else { + time.Sleep(10 * time.Millisecond) + } + + // This call to Signal should succeed because the process still exists. + // (Prior to the fix for #19314, this would fail with os.ErrProcessDone + // or an equivalent error.) + if err := cmd.Process.Signal(syscall.SIGCONT); err != nil { + t.Error(err) + syscall.Kill(cmd.Process.Pid, syscall.SIGCONT) + } + + // The SIGCONT should allow the process to wake up, notice that stdin + // is closed, and exit successfully. + stdin.Close() + err = <-ch + if err != nil { + t.Fatal(err) + } +} + +// https://go.dev/issue/50599: if Env is not set explicitly, setting Dir should +// implicitly update PWD to the correct path, and Environ should list the +// updated value. +func TestImplicitPWD(t *testing.T) { + t.Parallel() + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + dir string + want string + }{ + {"empty", "", cwd}, + {"dot", ".", cwd}, + {"dotdot", "..", filepath.Dir(cwd)}, + {"PWD", cwd, cwd}, + {"PWDdotdot", cwd + string(filepath.Separator) + "..", filepath.Dir(cwd)}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + if cmd.Env != nil { + t.Fatalf("test requires helperCommand not to set Env field") + } + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.want} + if tc.dir == "" { + if _, ok := os.LookupEnv("PWD"); !ok { + wantPWDs = nil + } + } + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\n`pwd` reported\n\t%s", tc.dir, got) + if got != tc.want { + t.Errorf("want\n\t%s", tc.want) + } + }) + } +} + +// However, if cmd.Env is set explicitly, setting Dir should not override it. +// (This checks that the implementation for https://go.dev/issue/50599 doesn't +// break existing users who may have explicitly mismatched the PWD variable.) +func TestExplicitPWD(t *testing.T) { + t.Parallel() + + maySkipHelperCommand("pwd") + testenv.MustHaveSymlink(t) + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + link := filepath.Join(t.TempDir(), "link") + if err := os.Symlink(cwd, link); err != nil { + t.Fatal(err) + } + + // Now link is another equally-valid name for cwd. If we set Dir to one and + // PWD to the other, the subprocess should report the PWD version. + cases := []struct { + name string + dir string + pwd string + }{ + {name: "original PWD", pwd: cwd}, + {name: "link PWD", pwd: link}, + {name: "in link with original PWD", dir: link, pwd: cwd}, + {name: "in dir with link PWD", dir: cwd, pwd: link}, + // Ideally we would also like to test what happens if we set PWD to + // something totally bogus (or the empty string), but then we would have no + // idea what output the subprocess should actually produce: cwd itself may + // contain symlinks preserved from the PWD value in the test's environment. + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + // This is intentionally opposite to the usual order of setting cmd.Dir + // and then calling cmd.Environ. Here, we *want* PWD not to match cmd.Dir, + // so we don't care whether cmd.Dir is reflected in cmd.Environ. + cmd.Env = append(cmd.Environ(), "PWD="+tc.pwd) + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.pwd} + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\nwith PWD=%s\nsubprocess os.Getwd() reported\n\t%s", tc.dir, tc.pwd, got) + if got != tc.pwd { + t.Errorf("want\n\t%s", tc.pwd) + } + }) + } +} diff --git a/src/os/exec/exec_test.go b/src/os/exec/exec_test.go new file mode 100644 index 0000000..71a0049 --- /dev/null +++ b/src/os/exec/exec_test.go @@ -0,0 +1,1837 @@ +// 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. + +// Use an external test to avoid os/exec -> net/http -> crypto/x509 -> os/exec +// circular dependency on non-cgo darwin. + +package exec_test + +import ( + "bufio" + "bytes" + "context" + "errors" + "flag" + "fmt" + "internal/poll" + "internal/testenv" + "io" + "log" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "os/exec/internal/fdtest" + "os/signal" + "path/filepath" + "runtime" + "runtime/debug" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" +) + +// haveUnexpectedFDs is set at init time to report whether any file descriptors +// were open at program start. +var haveUnexpectedFDs bool + +func init() { + godebug := os.Getenv("GODEBUG") + if godebug != "" { + godebug += "," + } + godebug += "execwait=2" + os.Setenv("GODEBUG", godebug) + + if os.Getenv("GO_EXEC_TEST_PID") != "" { + return + } + if runtime.GOOS == "windows" { + return + } + for fd := uintptr(3); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { + continue + } + + if fdtest.Exists(fd) { + haveUnexpectedFDs = true + return + } + } +} + +// TestMain allows the test binary to impersonate many other binaries, +// some of which may manipulate os.Stdin, os.Stdout, and/or os.Stderr +// (and thus cannot run as an ordinary Test function, since the testing +// package monkey-patches those variables before running tests). +func TestMain(m *testing.M) { + flag.Parse() + + pid := os.Getpid() + if os.Getenv("GO_EXEC_TEST_PID") == "" { + os.Setenv("GO_EXEC_TEST_PID", strconv.Itoa(pid)) + + if runtime.GOOS == "windows" { + // Normalize environment so that test behavior is consistent. + // (The behavior of LookPath varies depending on this variable.) + // + // Ideally we would test both with the variable set and with it cleared, + // but I (bcmills) am not sure that that's feasible: it may already be set + // in the Windows registry, and I'm not sure if it is possible to remove + // a registry variable in a program's environment. + // + // Per https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw#remarks, + // “the existence of the NoDefaultCurrentDirectoryInExePath environment + // variable is checked, and not its value.” + os.Setenv("NoDefaultCurrentDirectoryInExePath", "TRUE") + } + + code := m.Run() + if code == 0 && flag.Lookup("test.run").Value.String() == "" && flag.Lookup("test.list").Value.String() == "" { + for cmd := range helperCommands { + if _, ok := helperCommandUsed.Load(cmd); !ok { + fmt.Fprintf(os.Stderr, "helper command unused: %q\n", cmd) + code = 1 + } + } + } + + if !testing.Short() { + // Run a couple of GC cycles to increase the odds of detecting + // process leaks using the finalizers installed by GODEBUG=execwait=2. + runtime.GC() + runtime.GC() + } + + os.Exit(code) + } + + args := flag.Args() + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "No command\n") + os.Exit(2) + } + + cmd, args := args[0], args[1:] + f, ok := helperCommands[cmd] + if !ok { + fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) + os.Exit(2) + } + f(args...) + os.Exit(0) +} + +// registerHelperCommand registers a command that the test process can impersonate. +// A command should be registered in the same source file in which it is used. +// If all tests are run and pass, all registered commands must be used. +// (This prevents stale commands from accreting if tests are removed or +// refactored over time.) +func registerHelperCommand(name string, f func(...string)) { + if helperCommands[name] != nil { + panic("duplicate command registered: " + name) + } + helperCommands[name] = f +} + +// maySkipHelperCommand records that the test that uses the named helper command +// was invoked, but may call Skip on the test before actually calling +// helperCommand. +func maySkipHelperCommand(name string) { + helperCommandUsed.Store(name, true) +} + +// helperCommand returns an exec.Cmd that will run the named helper command. +func helperCommand(t *testing.T, name string, args ...string) *exec.Cmd { + t.Helper() + return helperCommandContext(t, nil, name, args...) +} + +// helperCommandContext is like helperCommand, but also accepts a Context under +// which to run the command. +func helperCommandContext(t *testing.T, ctx context.Context, name string, args ...string) (cmd *exec.Cmd) { + helperCommandUsed.LoadOrStore(name, true) + + t.Helper() + testenv.MustHaveExec(t) + + cs := append([]string{name}, args...) + if ctx != nil { + cmd = exec.CommandContext(ctx, exePath(t), cs...) + } else { + cmd = exec.Command(exePath(t), cs...) + } + return cmd +} + +// exePath returns the path to the running executable. +func exePath(t testing.TB) string { + exeOnce.Do(func() { + // Use os.Executable instead of os.Args[0] in case the caller modifies + // cmd.Dir: if the test binary is invoked like "./exec.test", it should + // not fail spuriously. + exeOnce.path, exeOnce.err = os.Executable() + }) + + if exeOnce.err != nil { + if t == nil { + panic(exeOnce.err) + } + t.Fatal(exeOnce.err) + } + + return exeOnce.path +} + +var exeOnce struct { + path string + err error + sync.Once +} + +func chdir(t *testing.T, dir string) { + t.Helper() + + prev, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + t.Logf("Chdir(%#q)", dir) + + t.Cleanup(func() { + if err := os.Chdir(prev); err != nil { + // Couldn't chdir back to the original working directory. + // panic instead of t.Fatal so that we don't run other tests + // in an unexpected location. + panic("couldn't restore working directory: " + err.Error()) + } + }) +} + +var helperCommandUsed sync.Map + +var helperCommands = map[string]func(...string){ + "echo": cmdEcho, + "echoenv": cmdEchoEnv, + "cat": cmdCat, + "pipetest": cmdPipeTest, + "stdinClose": cmdStdinClose, + "exit": cmdExit, + "describefiles": cmdDescribeFiles, + "stderrfail": cmdStderrFail, + "yes": cmdYes, + "hang": cmdHang, +} + +func cmdEcho(args ...string) { + iargs := []any{} + for _, s := range args { + iargs = append(iargs, s) + } + fmt.Println(iargs...) +} + +func cmdEchoEnv(args ...string) { + for _, s := range args { + fmt.Println(os.Getenv(s)) + } +} + +func cmdCat(args ...string) { + if len(args) == 0 { + io.Copy(os.Stdout, os.Stdin) + return + } + exit := 0 + for _, fn := range args { + f, err := os.Open(fn) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + exit = 2 + } else { + defer f.Close() + io.Copy(os.Stdout, f) + } + } + os.Exit(exit) +} + +func cmdPipeTest(...string) { + bufr := bufio.NewReader(os.Stdin) + for { + line, _, err := bufr.ReadLine() + if err == io.EOF { + break + } else if err != nil { + os.Exit(1) + } + if bytes.HasPrefix(line, []byte("O:")) { + os.Stdout.Write(line) + os.Stdout.Write([]byte{'\n'}) + } else if bytes.HasPrefix(line, []byte("E:")) { + os.Stderr.Write(line) + os.Stderr.Write([]byte{'\n'}) + } else { + os.Exit(1) + } + } +} + +func cmdStdinClose(...string) { + b, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + if s := string(b); s != stdinCloseTestString { + fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString) + os.Exit(1) + } +} + +func cmdExit(args ...string) { + n, _ := strconv.Atoi(args[0]) + os.Exit(n) +} + +func cmdDescribeFiles(args ...string) { + f := os.NewFile(3, fmt.Sprintf("fd3")) + ln, err := net.FileListener(f) + if err == nil { + fmt.Printf("fd3: listener %s\n", ln.Addr()) + ln.Close() + } +} + +func cmdStderrFail(...string) { + fmt.Fprintf(os.Stderr, "some stderr text\n") + os.Exit(1) +} + +func cmdYes(args ...string) { + if len(args) == 0 { + args = []string{"y"} + } + s := strings.Join(args, " ") + "\n" + for { + _, err := os.Stdout.WriteString(s) + if err != nil { + os.Exit(1) + } + } +} + +func TestEcho(t *testing.T) { + t.Parallel() + + bs, err := helperCommand(t, "echo", "foo bar", "baz").Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(bs), "foo bar baz\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + +func TestCommandRelativeName(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo", "foo") + + // Run our own binary as a relative path + // (e.g. "_test/exec.test") our parent directory. + base := filepath.Base(os.Args[0]) // "exec.test" + dir := filepath.Dir(os.Args[0]) // "/tmp/go-buildNNNN/os/exec/_test" + if dir == "." { + t.Skip("skipping; running test at root somehow") + } + parentDir := filepath.Dir(dir) // "/tmp/go-buildNNNN/os/exec" + dirBase := filepath.Base(dir) // "_test" + if dirBase == "." { + t.Skipf("skipping; unexpected shallow dir of %q", dir) + } + + cmd.Path = filepath.Join(dirBase, base) + cmd.Dir = parentDir + + out, err := cmd.Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(out), "foo\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + +func TestCatStdin(t *testing.T) { + t.Parallel() + + // Cat, testing stdin and stdout. + input := "Input string\nLine 2" + p := helperCommand(t, "cat") + p.Stdin = strings.NewReader(input) + bs, err := p.Output() + if err != nil { + t.Errorf("cat: %v", err) + } + s := string(bs) + if s != input { + t.Errorf("cat: want %q, got %q", input, s) + } +} + +func TestEchoFileRace(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("StdinPipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Start: %v", err) + } + wrote := make(chan bool) + go func() { + defer close(wrote) + fmt.Fprint(stdin, "echo\n") + }() + if err := cmd.Wait(); err != nil { + t.Fatalf("Wait: %v", err) + } + <-wrote +} + +func TestCatGoodAndBadFile(t *testing.T) { + t.Parallel() + + // Testing combined output and error values. + bs, err := helperCommand(t, "cat", "/bogus/file.foo", "exec_test.go").CombinedOutput() + if _, ok := err.(*exec.ExitError); !ok { + t.Errorf("expected *exec.ExitError from cat combined; got %T: %v", err, err) + } + errLine, body, ok := strings.Cut(string(bs), "\n") + if !ok { + t.Fatalf("expected two lines from cat; got %q", bs) + } + if !strings.HasPrefix(errLine, "Error: open /bogus/file.foo") { + t.Errorf("expected stderr to complain about file; got %q", errLine) + } + if !strings.Contains(body, "func TestCatGoodAndBadFile(t *testing.T)") { + t.Errorf("expected test code; got %q (len %d)", body, len(body)) + } +} + +func TestNoExistExecutable(t *testing.T) { + t.Parallel() + + // Can't run a non-existent executable + err := exec.Command("/no-exist-executable").Run() + if err == nil { + t.Error("expected error from /no-exist-executable") + } +} + +func TestExitStatus(t *testing.T) { + t.Parallel() + + // Test that exit values are returned correctly + cmd := helperCommand(t, "exit", "42") + err := cmd.Run() + want := "exit status 42" + switch runtime.GOOS { + case "plan9": + want = fmt.Sprintf("exit status: '%s %d: 42'", filepath.Base(cmd.Path), cmd.ProcessState.Pid()) + } + if werr, ok := err.(*exec.ExitError); ok { + if s := werr.Error(); s != want { + t.Errorf("from exit 42 got exit %q, want %q", s, want) + } + } else { + t.Fatalf("expected *exec.ExitError from exit 42; got %T: %v", err, err) + } +} + +func TestExitCode(t *testing.T) { + t.Parallel() + + // Test that exit code are returned correctly + cmd := helperCommand(t, "exit", "42") + cmd.Run() + want := 42 + if runtime.GOOS == "plan9" { + want = 1 + } + got := cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "/no-exist-executable") + cmd.Run() + want = 2 + if runtime.GOOS == "plan9" { + want = 1 + } + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "exit", "255") + cmd.Run() + want = 255 + if runtime.GOOS == "plan9" { + want = 1 + } + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "cat") + cmd.Run() + want = 0 + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + // Test when command does not call Run(). + cmd = helperCommand(t, "cat") + want = -1 + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } +} + +func TestPipes(t *testing.T) { + t.Parallel() + + check := func(what string, err error) { + if err != nil { + t.Fatalf("%s: %v", what, err) + } + } + // Cat, testing stdin and stdout. + c := helperCommand(t, "pipetest") + stdin, err := c.StdinPipe() + check("StdinPipe", err) + stdout, err := c.StdoutPipe() + check("StdoutPipe", err) + stderr, err := c.StderrPipe() + check("StderrPipe", err) + + outbr := bufio.NewReader(stdout) + errbr := bufio.NewReader(stderr) + line := func(what string, br *bufio.Reader) string { + line, _, err := br.ReadLine() + if err != nil { + t.Fatalf("%s: %v", what, err) + } + return string(line) + } + + err = c.Start() + check("Start", err) + + _, err = stdin.Write([]byte("O:I am output\n")) + check("first stdin Write", err) + if g, e := line("first output line", outbr), "O:I am output"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + _, err = stdin.Write([]byte("E:I am error\n")) + check("second stdin Write", err) + if g, e := line("first error line", errbr), "E:I am error"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + _, err = stdin.Write([]byte("O:I am output2\n")) + check("third stdin Write 3", err) + if g, e := line("second output line", outbr), "O:I am output2"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + stdin.Close() + err = c.Wait() + check("Wait", err) +} + +const stdinCloseTestString = "Some test string." + +// Issue 6270. +func TestStdinClose(t *testing.T) { + t.Parallel() + + check := func(what string, err error) { + if err != nil { + t.Fatalf("%s: %v", what, err) + } + } + cmd := helperCommand(t, "stdinClose") + stdin, err := cmd.StdinPipe() + check("StdinPipe", err) + // Check that we can access methods of the underlying os.File.` + if _, ok := stdin.(interface { + Fd() uintptr + }); !ok { + t.Error("can't access methods of underlying *os.File") + } + check("Start", cmd.Start()) + + var wg sync.WaitGroup + wg.Add(1) + defer wg.Wait() + go func() { + defer wg.Done() + + _, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString)) + check("Copy", err) + + // Before the fix, this next line would race with cmd.Wait. + if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("Close: %v", err) + } + }() + + check("Wait", cmd.Wait()) +} + +// Issue 17647. +// It used to be the case that TestStdinClose, above, would fail when +// run under the race detector. This test is a variant of TestStdinClose +// that also used to fail when run under the race detector. +// This test is run by cmd/dist under the race detector to verify that +// the race detector no longer reports any problems. +func TestStdinCloseRace(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "stdinClose") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("StdinPipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Start: %v", err) + + } + + var wg sync.WaitGroup + wg.Add(2) + defer wg.Wait() + + go func() { + defer wg.Done() + // We don't check the error return of Kill. It is + // possible that the process has already exited, in + // which case Kill will return an error "process + // already finished". The purpose of this test is to + // see whether the race detector reports an error; it + // doesn't matter whether this Kill succeeds or not. + cmd.Process.Kill() + }() + + go func() { + defer wg.Done() + // Send the wrong string, so that the child fails even + // if the other goroutine doesn't manage to kill it first. + // This test is to check that the race detector does not + // falsely report an error, so it doesn't matter how the + // child process fails. + io.Copy(stdin, strings.NewReader("unexpected string")) + if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("stdin.Close: %v", err) + } + }() + + if err := cmd.Wait(); err == nil { + t.Fatalf("Wait: succeeded unexpectedly") + } +} + +// Issue 5071 +func TestPipeLookPathLeak(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("we don't currently suppore counting open handles on windows") + } + // Not parallel: checks for leaked file descriptors + + openFDs := func() []uintptr { + var fds []uintptr + for i := uintptr(0); i < 100; i++ { + if fdtest.Exists(i) { + fds = append(fds, i) + } + } + return fds + } + + old := map[uintptr]bool{} + for _, fd := range openFDs() { + old[fd] = true + } + + for i := 0; i < 6; i++ { + cmd := exec.Command("something-that-does-not-exist-executable") + cmd.StdoutPipe() + cmd.StderrPipe() + cmd.StdinPipe() + if err := cmd.Run(); err == nil { + t.Fatal("unexpected success") + } + } + + // Since this test is not running in parallel, we don't expect any new file + // descriptors to be opened while it runs. However, if there are additional + // FDs present at the start of the test (for example, opened by libc), those + // may be closed due to a timeout of some sort. Allow those to go away, but + // check that no new FDs are added. + for _, fd := range openFDs() { + if !old[fd] { + t.Errorf("leaked file descriptor %v", fd) + } + } +} + +func TestExtraFiles(t *testing.T) { + if testing.Short() { + t.Skipf("skipping test in short mode that would build a helper binary") + } + + if haveUnexpectedFDs { + // The point of this test is to make sure that any + // descriptors we open are marked close-on-exec. + // If haveUnexpectedFDs is true then there were other + // descriptors open when we started the test, + // so those descriptors are clearly not close-on-exec, + // and they will confuse the test. We could modify + // the test to expect those descriptors to remain open, + // but since we don't know where they came from or what + // they are doing, that seems fragile. For example, + // perhaps they are from the startup code on this + // system for some reason. Also, this test is not + // system-specific; as long as most systems do not skip + // the test, we will still be testing what we care about. + t.Skip("skipping test because test was run with FDs open") + } + + testenv.MustHaveExec(t) + testenv.MustHaveGoBuild(t) + + // This test runs with cgo disabled. External linking needs cgo, so + // it doesn't work if external linking is required. + testenv.MustInternalLink(t, false) + + if runtime.GOOS == "windows" { + t.Skipf("skipping test on %q", runtime.GOOS) + } + + // Force network usage, to verify the epoll (or whatever) fd + // doesn't leak to the child, + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + + // Make sure duplicated fds don't leak to the child. + f, err := ln.(*net.TCPListener).File() + if err != nil { + t.Fatal(err) + } + defer f.Close() + ln2, err := net.FileListener(f) + if err != nil { + t.Fatal(err) + } + defer ln2.Close() + + // Force TLS root certs to be loaded (which might involve + // cgo), to make sure none of that potential C code leaks fds. + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + // quiet expected TLS handshake error "remote error: bad certificate" + ts.Config.ErrorLog = log.New(io.Discard, "", 0) + ts.StartTLS() + defer ts.Close() + _, err = http.Get(ts.URL) + if err == nil { + t.Errorf("success trying to fetch %s; want an error", ts.URL) + } + + tf, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("TempFile: %v", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + const text = "Hello, fd 3!" + _, err = tf.Write([]byte(text)) + if err != nil { + t.Fatalf("Write: %v", err) + } + _, err = tf.Seek(0, io.SeekStart) + if err != nil { + t.Fatalf("Seek: %v", err) + } + + tempdir := t.TempDir() + exe := filepath.Join(tempdir, "read3.exe") + + c := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, "read3.go") + // Build the test without cgo, so that C library functions don't + // open descriptors unexpectedly. See issue 25628. + c.Env = append(os.Environ(), "CGO_ENABLED=0") + if output, err := c.CombinedOutput(); err != nil { + t.Logf("go build -o %s read3.go\n%s", exe, output) + t.Fatalf("go build failed: %v", err) + } + + // Use a deadline to try to get some output even if the program hangs. + ctx := context.Background() + if deadline, ok := t.Deadline(); ok { + // Leave a 20% grace period to flush output, which may be large on the + // linux/386 builders because we're running the subprocess under strace. + deadline = deadline.Add(-time.Until(deadline) / 5) + + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, deadline) + defer cancel() + } + + c = exec.CommandContext(ctx, exe) + var stdout, stderr strings.Builder + c.Stdout = &stdout + c.Stderr = &stderr + c.ExtraFiles = []*os.File{tf} + if runtime.GOOS == "illumos" { + // Some facilities in illumos are implemented via access + // to /proc by libc; such accesses can briefly occupy a + // low-numbered fd. If this occurs concurrently with the + // test that checks for leaked descriptors, the check can + // become confused and report a spurious leaked descriptor. + // (See issue #42431 for more detailed analysis.) + // + // Attempt to constrain the use of additional threads in the + // child process to make this test less flaky: + c.Env = append(os.Environ(), "GOMAXPROCS=1") + } + err = c.Run() + if err != nil { + t.Fatalf("Run: %v\n--- stdout:\n%s--- stderr:\n%s", err, stdout.String(), stderr.String()) + } + if stdout.String() != text { + t.Errorf("got stdout %q, stderr %q; want %q on stdout", stdout.String(), stderr.String(), text) + } +} + +func TestExtraFilesRace(t *testing.T) { + if runtime.GOOS == "windows" { + maySkipHelperCommand("describefiles") + t.Skip("no operating system support; skipping") + } + t.Parallel() + + listen := func() net.Listener { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + return ln + } + listenerFile := func(ln net.Listener) *os.File { + f, err := ln.(*net.TCPListener).File() + if err != nil { + t.Fatal(err) + } + return f + } + runCommand := func(c *exec.Cmd, out chan<- string) { + bout, err := c.CombinedOutput() + if err != nil { + out <- "ERROR:" + err.Error() + } else { + out <- string(bout) + } + } + + for i := 0; i < 10; i++ { + if testing.Short() && i >= 3 { + break + } + la := listen() + ca := helperCommand(t, "describefiles") + ca.ExtraFiles = []*os.File{listenerFile(la)} + lb := listen() + cb := helperCommand(t, "describefiles") + cb.ExtraFiles = []*os.File{listenerFile(lb)} + ares := make(chan string) + bres := make(chan string) + go runCommand(ca, ares) + go runCommand(cb, bres) + if got, want := <-ares, fmt.Sprintf("fd3: listener %s\n", la.Addr()); got != want { + t.Errorf("iteration %d, process A got:\n%s\nwant:\n%s\n", i, got, want) + } + if got, want := <-bres, fmt.Sprintf("fd3: listener %s\n", lb.Addr()); got != want { + t.Errorf("iteration %d, process B got:\n%s\nwant:\n%s\n", i, got, want) + } + la.Close() + lb.Close() + for _, f := range ca.ExtraFiles { + f.Close() + } + for _, f := range cb.ExtraFiles { + f.Close() + } + } +} + +type delayedInfiniteReader struct{} + +func (delayedInfiniteReader) Read(b []byte) (int, error) { + time.Sleep(100 * time.Millisecond) + for i := range b { + b[i] = 'x' + } + return len(b), nil +} + +// Issue 9173: ignore stdin pipe writes if the program completes successfully. +func TestIgnorePipeErrorOnSuccess(t *testing.T) { + t.Parallel() + + testWith := func(r io.Reader) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo", "foo") + var out strings.Builder + cmd.Stdin = r + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + if got, want := out.String(), "foo\n"; got != want { + t.Errorf("output = %q; want %q", got, want) + } + } + } + t.Run("10MB", testWith(strings.NewReader(strings.Repeat("x", 10<<20)))) + t.Run("Infinite", testWith(delayedInfiniteReader{})) +} + +type badWriter struct{} + +func (w *badWriter) Write(data []byte) (int, error) { + return 0, io.ErrUnexpectedEOF +} + +func TestClosePipeOnCopyError(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "yes") + cmd.Stdout = new(badWriter) + err := cmd.Run() + if err == nil { + t.Errorf("yes unexpectedly completed successfully") + } +} + +func TestOutputStderrCapture(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "stderrfail") + _, err := cmd.Output() + ee, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("Output error type = %T; want ExitError", err) + } + got := string(ee.Stderr) + want := "some stderr text\n" + if got != want { + t.Errorf("ExitError.Stderr = %q; want %q", got, want) + } +} + +func TestContext(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + c := helperCommandContext(t, ctx, "pipetest") + stdin, err := c.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := c.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := c.Start(); err != nil { + t.Fatal(err) + } + + if _, err := stdin.Write([]byte("O:hi\n")); err != nil { + t.Fatal(err) + } + buf := make([]byte, 5) + n, err := io.ReadFull(stdout, buf) + if n != len(buf) || err != nil || string(buf) != "O:hi\n" { + t.Fatalf("ReadFull = %d, %v, %q", n, err, buf[:n]) + } + go cancel() + + if err := c.Wait(); err == nil { + t.Fatal("expected Wait failure") + } +} + +func TestContextCancel(t *testing.T) { + if runtime.GOOS == "netbsd" && runtime.GOARCH == "arm64" { + maySkipHelperCommand("cat") + testenv.SkipFlaky(t, 42061) + } + + // To reduce noise in the final goroutine dump, + // let other parallel tests complete if possible. + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + c := helperCommandContext(t, ctx, "cat") + + stdin, err := c.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + if err := c.Start(); err != nil { + t.Fatal(err) + } + + // At this point the process is alive. Ensure it by sending data to stdin. + if _, err := io.WriteString(stdin, "echo"); err != nil { + t.Fatal(err) + } + + cancel() + + // Calling cancel should have killed the process, so writes + // should now fail. Give the process a little while to die. + start := time.Now() + delay := 1 * time.Millisecond + for { + if _, err := io.WriteString(stdin, "echo"); err != nil { + break + } + + if time.Since(start) > time.Minute { + // Panic instead of calling t.Fatal so that we get a goroutine dump. + // We want to know exactly what the os/exec goroutines got stuck on. + debug.SetTraceback("system") + panic("canceling context did not stop program") + } + + // Back off exponentially (up to 1-second sleeps) to give the OS time to + // terminate the process. + delay *= 2 + if delay > 1*time.Second { + delay = 1 * time.Second + } + time.Sleep(delay) + } + + if err := c.Wait(); err == nil { + t.Error("program unexpectedly exited successfully") + } else { + t.Logf("exit status: %v", err) + } +} + +// test that environment variables are de-duped. +func TestDedupEnvEcho(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echoenv", "FOO") + cmd.Env = append(cmd.Environ(), "FOO=bad", "FOO=good") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if got, want := strings.TrimSpace(string(out)), "good"; got != want { + t.Errorf("output = %q; want %q", got, want) + } +} + +func TestEnvNULCharacter(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("plan9 explicitly allows NUL in the environment") + } + cmd := helperCommand(t, "echoenv", "FOO", "BAR") + cmd.Env = append(cmd.Environ(), "FOO=foo\x00BAR=bar") + out, err := cmd.CombinedOutput() + if err == nil { + t.Errorf("output = %q; want error", string(out)) + } +} + +func TestString(t *testing.T) { + t.Parallel() + + echoPath, err := exec.LookPath("echo") + if err != nil { + t.Skip(err) + } + tests := [...]struct { + path string + args []string + want string + }{ + {"echo", nil, echoPath}, + {"echo", []string{"a"}, echoPath + " a"}, + {"echo", []string{"a", "b"}, echoPath + " a b"}, + } + for _, test := range tests { + cmd := exec.Command(test.path, test.args...) + if got := cmd.String(); got != test.want { + t.Errorf("String(%q, %q) = %q, want %q", test.path, test.args, got, test.want) + } + } +} + +func TestStringPathNotResolved(t *testing.T) { + t.Parallel() + + _, err := exec.LookPath("makemeasandwich") + if err == nil { + t.Skip("wow, thanks") + } + + cmd := exec.Command("makemeasandwich", "-lettuce") + want := "makemeasandwich -lettuce" + if got := cmd.String(); got != want { + t.Errorf("String(%q, %q) = %q, want %q", "makemeasandwich", "-lettuce", got, want) + } +} + +func TestNoPath(t *testing.T) { + err := new(exec.Cmd).Start() + want := "exec: no command" + if err == nil || err.Error() != want { + t.Errorf("new(Cmd).Start() = %v, want %q", err, want) + } +} + +// TestDoubleStartLeavesPipesOpen checks for a regression in which calling +// Start twice, which returns an error on the second call, would spuriously +// close the pipes established in the first call. +func TestDoubleStartLeavesPipesOpen(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pipetest") + in, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := cmd.Wait(); err != nil { + t.Error(err) + } + }) + + if err := cmd.Start(); err == nil || !strings.HasSuffix(err.Error(), "already started") { + t.Fatalf("second call to Start returned a nil; want an 'already started' error") + } + + outc := make(chan []byte, 1) + go func() { + b, err := io.ReadAll(out) + if err != nil { + t.Error(err) + } + outc <- b + }() + + const msg = "O:Hello, pipe!\n" + + _, err = io.WriteString(in, msg) + if err != nil { + t.Fatal(err) + } + in.Close() + + b := <-outc + if !bytes.Equal(b, []byte(msg)) { + t.Fatalf("read %q from stdout pipe; want %q", b, msg) + } +} + +func cmdHang(args ...string) { + sleep, err := time.ParseDuration(args[0]) + if err != nil { + panic(err) + } + + fs := flag.NewFlagSet("hang", flag.ExitOnError) + exitOnInterrupt := fs.Bool("interrupt", false, "if true, commands should exit 0 on os.Interrupt") + subsleep := fs.Duration("subsleep", 0, "amount of time for the 'hang' helper to leave an orphaned subprocess sleeping with stderr open") + probe := fs.Duration("probe", 0, "if nonzero, the 'hang' helper should write to stderr at this interval, and exit nonzero if a write fails") + read := fs.Bool("read", false, "if true, the 'hang' helper should read stdin to completion before sleeping") + fs.Parse(args[1:]) + + pid := os.Getpid() + + if *subsleep != 0 { + cmd := exec.Command(exePath(nil), "hang", subsleep.String(), "-read=true", "-probe="+probe.String()) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + out, err := cmd.StdoutPipe() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + cmd.Start() + + buf := new(strings.Builder) + if _, err := io.Copy(buf, out); err != nil { + fmt.Fprintln(os.Stderr, err) + cmd.Process.Kill() + cmd.Wait() + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "%d: started %d: %v\n", pid, cmd.Process.Pid, cmd) + go cmd.Wait() // Release resources if cmd happens not to outlive this process. + } + + if *exitOnInterrupt { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + sig := <-c + fmt.Fprintf(os.Stderr, "%d: received %v\n", pid, sig) + os.Exit(0) + }() + } else { + signal.Ignore(os.Interrupt) + } + + // Signal that the process is set up by closing stdout. + os.Stdout.Close() + + if *read { + if pipeSignal != nil { + signal.Ignore(pipeSignal) + } + r := bufio.NewReader(os.Stdin) + for { + line, err := r.ReadBytes('\n') + if len(line) > 0 { + // Ignore write errors: we want to keep reading even if stderr is closed. + fmt.Fprintf(os.Stderr, "%d: read %s", pid, line) + } + if err != nil { + fmt.Fprintf(os.Stderr, "%d: finished read: %v", pid, err) + break + } + } + } + + if *probe != 0 { + ticker := time.NewTicker(*probe) + go func() { + for range ticker.C { + if _, err := fmt.Fprintf(os.Stderr, "%d: ok\n", pid); err != nil { + os.Exit(1) + } + } + }() + } + + if sleep != 0 { + time.Sleep(sleep) + fmt.Fprintf(os.Stderr, "%d: slept %v\n", pid, sleep) + } +} + +// A tickReader reads an unbounded sequence of timestamps at no more than a +// fixed interval. +type tickReader struct { + interval time.Duration + lastTick time.Time + s string +} + +func newTickReader(interval time.Duration) *tickReader { + return &tickReader{interval: interval} +} + +func (r *tickReader) Read(p []byte) (n int, err error) { + if len(r.s) == 0 { + if d := r.interval - time.Since(r.lastTick); d > 0 { + time.Sleep(d) + } + r.lastTick = time.Now() + r.s = r.lastTick.Format(time.RFC3339Nano + "\n") + } + + n = copy(p, r.s) + r.s = r.s[n:] + return n, nil +} + +func startHang(t *testing.T, ctx context.Context, hangTime time.Duration, interrupt os.Signal, waitDelay time.Duration, flags ...string) *exec.Cmd { + t.Helper() + + args := append([]string{hangTime.String()}, flags...) + cmd := helperCommandContext(t, ctx, "hang", args...) + cmd.Stdin = newTickReader(1 * time.Millisecond) + cmd.Stderr = new(strings.Builder) + if interrupt == nil { + cmd.Cancel = nil + } else { + cmd.Cancel = func() error { + return cmd.Process.Signal(interrupt) + } + } + cmd.WaitDelay = waitDelay + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + t.Log(cmd) + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // Wait for cmd to close stdout to signal that its handlers are installed. + buf := new(strings.Builder) + if _, err := io.Copy(buf, out); err != nil { + t.Error(err) + cmd.Process.Kill() + cmd.Wait() + t.FailNow() + } + if buf.Len() > 0 { + t.Logf("stdout %v:\n%s", cmd.Args, buf) + } + + return cmd +} + +func TestWaitInterrupt(t *testing.T) { + t.Parallel() + + // tooLong is an arbitrary duration that is expected to be much longer than + // the test runs, but short enough that leaked processes will eventually exit + // on their own. + const tooLong = 10 * time.Minute + + // Control case: with no cancellation and no WaitDelay, we should wait for the + // process to exit. + t.Run("Wait", func(t *testing.T) { + t.Parallel() + cmd := startHang(t, context.Background(), 1*time.Millisecond, os.Kill, 0) + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if err != nil { + t.Errorf("Wait: %v; want <nil>", err) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // With a very long WaitDelay and no Cancel function, we should wait for the + // process to exit even if the command's Context is cancelled. + t.Run("WaitDelay", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, nil, tooLong, "-interrupt=true") + cancel() + + time.Sleep(1 * time.Millisecond) + // At this point cmd should still be running (because we passed nil to + // startHang for the cancel signal). Sending it an explicit Interrupt signal + // should succeed. + if err := cmd.Process.Signal(os.Interrupt); err != nil { + t.Error(err) + } + + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This program exits with status 0, + // but pretty much always does so during the wait delay. + // Since the Cmd itself didn't do anything to stop the process when the + // context expired, a successful exit is valid (even if late) and does + // not merit a non-nil error. + if err != nil { + t.Errorf("Wait: %v; want nil", err) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // If the context is cancelled and the Cancel function sends os.Kill, + // the process should be terminated immediately, and its output + // pipes should be closed (causing Wait to return) after WaitDelay + // even if a child process is still writing to them. + t.Run("SIGKILL-hang", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Kill, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This test should kill the child process after 10ms, + // leaving a grandchild process writing probes in a loop. + // The child process should be reported as failed, + // and the grandchild will exit (or die by SIGPIPE) once the + // stderr pipe is closed. + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If the process exits with status 0 but leaves a child behind writing + // to its output pipes, Wait should only wait for WaitDelay before + // closing the pipes and returning. Wait should return ErrWaitDelay + // to indicate that the piped output may be incomplete even though the + // command returned a “success” code. + t.Run("Exit-hang", func(t *testing.T) { + t.Parallel() + + cmd := startHang(t, context.Background(), 1*time.Millisecond, nil, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms") + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This child process should exit immediately, + // leaving a grandchild process writing probes in a loop. + // Since the child has no ExitError to report but we did not + // read all of its output, Wait should return ErrWaitDelay. + if !errors.Is(err, exec.ErrWaitDelay) { + t.Errorf("Wait error = %v; want %T", err, exec.ErrWaitDelay) + } + }) + + // If the Cancel function sends a signal that the process can handle, and it + // handles that signal without actually exiting, then it should be terminated + // after the WaitDelay. + t.Run("SIGINT-ignored", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Interrupt, 10*time.Millisecond, "-interrupt=false") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This command ignores SIGINT, sleeping until it is killed. + // Wait should return the usual error for a killed process. + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If the process handles the cancellation signal and exits with status 0, + // Wait should report a non-nil error (because the process had to be + // interrupted), and it should be a context error (because there is no error + // to report from the child process itself). + t.Run("SIGINT-handled", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Interrupt, 0, "-interrupt=true") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if !errors.Is(err, ctx.Err()) { + t.Errorf("Wait error = %v; want %v", err, ctx.Err()) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // If the Cancel function sends SIGQUIT, it should be handled in the usual + // way: a Go program should dump its goroutines and exit with non-success + // status. (We expect SIGQUIT to be a common pattern in real-world use.) + t.Run("SIGQUIT", func(t *testing.T) { + if quitSignal == nil { + t.Skipf("skipping: SIGQUIT is not supported on %v", runtime.GOOS) + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, quitSignal, 0) + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %v", err, ctx.Err()) + } + + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 2 { + // The default os/signal handler exits with code 2. + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 2", code) + } + + if !strings.Contains(fmt.Sprint(cmd.Stderr), "\n\ngoroutine ") { + t.Errorf("cmd.Stderr does not contain a goroutine dump") + } + }) +} + +func TestCancelErrors(t *testing.T) { + t.Parallel() + + // If Cancel returns a non-ErrProcessDone error and the process + // exits successfully, Wait should wrap the error from Cancel. + t.Run("success after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + + errArbitrary := errors.New("arbitrary error") + cmd.Cancel = func() error { + stdin.Close() + t.Logf("Cancel returning %v", errArbitrary) + return errArbitrary + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + if !errors.Is(err, errArbitrary) || err == errArbitrary { + t.Errorf("Wait error = %v; want an error wrapping %v", err, errArbitrary) + } + }) + + // If Cancel returns an error equivalent to ErrProcessDone, + // Wait should ignore that error. (ErrProcessDone indicates that the + // process was already done before we tried to interrupt it — maybe we + // just didn't notice because Wait hadn't been called yet.) + t.Run("success after ErrProcessDone", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + // We intentionally race Cancel against the process exiting, + // but ensure that the process wins the race (and return ErrProcessDone + // from Cancel to report that). + interruptCalled := make(chan struct{}) + done := make(chan struct{}) + cmd.Cancel = func() error { + close(interruptCalled) + <-done + t.Logf("Cancel returning an error wrapping ErrProcessDone") + return fmt.Errorf("%w: stdout closed", os.ErrProcessDone) + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + cancel() + <-interruptCalled + stdin.Close() + io.Copy(io.Discard, stdout) // reaches EOF when the process exits + close(done) + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + if err != nil { + t.Errorf("Wait error = %v; want nil", err) + } + }) + + // If Cancel returns an error and the process is killed after + // WaitDelay, Wait should report the usual SIGKILL ExitError, not the + // error from Cancel. + t.Run("killed after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + errArbitrary := errors.New("arbitrary error") + var interruptCalled atomic.Bool + cmd.Cancel = func() error { + t.Logf("Cancel called") + interruptCalled.Store(true) + return errArbitrary + } + cmd.WaitDelay = 1 * time.Millisecond + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // Ensure that Cancel actually had the opportunity to + // return the error. + if !interruptCalled.Load() { + t.Errorf("Cancel was not called when the context was canceled") + } + + // This test should kill the child process after 1ms, + // To maximize compatibility with existing uses of exec.CommandContext, the + // resulting error should be an exec.ExitError without additional wrapping. + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If Cancel returns ErrProcessDone but the process is not actually done + // (and has to be killed), Wait should report the usual SIGKILL ExitError, + // not the error from Cancel. + t.Run("killed after spurious ErrProcessDone", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + var interruptCalled atomic.Bool + cmd.Cancel = func() error { + t.Logf("Cancel returning an error wrapping ErrProcessDone") + interruptCalled.Store(true) + return fmt.Errorf("%w: stdout closed", os.ErrProcessDone) + } + cmd.WaitDelay = 1 * time.Millisecond + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // Ensure that Cancel actually had the opportunity to + // return the error. + if !interruptCalled.Load() { + t.Errorf("Cancel was not called when the context was canceled") + } + + // This test should kill the child process after 1ms, + // To maximize compatibility with existing uses of exec.CommandContext, the + // resulting error should be an exec.ExitError without additional wrapping. + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("Wait error of type %T; want %T", err, ee) + } + }) + + // If Cancel returns an error and the process exits with an + // unsuccessful exit code, the process error should take precedence over the + // Cancel error. + t.Run("nonzero exit after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "stderrfail") + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + + errArbitrary := errors.New("arbitrary error") + interrupted := make(chan struct{}) + cmd.Cancel = func() error { + close(interrupted) + return errArbitrary + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + <-interrupted + io.Copy(io.Discard, stderr) + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if ee, ok := err.(*exec.ExitError); !ok || ee.ProcessState.ExitCode() != 1 { + t.Errorf("Wait error = %v; want exit status 1", err) + } + }) +} + +// TestConcurrentExec is a regression test for https://go.dev/issue/61080. +// +// Forking multiple child processes concurrently would sometimes hang on darwin. +// (This test hung on a gomote with -count=100 after only a few iterations.) +func TestConcurrentExec(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + // This test will spawn nHangs subprocesses that hang reading from stdin, + // and nExits subprocesses that exit immediately. + // + // When issue #61080 was present, a long-lived "hang" subprocess would + // occasionally inherit the fork/exec status pipe from an "exit" subprocess, + // causing the parent process (which expects to see an EOF on that pipe almost + // immediately) to unexpectedly block on reading from the pipe. + var ( + nHangs = runtime.GOMAXPROCS(0) + nExits = runtime.GOMAXPROCS(0) + hangs, exits sync.WaitGroup + ) + hangs.Add(nHangs) + exits.Add(nExits) + + // ready is done when the goroutines have done as much work as possible to + // prepare to create subprocesses. It isn't strictly necessary for the test, + // but helps to increase the repro rate by making it more likely that calls to + // syscall.StartProcess for the "hang" and "exit" goroutines overlap. + var ready sync.WaitGroup + ready.Add(nHangs + nExits) + + for i := 0; i < nHangs; i++ { + go func() { + defer hangs.Done() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + ready.Done() + t.Error(err) + return + } + cmd.Cancel = stdin.Close + ready.Done() + + ready.Wait() + if err := cmd.Start(); err != nil { + if !errors.Is(err, context.Canceled) { + t.Error(err) + } + return + } + + cmd.Wait() + }() + } + + for i := 0; i < nExits; i++ { + go func() { + defer exits.Done() + + cmd := helperCommandContext(t, ctx, "exit", "0") + ready.Done() + + ready.Wait() + if err := cmd.Run(); err != nil { + t.Error(err) + } + }() + } + + exits.Wait() + cancel() + hangs.Wait() +} + +// TestPathRace tests that [Cmd.String] can be called concurrently +// with [Cmd.Start]. +func TestPathRace(t *testing.T) { + cmd := helperCommand(t, "exit", "0") + + done := make(chan struct{}) + go func() { + out, err := cmd.CombinedOutput() + t.Logf("%v: %v\n%s", cmd, err, out) + close(done) + }() + + t.Logf("running in background: %v", cmd) + <-done +} diff --git a/src/os/exec/exec_unix.go b/src/os/exec/exec_unix.go new file mode 100644 index 0000000..3ed672a --- /dev/null +++ b/src/os/exec/exec_unix.go @@ -0,0 +1,24 @@ +// Copyright 2015 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 + +package exec + +import ( + "io/fs" + "syscall" +) + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore EPIPE errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 9173. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err == syscall.EPIPE +} diff --git a/src/os/exec/exec_unix_test.go b/src/os/exec/exec_unix_test.go new file mode 100644 index 0000000..d26c93a --- /dev/null +++ b/src/os/exec/exec_unix_test.go @@ -0,0 +1,17 @@ +// Copyright 2022 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 unix + +package exec_test + +import ( + "os" + "syscall" +) + +var ( + quitSignal os.Signal = syscall.SIGQUIT + pipeSignal os.Signal = syscall.SIGPIPE +) diff --git a/src/os/exec/exec_windows.go b/src/os/exec/exec_windows.go new file mode 100644 index 0000000..e7a2ee6 --- /dev/null +++ b/src/os/exec/exec_windows.go @@ -0,0 +1,23 @@ +// Copyright 2017 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. + +package exec + +import ( + "io/fs" + "syscall" +) + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore ERROR_BROKEN_PIPE and ERROR_NO_DATA errors copying + // to stdin if the program completed successfully otherwise. + // See Issue 20445. + const _ERROR_NO_DATA = syscall.Errno(0xe8) + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + (pe.Err == syscall.ERROR_BROKEN_PIPE || pe.Err == _ERROR_NO_DATA) +} diff --git a/src/os/exec/exec_windows_test.go b/src/os/exec/exec_windows_test.go new file mode 100644 index 0000000..efd3710 --- /dev/null +++ b/src/os/exec/exec_windows_test.go @@ -0,0 +1,109 @@ +// Copyright 2021 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 windows + +package exec_test + +import ( + "fmt" + "internal/testenv" + "io" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + "testing" +) + +var ( + quitSignal os.Signal = nil + pipeSignal os.Signal = syscall.SIGPIPE +) + +func init() { + registerHelperCommand("pipehandle", cmdPipeHandle) +} + +func cmdPipeHandle(args ...string) { + handle, _ := strconv.ParseUint(args[0], 16, 64) + pipe := os.NewFile(uintptr(handle), "") + _, err := fmt.Fprint(pipe, args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "writing to pipe failed: %v\n", err) + os.Exit(1) + } + pipe.Close() +} + +func TestPipePassing(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Error(err) + } + const marker = "arrakis, dune, desert planet" + childProc := helperCommand(t, "pipehandle", strconv.FormatUint(uint64(w.Fd()), 16), marker) + childProc.SysProcAttr = &syscall.SysProcAttr{AdditionalInheritedHandles: []syscall.Handle{syscall.Handle(w.Fd())}} + err = childProc.Start() + if err != nil { + t.Error(err) + } + w.Close() + response, err := io.ReadAll(r) + if err != nil { + t.Error(err) + } + r.Close() + if string(response) != marker { + t.Errorf("got %q; want %q", string(response), marker) + } + err = childProc.Wait() + if err != nil { + t.Error(err) + } +} + +func TestNoInheritHandles(t *testing.T) { + t.Parallel() + + cmd := testenv.Command(t, "cmd", "/c exit 88") + cmd.SysProcAttr = &syscall.SysProcAttr{NoInheritHandles: true} + err := cmd.Run() + exitError, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("got error %v; want ExitError", err) + } + if exitError.ExitCode() != 88 { + t.Fatalf("got exit code %d; want 88", exitError.ExitCode()) + } +} + +// start a child process without the user code explicitly starting +// with a copy of the parent's SYSTEMROOT. +// (See issue 25210.) +func TestChildCriticalEnv(t *testing.T) { + t.Parallel() + cmd := helperCommand(t, "echoenv", "SYSTEMROOT") + + // Explicitly remove SYSTEMROOT from the command's environment. + var env []string + for _, kv := range cmd.Environ() { + k, _, ok := strings.Cut(kv, "=") + if !ok || !strings.EqualFold(k, "SYSTEMROOT") { + env = append(env, kv) + } + } + cmd.Env = env + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(out)) == "" { + t.Error("no SYSTEMROOT found") + } +} diff --git a/src/os/exec/internal/fdtest/exists_plan9.go b/src/os/exec/internal/fdtest/exists_plan9.go new file mode 100644 index 0000000..8886e06 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_plan9.go @@ -0,0 +1,20 @@ +// Copyright 2021 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 + +package fdtest + +import ( + "syscall" +) + +const errBadFd = syscall.ErrorString("fd out of range or not open") + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var buf [1]byte + _, err := syscall.Fstat(int(fd), buf[:]) + return err != errBadFd +} diff --git a/src/os/exec/internal/fdtest/exists_test.go b/src/os/exec/internal/fdtest/exists_test.go new file mode 100644 index 0000000..a02dddf --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_test.go @@ -0,0 +1,21 @@ +// Copyright 2021 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. + +package fdtest + +import ( + "os" + "runtime" + "testing" +) + +func TestExists(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Exists not implemented for windows") + } + + if !Exists(os.Stdout.Fd()) { + t.Errorf("Exists(%d) got false want true", os.Stdout.Fd()) + } +} diff --git a/src/os/exec/internal/fdtest/exists_unix.go b/src/os/exec/internal/fdtest/exists_unix.go new file mode 100644 index 0000000..472a802 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_unix.go @@ -0,0 +1,19 @@ +// Copyright 2021 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 unix || wasm + +// Package fdtest provides test helpers for working with file descriptors across exec. +package fdtest + +import ( + "syscall" +) + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var s syscall.Stat_t + err := syscall.Fstat(int(fd), &s) + return err != syscall.EBADF +} diff --git a/src/os/exec/internal/fdtest/exists_windows.go b/src/os/exec/internal/fdtest/exists_windows.go new file mode 100644 index 0000000..72b8ccf --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_windows.go @@ -0,0 +1,12 @@ +// Copyright 2021 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 windows + +package fdtest + +// Exists is not implemented on windows and panics. +func Exists(fd uintptr) bool { + panic("unimplemented") +} diff --git a/src/os/exec/internal_test.go b/src/os/exec/internal_test.go new file mode 100644 index 0000000..68d517f --- /dev/null +++ b/src/os/exec/internal_test.go @@ -0,0 +1,61 @@ +// Copyright 2015 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. + +package exec + +import ( + "io" + "testing" +) + +func TestPrefixSuffixSaver(t *testing.T) { + tests := []struct { + N int + writes []string + want string + }{ + { + N: 2, + writes: nil, + want: "", + }, + { + N: 2, + writes: []string{"a"}, + want: "a", + }, + { + N: 2, + writes: []string{"abc", "d"}, + want: "abcd", + }, + { + N: 2, + writes: []string{"abc", "d", "e"}, + want: "ab\n... omitting 1 bytes ...\nde", + }, + { + N: 2, + writes: []string{"ab______________________yz"}, + want: "ab\n... omitting 22 bytes ...\nyz", + }, + { + N: 2, + writes: []string{"ab_______________________y", "z"}, + want: "ab\n... omitting 23 bytes ...\nyz", + }, + } + for i, tt := range tests { + w := &prefixSuffixSaver{N: tt.N} + for _, s := range tt.writes { + n, err := io.WriteString(w, s) + if err != nil || n != len(s) { + t.Errorf("%d. WriteString(%q) = %v, %v; want %v, %v", i, s, n, err, len(s), nil) + } + } + if got := string(w.Bytes()); got != tt.want { + t.Errorf("%d. Bytes = %q; want %q", i, got, tt.want) + } + } +} diff --git a/src/os/exec/lp_linux_test.go b/src/os/exec/lp_linux_test.go new file mode 100644 index 0000000..a7f9aa2 --- /dev/null +++ b/src/os/exec/lp_linux_test.go @@ -0,0 +1,88 @@ +// Copyright 2022 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. + +package exec_test + +import ( + "errors" + "internal/syscall/unix" + "internal/testenv" + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" +) + +func TestFindExecutableVsNoexec(t *testing.T) { + t.Parallel() + + // This test case relies on faccessat2(2) syscall, which appeared in Linux v5.8. + if major, minor := unix.KernelVersion(); major < 5 || (major == 5 && minor < 8) { + t.Skip("requires Linux kernel v5.8 with faccessat2(2) syscall") + } + + tmp := t.TempDir() + + // Create a tmpfs mount. + err := syscall.Mount("tmpfs", tmp, "tmpfs", 0, "") + if testenv.SyscallIsNotSupported(err) { + // Usually this means lack of CAP_SYS_ADMIN, but there might be + // other reasons, especially in restricted test environments. + t.Skipf("requires ability to mount tmpfs (%v)", err) + } else if err != nil { + t.Fatalf("mount %s failed: %v", tmp, err) + } + t.Cleanup(func() { + if err := syscall.Unmount(tmp, 0); err != nil { + t.Error(err) + } + }) + + // Create an executable. + path := filepath.Join(tmp, "program") + err = os.WriteFile(path, []byte("#!/bin/sh\necho 123\n"), 0o755) + if err != nil { + t.Fatal(err) + } + + // Check that it works as expected. + _, err = exec.LookPath(path) + if err != nil { + t.Fatalf("LookPath: got %v, want nil", err) + } + + for { + err = exec.Command(path).Run() + if err == nil { + break + } + if errors.Is(err, syscall.ETXTBSY) { + // A fork+exec in another process may be holding open the FD that we used + // to write the executable (see https://go.dev/issue/22315). + // Since the descriptor should have CLOEXEC set, the problem should resolve + // as soon as the forked child reaches its exec call. + // Keep retrying until that happens. + } else { + t.Fatalf("exec: got %v, want nil", err) + } + } + + // Remount with noexec flag. + err = syscall.Mount("", tmp, "", syscall.MS_REMOUNT|syscall.MS_NOEXEC, "") + if testenv.SyscallIsNotSupported(err) { + t.Skipf("requires ability to re-mount tmpfs (%v)", err) + } else if err != nil { + t.Fatalf("remount %s with noexec failed: %v", tmp, err) + } + + if err := exec.Command(path).Run(); err == nil { + t.Fatal("exec on noexec filesystem: got nil, want error") + } + + _, err = exec.LookPath(path) + if err == nil { + t.Fatalf("LookPath: got nil, want error") + } +} diff --git a/src/os/exec/lp_plan9.go b/src/os/exec/lp_plan9.go new file mode 100644 index 0000000..dffdbac --- /dev/null +++ b/src/os/exec/lp_plan9.go @@ -0,0 +1,72 @@ +// Copyright 2011 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. + +package exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $path") + +func findExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if m := d.Mode(); !m.IsDir() && m&0111 != 0 { + return nil + } + return fs.ErrPermission +} + +// LookPath searches for an executable named file in the +// directories named by the path environment variable. +// If file begins with "/", "#", "./", or "../", it is tried +// directly and the path is not consulted. +// On success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + // skip the path lookup for these prefixes + skip := []string{"/", "#", "./", "../"} + + for _, p := range skip { + if strings.HasPrefix(file, p) { + err := findExecutable(file) + if err == nil { + return file, nil + } + return "", &Error{file, err} + } + } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + path := filepath.Join(dir, file) + if err := findExecutable(path); err == nil { + if !filepath.IsAbs(path) { + if execerrdot.Value() != "0" { + return path, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return path, nil + } + } + return "", &Error{file, ErrNotFound} +} + +// lookExtensions is a no-op on non-Windows platforms, since +// they do not restrict executables to specific extensions. +func lookExtensions(path, dir string) (string, error) { + return path, nil +} diff --git a/src/os/exec/lp_test.go b/src/os/exec/lp_test.go new file mode 100644 index 0000000..77d8e84 --- /dev/null +++ b/src/os/exec/lp_test.go @@ -0,0 +1,33 @@ +// Copyright 2011 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. + +package exec + +import ( + "testing" +) + +var nonExistentPaths = []string{ + "some-non-existent-path", + "non-existent-path/slashed", +} + +func TestLookPathNotFound(t *testing.T) { + for _, name := range nonExistentPaths { + path, err := LookPath(name) + if err == nil { + t.Fatalf("LookPath found %q in $PATH", name) + } + if path != "" { + t.Fatalf("LookPath path == %q when err != nil", path) + } + perr, ok := err.(*Error) + if !ok { + t.Fatal("LookPath error is not an exec.Error") + } + if perr.Name != name { + t.Fatalf("want Error name %q, got %q", name, perr.Name) + } + } +} diff --git a/src/os/exec/lp_unix.go b/src/os/exec/lp_unix.go new file mode 100644 index 0000000..3787132 --- /dev/null +++ b/src/os/exec/lp_unix.go @@ -0,0 +1,88 @@ +// Copyright 2010 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 unix + +package exec + +import ( + "errors" + "internal/syscall/unix" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $PATH") + +func findExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + m := d.Mode() + if m.IsDir() { + return syscall.EISDIR + } + err = unix.Eaccess(file, unix.X_OK) + // ENOSYS means Eaccess is not available or not implemented. + // EPERM can be returned by Linux containers employing seccomp. + // In both cases, fall back to checking the permission bits. + if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) { + return err + } + if m&0111 != 0 { + return nil + } + return fs.ErrPermission +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// Otherwise, on success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + // NOTE(rsc): I wish we could use the Plan 9 behavior here + // (only bypass the path if file begins with / or ./ or ../) + // but that would not match all the Unix shells. + + if strings.Contains(file, "/") { + err := findExecutable(file) + if err == nil { + return file, nil + } + return "", &Error{file, err} + } + path := os.Getenv("PATH") + for _, dir := range filepath.SplitList(path) { + if dir == "" { + // Unix shell semantics: path element "" means "." + dir = "." + } + path := filepath.Join(dir, file) + if err := findExecutable(path); err == nil { + if !filepath.IsAbs(path) { + if execerrdot.Value() != "0" { + return path, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return path, nil + } + } + return "", &Error{file, ErrNotFound} +} + +// lookExtensions is a no-op on non-Windows platforms, since +// they do not restrict executables to specific extensions. +func lookExtensions(path, dir string) (string, error) { + return path, nil +} diff --git a/src/os/exec/lp_unix_test.go b/src/os/exec/lp_unix_test.go new file mode 100644 index 0000000..1503dda --- /dev/null +++ b/src/os/exec/lp_unix_test.go @@ -0,0 +1,39 @@ +// Copyright 2013 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 unix + +package exec_test + +import ( + "os" + "os/exec" + "testing" +) + +func TestLookPathUnixEmptyPath(t *testing.T) { + // Not parallel: uses Chdir and Setenv. + + tmp := t.TempDir() + chdir(t, tmp) + + f, err := os.OpenFile("exec_me", os.O_CREATE|os.O_EXCL, 0700) + if err != nil { + t.Fatal("OpenFile failed: ", err) + } + err = f.Close() + if err != nil { + t.Fatal("Close failed: ", err) + } + + t.Setenv("PATH", "") + + path, err := exec.LookPath("exec_me") + if err == nil { + t.Fatal("LookPath found exec_me in empty $PATH") + } + if path != "" { + t.Fatalf("LookPath path == %q when err != nil", path) + } +} diff --git a/src/os/exec/lp_wasm.go b/src/os/exec/lp_wasm.go new file mode 100644 index 0000000..3c81904 --- /dev/null +++ b/src/os/exec/lp_wasm.go @@ -0,0 +1,29 @@ +// Copyright 2018 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 wasm + +package exec + +import ( + "errors" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $PATH") + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// The result may be an absolute path or a path relative to the current directory. +func LookPath(file string) (string, error) { + // Wasm can not execute processes, so act as if there are no executables at all. + return "", &Error{file, ErrNotFound} +} + +// lookExtensions is a no-op on non-Windows platforms, since +// they do not restrict executables to specific extensions. +func lookExtensions(path, dir string) (string, error) { + return path, nil +} diff --git a/src/os/exec/lp_windows.go b/src/os/exec/lp_windows.go new file mode 100644 index 0000000..698a97c --- /dev/null +++ b/src/os/exec/lp_windows.go @@ -0,0 +1,212 @@ +// Copyright 2010 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. + +package exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in %PATH%") + +func chkStat(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if d.IsDir() { + return fs.ErrPermission + } + return nil +} + +func hasExt(file string) bool { + i := strings.LastIndex(file, ".") + if i < 0 { + return false + } + return strings.LastIndexAny(file, `:\/`) < i +} + +func findExecutable(file string, exts []string) (string, error) { + if len(exts) == 0 { + return file, chkStat(file) + } + if hasExt(file) { + if chkStat(file) == nil { + return file, nil + } + // Keep checking exts below, so that programs with weird names + // like "foo.bat.exe" will resolve instead of failing. + } + for _, e := range exts { + if f := file + e; chkStat(f) == nil { + return f, nil + } + } + if hasExt(file) { + return "", fs.ErrNotExist + } + return "", ErrNotFound +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// LookPath also uses PATHEXT environment variable to match +// a suitable candidate. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// Otherwise, on success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + return lookPath(file, pathExt()) +} + +// lookExtensions finds windows executable by its dir and path. +// It uses LookPath to try appropriate extensions. +// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`. +// +// If the path already has an extension found in PATHEXT, +// lookExtensions returns it directly without searching +// for additional extensions. For example, +// "C:\foo\example.com" would be returned as-is even if the +// program is actually "C:\foo\example.com.exe". +func lookExtensions(path, dir string) (string, error) { + if filepath.Base(path) == path { + path = "." + string(filepath.Separator) + path + } + exts := pathExt() + if ext := filepath.Ext(path); ext != "" { + for _, e := range exts { + if strings.EqualFold(ext, e) { + // Assume that path has already been resolved. + return path, nil + } + } + } + if dir == "" { + return lookPath(path, exts) + } + if filepath.VolumeName(path) != "" { + return lookPath(path, exts) + } + if len(path) > 1 && os.IsPathSeparator(path[0]) { + return lookPath(path, exts) + } + dirandpath := filepath.Join(dir, path) + // We assume that LookPath will only add file extension. + lp, err := lookPath(dirandpath, exts) + if err != nil { + return "", err + } + ext := strings.TrimPrefix(lp, dirandpath) + return path + ext, nil +} + +func pathExt() []string { + var exts []string + x := os.Getenv(`PATHEXT`) + if x != "" { + for _, e := range strings.Split(strings.ToLower(x), `;`) { + if e == "" { + continue + } + if e[0] != '.' { + e = "." + e + } + exts = append(exts, e) + } + } else { + exts = []string{".com", ".exe", ".bat", ".cmd"} + } + return exts +} + +// lookPath implements LookPath for the given PATHEXT list. +func lookPath(file string, exts []string) (string, error) { + if strings.ContainsAny(file, `:\/`) { + f, err := findExecutable(file, exts) + if err == nil { + return f, nil + } + return "", &Error{file, err} + } + + // On Windows, creating the NoDefaultCurrentDirectoryInExePath + // environment variable (with any value or no value!) signals that + // path lookups should skip the current directory. + // In theory we are supposed to call NeedCurrentDirectoryForExePathW + // "as the registry location of this environment variable can change" + // but that seems exceedingly unlikely: it would break all users who + // have configured their environment this way! + // https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw + // See also go.dev/issue/43947. + var ( + dotf string + dotErr error + ) + if _, found := syscall.Getenv("NoDefaultCurrentDirectoryInExePath"); !found { + if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { + if execerrdot.Value() == "0" { + execerrdot.IncNonDefault() + return f, nil + } + dotf, dotErr = f, &Error{file, ErrDot} + } + } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + if dir == "" { + // Skip empty entries, consistent with what PowerShell does. + // (See https://go.dev/issue/61493#issuecomment-1649724826.) + continue + } + + if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { + if dotErr != nil { + // https://go.dev/issue/53536: if we resolved a relative path implicitly, + // and it is the same executable that would be resolved from the explicit %PATH%, + // prefer the explicit name for the executable (and, likely, no error) instead + // of the equivalent implicit name with ErrDot. + // + // Otherwise, return the ErrDot for the implicit path as soon as we find + // out that the explicit one doesn't match. + dotfi, dotfiErr := os.Lstat(dotf) + fi, fiErr := os.Lstat(f) + if dotfiErr != nil || fiErr != nil || !os.SameFile(dotfi, fi) { + return dotf, dotErr + } + } + + if !filepath.IsAbs(f) { + if execerrdot.Value() != "0" { + // If this is the same relative path that we already found, + // dotErr is non-nil and we already checked it above. + // Otherwise, record this path as the one to which we must resolve, + // with or without a dotErr. + if dotErr == nil { + dotf, dotErr = f, &Error{file, ErrDot} + } + continue + } + execerrdot.IncNonDefault() + } + return f, nil + } + } + + if dotErr != nil { + return dotf, dotErr + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_windows_test.go b/src/os/exec/lp_windows_test.go new file mode 100644 index 0000000..a92a297 --- /dev/null +++ b/src/os/exec/lp_windows_test.go @@ -0,0 +1,637 @@ +// Copyright 2013 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. + +// Use an external test to avoid os/exec -> internal/testenv -> os/exec +// circular dependency. + +package exec_test + +import ( + "errors" + "fmt" + "internal/testenv" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "testing" +) + +func init() { + registerHelperCommand("printpath", cmdPrintPath) +} + +func cmdPrintPath(args ...string) { + exe, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "Executable: %v\n", err) + os.Exit(1) + } + fmt.Println(exe) +} + +// makePATH returns a PATH variable referring to the +// given directories relative to a root directory. +// +// The empty string results in an empty entry. +// Paths beginning with . are kept as relative entries. +func makePATH(root string, dirs []string) string { + paths := make([]string, 0, len(dirs)) + for _, d := range dirs { + switch { + case d == "": + paths = append(paths, "") + case d == "." || (len(d) >= 2 && d[0] == '.' && os.IsPathSeparator(d[1])): + paths = append(paths, filepath.Clean(d)) + default: + paths = append(paths, filepath.Join(root, d)) + } + } + return strings.Join(paths, string(os.PathListSeparator)) +} + +// installProgs creates executable files (or symlinks to executable files) at +// multiple destination paths. It uses root as prefix for all destination files. +func installProgs(t *testing.T, root string, files []string) { + for _, f := range files { + dstPath := filepath.Join(root, f) + + dir := filepath.Dir(dstPath) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + + if os.IsPathSeparator(f[len(f)-1]) { + continue // directory and PATH entry only. + } + if strings.EqualFold(filepath.Ext(f), ".bat") { + installBat(t, dstPath) + } else { + installExe(t, dstPath) + } + } +} + +// installExe installs a copy of the test executable +// at the given location, creating directories as needed. +// +// (We use a copy instead of just a symlink to ensure that os.Executable +// always reports an unambiguous path, regardless of how it is implemented.) +func installExe(t *testing.T, dstPath string) { + src, err := os.Open(exePath(t)) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o777) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := dst.Close(); err != nil { + t.Fatal(err) + } + }() + + _, err = io.Copy(dst, src) + if err != nil { + t.Fatal(err) + } +} + +// installBat creates a batch file at dst that prints its own +// path when run. +func installBat(t *testing.T, dstPath string) { + dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o777) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := dst.Close(); err != nil { + t.Fatal(err) + } + }() + + if _, err := fmt.Fprintf(dst, "@echo %s\r\n", dstPath); err != nil { + t.Fatal(err) + } +} + +type lookPathTest struct { + name string + PATHEXT string // empty to use default + files []string + PATH []string // if nil, use all parent directories from files + searchFor string + want string + wantErr error + skipCmdExeCheck bool // if true, do not check want against the behavior of cmd.exe +} + +var lookPathTests = []lookPathTest{ + { + name: "first match", + files: []string{`p1\a.exe`, `p2\a.exe`, `p2\a`}, + searchFor: `a`, + want: `p1\a.exe`, + }, + { + name: "dirs with extensions", + files: []string{`p1.dir\a`, `p2.dir\a.exe`}, + searchFor: `a`, + want: `p2.dir\a.exe`, + }, + { + name: "first with extension", + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + want: `p1\a.exe`, + }, + { + name: "specific name", + files: []string{`p1\a.exe`, `p2\b.exe`}, + searchFor: `b`, + want: `p2\b.exe`, + }, + { + name: "no extension", + files: []string{`p1\b`, `p2\a`}, + searchFor: `a`, + wantErr: exec.ErrNotFound, + }, + { + name: "directory, no extension", + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `p2\a`, + want: `p2\a.exe`, + }, + { + name: "no match", + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `b`, + wantErr: exec.ErrNotFound, + }, + { + name: "no match with dir", + files: []string{`p1\b.exe`, `p2\a.exe`}, + searchFor: `p2\b`, + wantErr: exec.ErrNotFound, + }, + { + name: "extensionless file in CWD ignored", + files: []string{`a`, `p1\a.exe`, `p2\a.exe`}, + searchFor: `a`, + want: `p1\a.exe`, + }, + { + name: "extensionless file in PATH ignored", + files: []string{`p1\a`, `p2\a.exe`}, + searchFor: `a`, + want: `p2\a.exe`, + }, + { + name: "specific extension", + files: []string{`p1\a.exe`, `p2\a.bat`}, + searchFor: `a.bat`, + want: `p2\a.bat`, + }, + { + name: "mismatched extension", + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.com`, + wantErr: exec.ErrNotFound, + }, + { + name: "doubled extension", + files: []string{`p1\a.exe.exe`}, + searchFor: `a.exe`, + want: `p1\a.exe.exe`, + }, + { + name: "extension not in PATHEXT", + PATHEXT: `.COM;.BAT`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + want: `p1\a.exe`, + }, + { + name: "first allowed by PATHEXT", + PATHEXT: `.COM;.EXE`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + want: `p2\a.exe`, + }, + { + name: "first directory containing a PATHEXT match", + PATHEXT: `.COM;.EXE;.BAT`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + want: `p1\a.bat`, + }, + { + name: "first PATHEXT entry", + PATHEXT: `.COM;.EXE;.BAT`, + files: []string{`p1\a.bat`, `p1\a.exe`, `p2\a.bat`, `p2\a.exe`}, + searchFor: `a`, + want: `p1\a.exe`, + }, + { + name: "ignore dir with PATHEXT extension", + files: []string{`a.exe\`}, + searchFor: `a`, + wantErr: exec.ErrNotFound, + }, + { + name: "ignore empty PATH entry", + files: []string{`a.bat`, `p\a.bat`}, + PATH: []string{`p`}, + searchFor: `a`, + want: `p\a.bat`, + // If cmd.exe is too old it might not respect NoDefaultCurrentDirectoryInExePath, + // so skip that check. + skipCmdExeCheck: true, + }, + { + name: "return ErrDot if found by a different absolute path", + files: []string{`p1\a.bat`, `p2\a.bat`}, + PATH: []string{`.\p1`, `p2`}, + searchFor: `a`, + want: `p1\a.bat`, + wantErr: exec.ErrDot, + }, + { + name: "suppress ErrDot if also found in absolute path", + files: []string{`p1\a.bat`, `p2\a.bat`}, + PATH: []string{`.\p1`, `p1`, `p2`}, + searchFor: `a`, + want: `p1\a.bat`, + }, +} + +func TestLookPathWindows(t *testing.T) { + // Not parallel: uses Chdir and Setenv. + + // We are using the "printpath" command mode to test exec.Command here, + // so we won't be calling helperCommand to resolve it. + // That may cause it to appear to be unused. + maySkipHelperCommand("printpath") + + // Before we begin, find the absolute path to cmd.exe. + // In non-short mode, we will use it to check the ground truth + // of the test's "want" field. + cmdExe, err := exec.LookPath("cmd") + if err != nil { + t.Fatal(err) + } + + for _, tt := range lookPathTests { + t.Run(tt.name, func(t *testing.T) { + if tt.want == "" && tt.wantErr == nil { + t.Fatalf("test must specify either want or wantErr") + } + + root := t.TempDir() + installProgs(t, root, tt.files) + + if tt.PATHEXT != "" { + t.Setenv("PATHEXT", tt.PATHEXT) + t.Logf("set PATHEXT=%s", tt.PATHEXT) + } + + var pathVar string + if tt.PATH == nil { + paths := make([]string, 0, len(tt.files)) + for _, f := range tt.files { + dir := filepath.Join(root, filepath.Dir(f)) + if !slices.Contains(paths, dir) { + paths = append(paths, dir) + } + } + pathVar = strings.Join(paths, string(os.PathListSeparator)) + } else { + pathVar = makePATH(root, tt.PATH) + } + t.Setenv("PATH", pathVar) + t.Logf("set PATH=%s", pathVar) + + chdir(t, root) + + if !testing.Short() && !(tt.skipCmdExeCheck || errors.Is(tt.wantErr, exec.ErrDot)) { + // Check that cmd.exe, which is our source of ground truth, + // agrees that our test case is correct. + cmd := testenv.Command(t, cmdExe, "/c", tt.searchFor, "printpath") + out, err := cmd.Output() + if err == nil { + gotAbs := strings.TrimSpace(string(out)) + wantAbs := "" + if tt.want != "" { + wantAbs = filepath.Join(root, tt.want) + } + if gotAbs != wantAbs { + // cmd.exe disagrees. Probably the test case is wrong? + t.Fatalf("%v\n\tresolved to %s\n\twant %s", cmd, gotAbs, wantAbs) + } + } else if tt.wantErr == nil { + if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { + t.Fatalf("%v: %v\n%s", cmd, err, ee.Stderr) + } + t.Fatalf("%v: %v", cmd, err) + } + } + + got, err := exec.LookPath(tt.searchFor) + if filepath.IsAbs(got) { + got, err = filepath.Rel(root, got) + if err != nil { + t.Fatal(err) + } + } + if got != tt.want { + t.Errorf("LookPath(%#q) = %#q; want %#q", tt.searchFor, got, tt.want) + } + if !errors.Is(err, tt.wantErr) { + t.Errorf("LookPath(%#q): %v; want %v", tt.searchFor, err, tt.wantErr) + } + }) + } +} + +type commandTest struct { + name string + PATH []string + files []string + dir string + arg0 string + want string + wantPath string // the resolved c.Path, if different from want + wantErrDot bool + wantRunErr error +} + +var commandTests = []commandTest{ + // testing commands with no slash, like `a.exe` + { + name: "current directory", + files: []string{`a.exe`}, + PATH: []string{"."}, + arg0: `a.exe`, + want: `a.exe`, + wantErrDot: true, + }, + { + name: "with extra PATH", + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2", "p"}, + arg0: `a.exe`, + want: `a.exe`, + wantErrDot: true, + }, + { + name: "with extra PATH and no extension", + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2", "p"}, + arg0: `a`, + want: `a.exe`, + wantErrDot: true, + }, + // testing commands with slash, like `.\a.exe` + { + name: "with dir", + files: []string{`p\a.exe`}, + PATH: []string{"."}, + arg0: `p\a.exe`, + want: `p\a.exe`, + }, + { + name: "with explicit dot", + files: []string{`p\a.exe`}, + PATH: []string{"."}, + arg0: `.\p\a.exe`, + want: `p\a.exe`, + }, + { + name: "with irrelevant PATH", + files: []string{`p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2"}, + arg0: `p\a.exe`, + want: `p\a.exe`, + }, + { + name: "with slash and no extension", + files: []string{`p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2"}, + arg0: `p\a`, + want: `p\a.exe`, + }, + // tests commands, like `a.exe`, with c.Dir set + { + // should not find a.exe in p, because LookPath(`a.exe`) will fail when + // called by Command (before Dir is set), and that error is sticky. + name: "not found before Dir", + files: []string{`p\a.exe`}, + PATH: []string{"."}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + wantRunErr: exec.ErrNotFound, + }, + { + // LookPath(`a.exe`) will resolve to `.\a.exe`, but prefixing that with + // dir `p\a.exe` will refer to a non-existent file + name: "resolved before Dir", + files: []string{`a.exe`, `p\not_important_file`}, + PATH: []string{"."}, + dir: `p`, + arg0: `a.exe`, + want: `a.exe`, + wantErrDot: true, + wantRunErr: fs.ErrNotExist, + }, + { + // like above, but making test succeed by installing file + // in referred destination (so LookPath(`a.exe`) will still + // find `.\a.exe`, but we successfully execute `p\a.exe`) + name: "relative to Dir", + files: []string{`a.exe`, `p\a.exe`}, + PATH: []string{"."}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + wantErrDot: true, + }, + { + // like above, but add PATH in attempt to break the test + name: "relative to Dir with extra PATH", + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2", "p"}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + wantErrDot: true, + }, + { + // like above, but use "a" instead of "a.exe" for command + name: "relative to Dir with extra PATH and no extension", + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2", "p"}, + dir: `p`, + arg0: `a`, + want: `p\a.exe`, + wantErrDot: true, + }, + { + // finds `a.exe` in the PATH regardless of Dir because Command resolves the + // full path (using LookPath) before Dir is set. + name: "from PATH with no match in Dir", + files: []string{`p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2", "p"}, + dir: `p`, + arg0: `a.exe`, + want: `p2\a.exe`, + }, + // tests commands, like `.\a.exe`, with c.Dir set + { + // should use dir when command is path, like ".\a.exe" + name: "relative to Dir with explicit dot", + files: []string{`p\a.exe`}, + PATH: []string{"."}, + dir: `p`, + arg0: `.\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but with PATH added in attempt to break it + name: "relative to Dir with dot and extra PATH", + files: []string{`p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2"}, + dir: `p`, + arg0: `.\a.exe`, + want: `p\a.exe`, + }, + { + // LookPath(".\a") will fail before Dir is set, and that error is sticky. + name: "relative to Dir with dot and extra PATH and no extension", + files: []string{`p\a.exe`, `p2\a.exe`}, + PATH: []string{".", "p2"}, + dir: `p`, + arg0: `.\a`, + want: `p\a.exe`, + }, + { + // LookPath(".\a") will fail before Dir is set, and that error is sticky. + name: "relative to Dir with different extension", + files: []string{`a.exe`, `p\a.bat`}, + PATH: []string{"."}, + dir: `p`, + arg0: `.\a`, + want: `p\a.bat`, + }, +} + +func TestCommand(t *testing.T) { + // Not parallel: uses Chdir and Setenv. + + // We are using the "printpath" command mode to test exec.Command here, + // so we won't be calling helperCommand to resolve it. + // That may cause it to appear to be unused. + maySkipHelperCommand("printpath") + + for _, tt := range commandTests { + t.Run(tt.name, func(t *testing.T) { + if tt.PATH == nil { + t.Fatalf("test must specify PATH") + } + + root := t.TempDir() + installProgs(t, root, tt.files) + + pathVar := makePATH(root, tt.PATH) + t.Setenv("PATH", pathVar) + t.Logf("set PATH=%s", pathVar) + + chdir(t, root) + + cmd := exec.Command(tt.arg0, "printpath") + cmd.Dir = filepath.Join(root, tt.dir) + if tt.wantErrDot { + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } else { + t.Fatalf("cmd.Err = %v; want ErrDot", cmd.Err) + } + } + + out, err := cmd.Output() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { + t.Logf("%v: %v\n%s", cmd, err, ee.Stderr) + } else { + t.Logf("%v: %v", cmd, err) + } + if !errors.Is(err, tt.wantRunErr) { + t.Errorf("want %v", tt.wantRunErr) + } + return + } + + got := strings.TrimSpace(string(out)) + if filepath.IsAbs(got) { + got, err = filepath.Rel(root, got) + if err != nil { + t.Fatal(err) + } + } + if got != tt.want { + t.Errorf("\nran %#q\nwant %#q", got, tt.want) + } + + gotPath := cmd.Path + wantPath := tt.wantPath + if wantPath == "" { + if strings.Contains(tt.arg0, `\`) { + wantPath = tt.arg0 + } else if tt.wantErrDot { + wantPath = strings.TrimPrefix(tt.want, tt.dir+`\`) + } else { + wantPath = filepath.Join(root, tt.want) + } + } + if gotPath != wantPath { + t.Errorf("\ncmd.Path = %#q\nwant %#q", gotPath, wantPath) + } + }) + } +} + +func TestAbsCommandWithDoubledExtension(t *testing.T) { + t.Parallel() + + // We expect that ".com" is always included in PATHEXT, but it may also be + // found in the import path of a Go package. If it is at the root of the + // import path, the resulting executable may be named like "example.com.exe". + // + // Since "example.com" looks like a proper executable name, it is probably ok + // for exec.Command to try to run it directly without re-resolving it. + // However, exec.LookPath should try a little harder to figure it out. + + comPath := filepath.Join(t.TempDir(), "example.com") + batPath := comPath + ".bat" + installBat(t, batPath) + + cmd := exec.Command(comPath) + out, err := cmd.CombinedOutput() + t.Logf("%v: %v\n%s", cmd, err, out) + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("Command(%#q).Run: %v\nwant fs.ErrNotExist", comPath, err) + } + + resolved, err := exec.LookPath(comPath) + if err != nil || resolved != batPath { + t.Fatalf("LookPath(%#q) = %v, %v; want %#q, <nil>", comPath, resolved, err, batPath) + } +} diff --git a/src/os/exec/read3.go b/src/os/exec/read3.go new file mode 100644 index 0000000..8327d73 --- /dev/null +++ b/src/os/exec/read3.go @@ -0,0 +1,91 @@ +// Copyright 2020 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 ignore + +// This is a test program that verifies that it can read from +// descriptor 3 and that no other descriptors are open. +// This is not done via TestHelperProcess and GO_EXEC_TEST_PID +// because we want to ensure that this program does not use cgo, +// because C libraries can open file descriptors behind our backs +// and confuse the test. See issue 25628. +package main + +import ( + "fmt" + "internal/poll" + "io" + "os" + "os/exec" + "os/exec/internal/fdtest" + "runtime" + "strings" +) + +func main() { + fd3 := os.NewFile(3, "fd3") + defer fd3.Close() + + bs, err := io.ReadAll(fd3) + if err != nil { + fmt.Printf("ReadAll from fd 3: %v\n", err) + os.Exit(1) + } + + // Now verify that there are no other open fds. + // stdin == 0 + // stdout == 1 + // stderr == 2 + // descriptor from parent == 3 + // All descriptors 4 and up should be available, + // except for any used by the network poller. + for fd := uintptr(4); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { + continue + } + + if !fdtest.Exists(fd) { + continue + } + + fmt.Printf("leaked parent file. fdtest.Exists(%d) got true want false\n", fd) + + fdfile := fmt.Sprintf("/proc/self/fd/%d", fd) + link, err := os.Readlink(fdfile) + fmt.Printf("readlink(%q) = %q, %v\n", fdfile, link, err) + + var args []string + switch runtime.GOOS { + case "plan9": + args = []string{fmt.Sprintf("/proc/%d/fd", os.Getpid())} + case "aix", "solaris", "illumos": + args = []string{fmt.Sprint(os.Getpid())} + default: + args = []string{"-p", fmt.Sprint(os.Getpid())} + } + + // Determine which command to use to display open files. + ofcmd := "lsof" + switch runtime.GOOS { + case "dragonfly", "freebsd", "netbsd", "openbsd": + ofcmd = "fstat" + case "plan9": + ofcmd = "/bin/cat" + case "aix": + ofcmd = "procfiles" + case "solaris", "illumos": + ofcmd = "pfiles" + } + + cmd := exec.Command(ofcmd, args...) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "%s failed: %v\n", strings.Join(cmd.Args, " "), err) + } + fmt.Printf("%s", out) + os.Exit(1) + } + + os.Stdout.Write(bs) +} diff --git a/src/os/exec_plan9.go b/src/os/exec_plan9.go new file mode 100644 index 0000000..69714ff --- /dev/null +++ b/src/os/exec_plan9.go @@ -0,0 +1,149 @@ +// 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. + +package os + +import ( + "internal/itoa" + "runtime" + "syscall" + "time" +) + +// The only signal values guaranteed to be present in the os package +// on all systems are Interrupt (send the process an interrupt) and +// Kill (force the process to exit). Interrupt is not implemented on +// Windows; using it with os.Process.Signal will return an error. +var ( + Interrupt Signal = syscall.Note("interrupt") + Kill Signal = syscall.Note("kill") +) + +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + sysattr := &syscall.ProcAttr{ + Dir: attr.Dir, + Env: attr.Env, + Sys: attr.Sys, + } + + sysattr.Files = make([]uintptr, 0, len(attr.Files)) + for _, f := range attr.Files { + sysattr.Files = append(sysattr.Files, f.Fd()) + } + + pid, h, e := syscall.StartProcess(name, argv, sysattr) + if e != nil { + return nil, &PathError{Op: "fork/exec", Path: name, Err: e} + } + + return newProcess(pid, h), nil +} + +func (p *Process) writeProcFile(file string, data string) error { + f, e := OpenFile("/proc/"+itoa.Itoa(p.Pid)+"/"+file, O_WRONLY, 0) + if e != nil { + return e + } + defer f.Close() + _, e = f.Write([]byte(data)) + return e +} + +func (p *Process) signal(sig Signal) error { + if p.done() { + return ErrProcessDone + } + if e := p.writeProcFile("note", sig.String()); e != nil { + return NewSyscallError("signal", e) + } + return nil +} + +func (p *Process) kill() error { + return p.signal(Kill) +} + +func (p *Process) wait() (ps *ProcessState, err error) { + var waitmsg syscall.Waitmsg + + if p.Pid == -1 { + return nil, ErrInvalid + } + err = syscall.WaitProcess(p.Pid, &waitmsg) + if err != nil { + return nil, NewSyscallError("wait", err) + } + + p.setDone() + ps = &ProcessState{ + pid: waitmsg.Pid, + status: &waitmsg, + } + return ps, nil +} + +func (p *Process) release() error { + // NOOP for Plan 9. + p.Pid = -1 + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + // NOOP for Plan 9. + return newProcess(pid, 0), nil +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status *syscall.Waitmsg // System-dependent status info. +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) exited() bool { + return p.status.Exited() +} + +func (p *ProcessState) success() bool { + return p.status.ExitStatus() == 0 +} + +func (p *ProcessState) sys() any { + return p.status +} + +func (p *ProcessState) sysUsage() any { + return p.status +} + +func (p *ProcessState) userTime() time.Duration { + return time.Duration(p.status.Time[0]) * time.Millisecond +} + +func (p *ProcessState) systemTime() time.Duration { + return time.Duration(p.status.Time[1]) * time.Millisecond +} + +func (p *ProcessState) String() string { + if p == nil { + return "<nil>" + } + return "exit status: " + p.status.Msg +} + +// 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 { + // return -1 if the process hasn't started. + if p == nil { + return -1 + } + return p.status.ExitStatus() +} diff --git a/src/os/exec_posix.go b/src/os/exec_posix.go new file mode 100644 index 0000000..4f9ea08 --- /dev/null +++ b/src/os/exec_posix.go @@ -0,0 +1,136 @@ +// 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 unix || (js && wasm) || wasip1 || windows + +package os + +import ( + "internal/itoa" + "internal/syscall/execenv" + "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 +) + +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + // If there is no SysProcAttr (ie. no Chroot or changed + // UID/GID), double-check existence of the directory we want + // to chdir into. We can make the error clearer this way. + if attr != nil && attr.Sys == nil && attr.Dir != "" { + if _, err := Stat(attr.Dir); err != nil { + pe := err.(*PathError) + pe.Op = "chdir" + return nil, pe + } + } + + sysattr := &syscall.ProcAttr{ + Dir: attr.Dir, + Env: attr.Env, + Sys: attr.Sys, + } + if sysattr.Env == nil { + sysattr.Env, err = execenv.Default(sysattr.Sys) + if err != nil { + return nil, err + } + } + sysattr.Files = make([]uintptr, 0, len(attr.Files)) + for _, f := range attr.Files { + sysattr.Files = append(sysattr.Files, f.Fd()) + } + + pid, h, e := syscall.StartProcess(name, argv, sysattr) + + // Make sure we don't run the finalizers of attr.Files. + runtime.KeepAlive(attr) + + if e != nil { + return nil, &PathError{Op: "fork/exec", Path: name, Err: e} + } + + return newProcess(pid, h), nil +} + +func (p *Process) kill() error { + return p.Signal(Kill) +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status syscall.WaitStatus // System-dependent status info. + rusage *syscall.Rusage +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) exited() bool { + return p.status.Exited() +} + +func (p *ProcessState) success() bool { + return p.status.ExitStatus() == 0 +} + +func (p *ProcessState) sys() any { + return p.status +} + +func (p *ProcessState) sysUsage() any { + return p.rusage +} + +func (p *ProcessState) String() string { + if p == nil { + return "<nil>" + } + status := p.Sys().(syscall.WaitStatus) + res := "" + switch { + case status.Exited(): + code := status.ExitStatus() + if runtime.GOOS == "windows" && uint(code) >= 1<<16 { // windows uses large hex numbers + res = "exit status " + itoa.Uitox(uint(code)) + } else { // unix systems use small decimal integers + res = "exit status " + itoa.Itoa(code) // unix + } + case status.Signaled(): + res = "signal: " + status.Signal().String() + case status.Stopped(): + res = "stop signal: " + status.StopSignal().String() + if status.StopSignal() == syscall.SIGTRAP && status.TrapCause() != 0 { + res += " (trap " + itoa.Itoa(status.TrapCause()) + ")" + } + case status.Continued(): + res = "continued" + } + if status.CoreDump() { + res += " (core dumped)" + } + return res +} + +// 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 { + // return -1 if the process hasn't started. + if p == nil { + return -1 + } + return p.status.ExitStatus() +} diff --git a/src/os/exec_unix.go b/src/os/exec_unix.go new file mode 100644 index 0000000..36b320d --- /dev/null +++ b/src/os/exec_unix.go @@ -0,0 +1,104 @@ +// 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 unix || (js && wasm) || wasip1 + +package os + +import ( + "errors" + "runtime" + "syscall" + "time" +) + +func (p *Process) wait() (ps *ProcessState, err error) { + if p.Pid == -1 { + return nil, syscall.EINVAL + } + + // If we can block until Wait4 will succeed immediately, do so. + ready, err := p.blockUntilWaitable() + if err != nil { + return nil, err + } + if ready { + // Mark the process done now, before the call to Wait4, + // so that Process.signal will not send a signal. + p.setDone() + // Acquire a write lock on sigMu to wait for any + // active call to the signal method to complete. + p.sigMu.Lock() + p.sigMu.Unlock() + } + + var ( + status syscall.WaitStatus + rusage syscall.Rusage + pid1 int + e error + ) + for { + pid1, e = syscall.Wait4(p.Pid, &status, 0, &rusage) + if e != syscall.EINTR { + break + } + } + if e != nil { + return nil, NewSyscallError("wait", e) + } + p.setDone() + ps = &ProcessState{ + pid: pid1, + status: status, + rusage: &rusage, + } + return ps, nil +} + +func (p *Process) signal(sig Signal) error { + if p.Pid == -1 { + return errors.New("os: process already released") + } + if p.Pid == 0 { + return errors.New("os: process not initialized") + } + p.sigMu.RLock() + defer p.sigMu.RUnlock() + if p.done() { + return ErrProcessDone + } + s, ok := sig.(syscall.Signal) + if !ok { + return errors.New("os: unsupported signal type") + } + if e := syscall.Kill(p.Pid, s); e != nil { + if e == syscall.ESRCH { + return ErrProcessDone + } + return e + } + return nil +} + +func (p *Process) release() error { + // NOOP for unix. + p.Pid = -1 + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + // NOOP for unix. + return newProcess(pid, 0), nil +} + +func (p *ProcessState) userTime() time.Duration { + return time.Duration(p.rusage.Utime.Nano()) * time.Nanosecond +} + +func (p *ProcessState) systemTime() time.Duration { + return time.Duration(p.rusage.Stime.Nano()) * time.Nanosecond +} diff --git a/src/os/exec_unix_test.go b/src/os/exec_unix_test.go new file mode 100644 index 0000000..88e1b63 --- /dev/null +++ b/src/os/exec_unix_test.go @@ -0,0 +1,45 @@ +// Copyright 2020 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 unix + +package os_test + +import ( + "internal/testenv" + . "os" + "syscall" + "testing" +) + +func TestErrProcessDone(t *testing.T) { + testenv.MustHaveGoBuild(t) + t.Parallel() + + p, err := StartProcess(testenv.GoToolPath(t), []string{"go"}, &ProcAttr{}) + if err != nil { + t.Fatalf("starting test process: %v", err) + } + p.Wait() + if got := p.Signal(Kill); got != ErrProcessDone { + t.Errorf("got %v want %v", got, ErrProcessDone) + } +} + +func TestUNIXProcessAlive(t *testing.T) { + testenv.MustHaveGoBuild(t) + t.Parallel() + + p, err := StartProcess(testenv.GoToolPath(t), []string{"sleep", "1"}, &ProcAttr{}) + if err != nil { + t.Skipf("starting test process: %v", err) + } + defer p.Kill() + + proc, _ := FindProcess(p.Pid) + err = proc.Signal(syscall.Signal(0)) + if err != nil { + t.Errorf("OS reported error for running process: %v", err) + } +} diff --git a/src/os/exec_windows.go b/src/os/exec_windows.go new file mode 100644 index 0000000..061a12b --- /dev/null +++ b/src/os/exec_windows.go @@ -0,0 +1,175 @@ +// 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. + +package os + +import ( + "errors" + "internal/syscall/windows" + "runtime" + "sync/atomic" + "syscall" + "time" +) + +func (p *Process) wait() (ps *ProcessState, err error) { + handle := atomic.LoadUintptr(&p.handle) + s, e := syscall.WaitForSingleObject(syscall.Handle(handle), syscall.INFINITE) + switch s { + case syscall.WAIT_OBJECT_0: + break + case syscall.WAIT_FAILED: + return nil, NewSyscallError("WaitForSingleObject", e) + default: + return nil, errors.New("os: unexpected result from WaitForSingleObject") + } + var ec uint32 + e = syscall.GetExitCodeProcess(syscall.Handle(handle), &ec) + if e != nil { + return nil, NewSyscallError("GetExitCodeProcess", e) + } + var u syscall.Rusage + e = syscall.GetProcessTimes(syscall.Handle(handle), &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime) + if e != nil { + return nil, NewSyscallError("GetProcessTimes", e) + } + p.setDone() + defer p.Release() + return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil +} + +func (p *Process) signal(sig Signal) error { + handle := atomic.LoadUintptr(&p.handle) + if handle == uintptr(syscall.InvalidHandle) { + return syscall.EINVAL + } + if p.done() { + return ErrProcessDone + } + if sig == Kill { + var terminationHandle syscall.Handle + e := syscall.DuplicateHandle(^syscall.Handle(0), syscall.Handle(handle), ^syscall.Handle(0), &terminationHandle, syscall.PROCESS_TERMINATE, false, 0) + if e != nil { + return NewSyscallError("DuplicateHandle", e) + } + runtime.KeepAlive(p) + defer syscall.CloseHandle(terminationHandle) + e = syscall.TerminateProcess(syscall.Handle(terminationHandle), 1) + return NewSyscallError("TerminateProcess", e) + } + // TODO(rsc): Handle Interrupt too? + return syscall.Errno(syscall.EWINDOWS) +} + +func (p *Process) release() error { + handle := atomic.SwapUintptr(&p.handle, uintptr(syscall.InvalidHandle)) + if handle == uintptr(syscall.InvalidHandle) { + return syscall.EINVAL + } + e := syscall.CloseHandle(syscall.Handle(handle)) + if e != nil { + return NewSyscallError("CloseHandle", e) + } + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +func findProcess(pid int) (p *Process, err error) { + const da = syscall.STANDARD_RIGHTS_READ | + syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE + h, e := syscall.OpenProcess(da, false, uint32(pid)) + if e != nil { + return nil, NewSyscallError("OpenProcess", e) + } + return newProcess(pid, uintptr(h)), nil +} + +func init() { + cmd := windows.UTF16PtrToString(syscall.GetCommandLine()) + if len(cmd) == 0 { + arg0, _ := Executable() + Args = []string{arg0} + } else { + Args = commandLineToArgv(cmd) + } +} + +// appendBSBytes appends n '\\' bytes to b and returns the resulting slice. +func appendBSBytes(b []byte, n int) []byte { + for ; n > 0; n-- { + b = append(b, '\\') + } + return b +} + +// readNextArg splits command line string cmd into next +// argument and command line remainder. +func readNextArg(cmd string) (arg []byte, rest string) { + var b []byte + var inquote bool + var nslash int + for ; len(cmd) > 0; cmd = cmd[1:] { + c := cmd[0] + switch c { + case ' ', '\t': + if !inquote { + return appendBSBytes(b, nslash), cmd[1:] + } + case '"': + b = appendBSBytes(b, nslash/2) + if nslash%2 == 0 { + // use "Prior to 2008" rule from + // http://daviddeley.com/autohotkey/parameters/parameters.htm + // section 5.2 to deal with double double quotes + if inquote && len(cmd) > 1 && cmd[1] == '"' { + b = append(b, c) + cmd = cmd[1:] + } + inquote = !inquote + } else { + b = append(b, c) + } + nslash = 0 + continue + case '\\': + nslash++ + continue + } + b = appendBSBytes(b, nslash) + nslash = 0 + b = append(b, c) + } + return appendBSBytes(b, nslash), "" +} + +// commandLineToArgv splits a command line into individual argument +// strings, following the Windows conventions documented +// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV +func commandLineToArgv(cmd string) []string { + var args []string + for len(cmd) > 0 { + if cmd[0] == ' ' || cmd[0] == '\t' { + cmd = cmd[1:] + continue + } + var arg []byte + arg, cmd = readNextArg(cmd) + args = append(args, string(arg)) + } + return args +} + +func ftToDuration(ft *syscall.Filetime) time.Duration { + n := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime) // in 100-nanosecond intervals + return time.Duration(n*100) * time.Nanosecond +} + +func (p *ProcessState) userTime() time.Duration { + return ftToDuration(&p.rusage.UserTime) +} + +func (p *ProcessState) systemTime() time.Duration { + return ftToDuration(&p.rusage.KernelTime) +} diff --git a/src/os/exec_windows_test.go b/src/os/exec_windows_test.go new file mode 100644 index 0000000..f8ed4cd --- /dev/null +++ b/src/os/exec_windows_test.go @@ -0,0 +1,83 @@ +// Copyright 2023 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 windows + +package os_test + +import ( + "internal/testenv" + "io" + . "os" + "path/filepath" + "sync" + "testing" +) + +func TestRemoveAllWithExecutedProcess(t *testing.T) { + // Regression test for golang.org/issue/25965. + if testing.Short() { + t.Skip("slow test; skipping") + } + testenv.MustHaveExec(t) + + name, err := Executable() + if err != nil { + t.Fatal(err) + } + r, err := Open(name) + if err != nil { + t.Fatal(err) + } + defer r.Close() + const n = 100 + var execs [n]string + // First create n executables. + for i := 0; i < n; i++ { + // Rewind r. + if _, err := r.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + name := filepath.Join(t.TempDir(), "test.exe") + execs[i] = name + w, err := Create(name) + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(w, r); err != nil { + w.Close() + t.Fatal(err) + } + if err := w.Sync(); err != nil { + w.Close() + t.Fatal(err) + } + if err = w.Close(); err != nil { + t.Fatal(err) + } + } + // Then run each executable and remove its directory. + // Run each executable in a separate goroutine to add some load + // and increase the chance of triggering the bug. + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + defer wg.Done() + name := execs[i] + dir := filepath.Dir(name) + // Run test.exe without executing any test, just to make it do something. + cmd := testenv.Command(t, name, "-test.run=^$") + if err := cmd.Run(); err != nil { + t.Errorf("exec failed: %v", err) + } + // Remove dir and check that it doesn't return `ERROR_ACCESS_DENIED`. + err = RemoveAll(dir) + if err != nil { + t.Errorf("RemoveAll failed: %v", err) + } + }(i) + } + wg.Wait() +} diff --git a/src/os/executable.go b/src/os/executable.go new file mode 100644 index 0000000..cc3134a --- /dev/null +++ b/src/os/executable.go @@ -0,0 +1,20 @@ +// Copyright 2016 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. + +package os + +// Executable returns the path name for the executable that started +// the current process. There is no guarantee that the path is still +// pointing to the correct executable. If a symlink was used to start +// the process, depending on the operating system, the result might +// be the symlink or the path it pointed to. If a stable result is +// needed, path/filepath.EvalSymlinks might help. +// +// Executable returns an absolute path unless an error occurred. +// +// The main use case is finding resources located relative to an +// executable. +func Executable() (string, error) { + return executable() +} diff --git a/src/os/executable_darwin.go b/src/os/executable_darwin.go new file mode 100644 index 0000000..dae9f4e --- /dev/null +++ b/src/os/executable_darwin.go @@ -0,0 +1,29 @@ +// Copyright 2016 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. + +package os + +import "errors" + +var executablePath string // set by ../runtime/os_darwin.go + +var initCwd, initCwdErr = Getwd() + +func executable() (string, error) { + ep := executablePath + if len(ep) == 0 { + return ep, errors.New("cannot find executable path") + } + if ep[0] != '/' { + if initCwdErr != nil { + return ep, initCwdErr + } + if len(ep) > 2 && ep[0:2] == "./" { + // skip "./" + ep = ep[2:] + } + ep = initCwd + "/" + ep + } + return ep, nil +} diff --git a/src/os/executable_dragonfly.go b/src/os/executable_dragonfly.go new file mode 100644 index 0000000..19c2ae8 --- /dev/null +++ b/src/os/executable_dragonfly.go @@ -0,0 +1,12 @@ +// Copyright 2020 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. + +package os + +// From DragonFly's <sys/sysctl.h> +const ( + _CTL_KERN = 1 + _KERN_PROC = 14 + _KERN_PROC_PATHNAME = 9 +) diff --git a/src/os/executable_freebsd.go b/src/os/executable_freebsd.go new file mode 100644 index 0000000..95f1a93 --- /dev/null +++ b/src/os/executable_freebsd.go @@ -0,0 +1,12 @@ +// Copyright 2020 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. + +package os + +// From FreeBSD's <sys/sysctl.h> +const ( + _CTL_KERN = 1 + _KERN_PROC = 14 + _KERN_PROC_PATHNAME = 12 +) diff --git a/src/os/executable_path.go b/src/os/executable_path.go new file mode 100644 index 0000000..d6161bc --- /dev/null +++ b/src/os/executable_path.go @@ -0,0 +1,104 @@ +// Copyright 2017 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 || openbsd + +package os + +// We query the working directory at init, to use it later to search for the +// executable file +// errWd will be checked later, if we need to use initWd +var initWd, errWd = Getwd() + +func executable() (string, error) { + var exePath string + if len(Args) == 0 || Args[0] == "" { + return "", ErrNotExist + } + if IsPathSeparator(Args[0][0]) { + // Args[0] is an absolute path, so it is the executable. + // Note that we only need to worry about Unix paths here. + exePath = Args[0] + } else { + for i := 1; i < len(Args[0]); i++ { + if IsPathSeparator(Args[0][i]) { + // Args[0] is a relative path: prepend the + // initial working directory. + if errWd != nil { + return "", errWd + } + exePath = initWd + string(PathSeparator) + Args[0] + break + } + } + } + if exePath != "" { + if err := isExecutable(exePath); err != nil { + return "", err + } + return exePath, nil + } + // Search for executable in $PATH. + for _, dir := range splitPathList(Getenv("PATH")) { + if len(dir) == 0 { + dir = "." + } + if !IsPathSeparator(dir[0]) { + if errWd != nil { + return "", errWd + } + dir = initWd + string(PathSeparator) + dir + } + exePath = dir + string(PathSeparator) + Args[0] + switch isExecutable(exePath) { + case nil: + return exePath, nil + case ErrPermission: + return "", ErrPermission + } + } + return "", ErrNotExist +} + +// isExecutable returns an error if a given file is not an executable. +func isExecutable(path string) error { + stat, err := Stat(path) + if err != nil { + return err + } + mode := stat.Mode() + if !mode.IsRegular() { + return ErrPermission + } + if (mode & 0111) == 0 { + return ErrPermission + } + return nil +} + +// splitPathList splits a path list. +// This is based on genSplit from strings/strings.go +func splitPathList(pathList string) []string { + if pathList == "" { + return nil + } + n := 1 + for i := 0; i < len(pathList); i++ { + if pathList[i] == PathListSeparator { + n++ + } + } + start := 0 + a := make([]string, n) + na := 0 + for i := 0; i+1 <= len(pathList) && na+1 < n; i++ { + if pathList[i] == PathListSeparator { + a[na] = pathList[start:i] + na++ + start = i + 1 + } + } + a[na] = pathList[start:] + return a[:na+1] +} diff --git a/src/os/executable_plan9.go b/src/os/executable_plan9.go new file mode 100644 index 0000000..8d8c832 --- /dev/null +++ b/src/os/executable_plan9.go @@ -0,0 +1,22 @@ +// Copyright 2016 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 + +package os + +import ( + "internal/itoa" + "syscall" +) + +func executable() (string, error) { + fn := "/proc/" + itoa.Itoa(Getpid()) + "/text" + f, err := Open(fn) + if err != nil { + return "", err + } + defer f.Close() + return syscall.Fd2path(int(f.Fd())) +} diff --git a/src/os/executable_procfs.go b/src/os/executable_procfs.go new file mode 100644 index 0000000..94e674e --- /dev/null +++ b/src/os/executable_procfs.go @@ -0,0 +1,37 @@ +// Copyright 2016 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 || netbsd + +package os + +import ( + "errors" + "runtime" +) + +func executable() (string, error) { + var procfn string + switch runtime.GOOS { + default: + return "", errors.New("Executable not implemented for " + runtime.GOOS) + case "linux", "android": + procfn = "/proc/self/exe" + case "netbsd": + procfn = "/proc/curproc/exe" + } + path, err := Readlink(procfn) + + // When the executable has been deleted then Readlink returns a + // path appended with " (deleted)". + return stringsTrimSuffix(path, " (deleted)"), err +} + +// stringsTrimSuffix is the same as strings.TrimSuffix. +func stringsTrimSuffix(s, suffix string) string { + if len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix { + return s[:len(s)-len(suffix)] + } + return s +} diff --git a/src/os/executable_solaris.go b/src/os/executable_solaris.go new file mode 100644 index 0000000..b145980 --- /dev/null +++ b/src/os/executable_solaris.go @@ -0,0 +1,32 @@ +// Copyright 2016 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. + +package os + +import "syscall" + +var executablePath string // set by sysauxv in ../runtime/os3_solaris.go + +var initCwd, initCwdErr = Getwd() + +func executable() (string, error) { + path := executablePath + if len(path) == 0 { + path, err := syscall.Getexecname() + if err != nil { + return path, err + } + } + if len(path) > 0 && path[0] != '/' { + if initCwdErr != nil { + return path, initCwdErr + } + if len(path) > 2 && path[0:2] == "./" { + // skip "./" + path = path[2:] + } + return initCwd + "/" + path, nil + } + return path, nil +} diff --git a/src/os/executable_sysctl.go b/src/os/executable_sysctl.go new file mode 100644 index 0000000..3c2aeac --- /dev/null +++ b/src/os/executable_sysctl.go @@ -0,0 +1,35 @@ +// Copyright 2016 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 freebsd || dragonfly + +package os + +import ( + "syscall" + "unsafe" +) + +func executable() (string, error) { + mib := [4]int32{_CTL_KERN, _KERN_PROC, _KERN_PROC_PATHNAME, -1} + + n := uintptr(0) + // get length + _, _, err := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0) + if err != 0 { + return "", err + } + if n == 0 { // shouldn't happen + return "", nil + } + buf := make([]byte, n) + _, _, err = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0) + if err != 0 { + return "", err + } + if n == 0 { // shouldn't happen + return "", nil + } + return string(buf[:n-1]), nil +} diff --git a/src/os/executable_test.go b/src/os/executable_test.go new file mode 100644 index 0000000..98b72d7 --- /dev/null +++ b/src/os/executable_test.go @@ -0,0 +1,155 @@ +// Copyright 2016 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. + +package os_test + +import ( + "fmt" + "internal/testenv" + "os" + "path/filepath" + "runtime" + "testing" +) + +const executable_EnvVar = "OSTEST_OUTPUT_EXECPATH" + +func TestExecutable(t *testing.T) { + testenv.MustHaveExec(t) + t.Parallel() + + ep, err := os.Executable() + if err != nil { + t.Fatalf("Executable failed: %v", err) + } + // we want fn to be of the form "dir/prog" + dir := filepath.Dir(filepath.Dir(ep)) + fn, err := filepath.Rel(dir, ep) + if err != nil { + t.Fatalf("filepath.Rel: %v", err) + } + + cmd := testenv.Command(t, fn, "-test.run=^$") + // make child start with a relative program path + cmd.Dir = dir + cmd.Path = fn + if runtime.GOOS == "openbsd" || runtime.GOOS == "aix" { + // OpenBSD and AIX rely on argv[0] + } else { + // forge argv[0] for child, so that we can verify we could correctly + // get real path of the executable without influenced by argv[0]. + cmd.Args[0] = "-" + } + cmd.Env = append(cmd.Environ(), fmt.Sprintf("%s=1", executable_EnvVar)) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("exec(self) failed: %v", err) + } + outs := string(out) + if !filepath.IsAbs(outs) { + t.Fatalf("Child returned %q, want an absolute path", out) + } + if !sameFile(outs, ep) { + t.Fatalf("Child returned %q, not the same file as %q", out, ep) + } +} + +func sameFile(fn1, fn2 string) bool { + fi1, err := os.Stat(fn1) + if err != nil { + return false + } + fi2, err := os.Stat(fn2) + if err != nil { + return false + } + return os.SameFile(fi1, fi2) +} + +func init() { + if e := os.Getenv(executable_EnvVar); e != "" { + // first chdir to another path + dir := "/" + if runtime.GOOS == "windows" { + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + dir = filepath.VolumeName(cwd) + } + os.Chdir(dir) + if ep, err := os.Executable(); err != nil { + fmt.Fprint(os.Stderr, "ERROR: ", err) + } else { + fmt.Fprint(os.Stderr, ep) + } + os.Exit(0) + } +} + +func TestExecutableDeleted(t *testing.T) { + testenv.MustHaveGoBuild(t) + switch runtime.GOOS { + case "windows", "plan9": + t.Skipf("%v does not support deleting running binary", runtime.GOOS) + case "openbsd", "freebsd", "aix": + t.Skipf("%v does not support reading deleted binary name", runtime.GOOS) + } + t.Parallel() + + dir := t.TempDir() + + src := filepath.Join(dir, "testdel.go") + exe := filepath.Join(dir, "testdel.exe") + + err := os.WriteFile(src, []byte(testExecutableDeletion), 0666) + if err != nil { + t.Fatal(err) + } + + out, err := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, src).CombinedOutput() + t.Logf("build output:\n%s", out) + if err != nil { + t.Fatal(err) + } + + out, err = testenv.Command(t, exe).CombinedOutput() + t.Logf("exec output:\n%s", out) + if err != nil { + t.Fatal(err) + } +} + +const testExecutableDeletion = `package main + +import ( + "fmt" + "os" +) + +func main() { + before, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read executable name before deletion: %v\n", err) + os.Exit(1) + } + + err = os.Remove(before) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to remove executable: %v\n", err) + os.Exit(1) + } + + after, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read executable name after deletion: %v\n", err) + os.Exit(1) + } + + if before != after { + fmt.Fprintf(os.Stderr, "before and after do not match: %v != %v\n", before, after) + os.Exit(1) + } +} +` diff --git a/src/os/executable_wasm.go b/src/os/executable_wasm.go new file mode 100644 index 0000000..a88360c --- /dev/null +++ b/src/os/executable_wasm.go @@ -0,0 +1,16 @@ +// Copyright 2023 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 wasm + +package os + +import ( + "errors" + "runtime" +) + +func executable() (string, error) { + return "", errors.New("Executable not implemented for " + runtime.GOOS) +} diff --git a/src/os/executable_windows.go b/src/os/executable_windows.go new file mode 100644 index 0000000..fc5cf86 --- /dev/null +++ b/src/os/executable_windows.go @@ -0,0 +1,32 @@ +// Copyright 2016 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. + +package os + +import ( + "internal/syscall/windows" + "syscall" +) + +func getModuleFileName(handle syscall.Handle) (string, error) { + n := uint32(1024) + var buf []uint16 + for { + buf = make([]uint16, n) + r, err := windows.GetModuleFileName(handle, &buf[0], n) + if err != nil { + return "", err + } + if r < n { + break + } + // r == n means n not big enough + n += 1024 + } + return syscall.UTF16ToString(buf), nil +} + +func executable() (string, error) { + return getModuleFileName(0) +} |