"runtime"
"strconv"
"strings"
+ "sync/atomic"
"syscall"
"time"
)
// the work of resolving the extension, so Start doesn't need to do it again.
// This is only used on Windows.
cachedLookExtensions struct{ in, out string }
+
+ // startCalled records that Start was attempted, regardless of outcome.
+ startCalled atomic.Bool
}
// A ctxResult reports the result of watching the Context associated with a
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 {
+ // It is an error to call Start twice even if the first call did not create a process.
+ if c.startCalled.Swap(true) {
return errors.New("exec: already started")
}
if !started {
closeDescriptors(c.parentIOPipes)
c.parentIOPipes = nil
+ c.goroutine = nil // aid GC, finalization of pipe fds
}
}()
}
})
}
+
+// Calling Start twice is an error, regardless of outcome.
+func TestStart_twice(t *testing.T) {
+ testenv.MustHaveExec(t)
+
+ cmd := exec.Command("/bin/nonesuch")
+ for i, want := range []string{
+ cond(runtime.GOOS == "windows",
+ `exec: "/bin/nonesuch": executable file not found in %PATH%`,
+ "fork/exec /bin/nonesuch: no such file or directory"),
+ "exec: already started",
+ } {
+ err := cmd.Start()
+ if got := fmt.Sprint(err); got != want {
+ t.Errorf("Start call #%d return err %q, want %q", i+1, got, want)
+ }
+ }
+}
+
+func cond[T any](cond bool, t, f T) T {
+ if cond {
+ return t
+ } else {
+ return f
+ }
+}