| // 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 unix |
| |
| package syscall_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "internal/testenv" |
| "io" |
| "math/rand" |
| "os" |
| "os/exec" |
| "os/signal" |
| "strconv" |
| "syscall" |
| "testing" |
| "time" |
| "unsafe" |
| ) |
| |
| type command struct { |
| pipe io.WriteCloser |
| proc *exec.Cmd |
| test *testing.T |
| } |
| |
| func (c *command) Info() (pid, pgrp int) { |
| pid = c.proc.Process.Pid |
| |
| pgrp, err := syscall.Getpgid(pid) |
| if err != nil { |
| c.test.Fatal(err) |
| } |
| |
| return |
| } |
| |
| func (c *command) Start() { |
| if err := c.proc.Start(); err != nil { |
| c.test.Fatal(err) |
| } |
| } |
| |
| func (c *command) Stop() { |
| c.pipe.Close() |
| if err := c.proc.Wait(); err != nil { |
| c.test.Fatal(err) |
| } |
| } |
| |
| func create(t *testing.T) *command { |
| testenv.MustHaveExec(t) |
| |
| proc := exec.Command("cat") |
| stdin, err := proc.StdinPipe() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| return &command{stdin, proc, t} |
| } |
| |
| func parent() (pid, pgrp int) { |
| return syscall.Getpid(), syscall.Getpgrp() |
| } |
| |
| func TestZeroSysProcAttr(t *testing.T) { |
| ppid, ppgrp := parent() |
| |
| cmd := create(t) |
| |
| cmd.Start() |
| defer cmd.Stop() |
| |
| cpid, cpgrp := cmd.Info() |
| |
| if cpid == ppid { |
| t.Fatalf("Parent and child have the same process ID") |
| } |
| |
| if cpgrp != ppgrp { |
| t.Fatalf("Child is not in parent's process group") |
| } |
| } |
| |
| func TestSetpgid(t *testing.T) { |
| ppid, ppgrp := parent() |
| |
| cmd := create(t) |
| |
| cmd.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} |
| cmd.Start() |
| defer cmd.Stop() |
| |
| cpid, cpgrp := cmd.Info() |
| |
| if cpid == ppid { |
| t.Fatalf("Parent and child have the same process ID") |
| } |
| |
| if cpgrp == ppgrp { |
| t.Fatalf("Parent and child are in the same process group") |
| } |
| |
| if cpid != cpgrp { |
| t.Fatalf("Child's process group is not the child's process ID") |
| } |
| } |
| |
| func TestPgid(t *testing.T) { |
| ppid, ppgrp := parent() |
| |
| cmd1 := create(t) |
| |
| cmd1.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} |
| cmd1.Start() |
| defer cmd1.Stop() |
| |
| cpid1, cpgrp1 := cmd1.Info() |
| |
| if cpid1 == ppid { |
| t.Fatalf("Parent and child 1 have the same process ID") |
| } |
| |
| if cpgrp1 == ppgrp { |
| t.Fatalf("Parent and child 1 are in the same process group") |
| } |
| |
| if cpid1 != cpgrp1 { |
| t.Fatalf("Child 1's process group is not its process ID") |
| } |
| |
| cmd2 := create(t) |
| |
| cmd2.proc.SysProcAttr = &syscall.SysProcAttr{ |
| Setpgid: true, |
| Pgid: cpgrp1, |
| } |
| cmd2.Start() |
| defer cmd2.Stop() |
| |
| cpid2, cpgrp2 := cmd2.Info() |
| |
| if cpid2 == ppid { |
| t.Fatalf("Parent and child 2 have the same process ID") |
| } |
| |
| if cpgrp2 == ppgrp { |
| t.Fatalf("Parent and child 2 are in the same process group") |
| } |
| |
| if cpid2 == cpgrp2 { |
| t.Fatalf("Child 2's process group is its process ID") |
| } |
| |
| if cpid1 == cpid2 { |
| t.Fatalf("Child 1 and 2 have the same process ID") |
| } |
| |
| if cpgrp1 != cpgrp2 { |
| t.Fatalf("Child 1 and 2 are not in the same process group") |
| } |
| } |
| |
| func TestForeground(t *testing.T) { |
| signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU) |
| defer signal.Reset() |
| |
| tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) |
| if err != nil { |
| t.Skipf("Can't test Foreground. Couldn't open /dev/tty: %s", err) |
| } |
| defer tty.Close() |
| |
| // This should really be pid_t, however _C_int (aka int32) is generally |
| // equivalent. |
| fpgrp := int32(0) |
| |
| errno := syscall.Ioctl(tty.Fd(), syscall.TIOCGPGRP, uintptr(unsafe.Pointer(&fpgrp))) |
| if errno != 0 { |
| t.Fatalf("TIOCGPGRP failed with error code: %s", errno) |
| } |
| |
| if fpgrp == 0 { |
| t.Fatalf("Foreground process group is zero") |
| } |
| |
| ppid, ppgrp := parent() |
| |
| cmd := create(t) |
| |
| cmd.proc.SysProcAttr = &syscall.SysProcAttr{ |
| Ctty: int(tty.Fd()), |
| Foreground: true, |
| } |
| cmd.Start() |
| |
| cpid, cpgrp := cmd.Info() |
| |
| if cpid == ppid { |
| t.Fatalf("Parent and child have the same process ID") |
| } |
| |
| if cpgrp == ppgrp { |
| t.Fatalf("Parent and child are in the same process group") |
| } |
| |
| if cpid != cpgrp { |
| t.Fatalf("Child's process group is not the child's process ID") |
| } |
| |
| cmd.Stop() |
| |
| // This call fails on darwin/arm64. The failure doesn't matter, though. |
| // This is just best effort. |
| syscall.Ioctl(tty.Fd(), syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&fpgrp))) |
| } |
| |
| func TestForegroundSignal(t *testing.T) { |
| tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) |
| if err != nil { |
| t.Skipf("couldn't open /dev/tty: %s", err) |
| } |
| defer tty.Close() |
| |
| // This should really be pid_t, however _C_int (aka int32) is generally |
| // equivalent. |
| fpgrp := int32(0) |
| |
| errno := syscall.Ioctl(tty.Fd(), syscall.TIOCGPGRP, uintptr(unsafe.Pointer(&fpgrp))) |
| if errno != 0 { |
| t.Fatalf("TIOCGPGRP failed with error code: %s", errno) |
| } |
| |
| if fpgrp == 0 { |
| t.Fatalf("Foreground process group is zero") |
| } |
| |
| defer func() { |
| signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU) |
| syscall.Ioctl(tty.Fd(), syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&fpgrp))) |
| signal.Reset() |
| }() |
| |
| ch1 := make(chan os.Signal, 1) |
| ch2 := make(chan bool) |
| |
| signal.Notify(ch1, syscall.SIGTTIN, syscall.SIGTTOU) |
| defer signal.Stop(ch1) |
| |
| cmd := create(t) |
| |
| go func() { |
| cmd.proc.SysProcAttr = &syscall.SysProcAttr{ |
| Ctty: int(tty.Fd()), |
| Foreground: true, |
| } |
| cmd.Start() |
| cmd.Stop() |
| close(ch2) |
| }() |
| |
| timer := time.NewTimer(30 * time.Second) |
| defer timer.Stop() |
| for { |
| select { |
| case sig := <-ch1: |
| t.Errorf("unexpected signal %v", sig) |
| case <-ch2: |
| // Success. |
| return |
| case <-timer.C: |
| t.Fatal("timed out waiting for child process") |
| } |
| } |
| } |
| |
| // Test a couple of cases that SysProcAttr can't handle. Issue 29458. |
| func TestInvalidExec(t *testing.T) { |
| t.Parallel() |
| t.Run("SetCtty-Foreground", func(t *testing.T) { |
| t.Parallel() |
| cmd := create(t) |
| cmd.proc.SysProcAttr = &syscall.SysProcAttr{ |
| Setctty: true, |
| Foreground: true, |
| Ctty: 0, |
| } |
| if err := cmd.proc.Start(); err == nil { |
| t.Error("expected error setting both SetCtty and Foreground") |
| } |
| }) |
| t.Run("invalid-Ctty", func(t *testing.T) { |
| t.Parallel() |
| cmd := create(t) |
| cmd.proc.SysProcAttr = &syscall.SysProcAttr{ |
| Setctty: true, |
| Ctty: 3, |
| } |
| if err := cmd.proc.Start(); err == nil { |
| t.Error("expected error with invalid Ctty value") |
| } |
| }) |
| } |
| |
| // TestExec is for issue #41702. |
| func TestExec(t *testing.T) { |
| testenv.MustHaveExec(t) |
| cmd := exec.Command(os.Args[0], "-test.run=TestExecHelper") |
| cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=2") |
| o, err := cmd.CombinedOutput() |
| if err != nil { |
| t.Errorf("%s\n%v", o, err) |
| } |
| } |
| |
| // TestExecHelper is used by TestExec. It does nothing by itself. |
| // In testing on macOS 10.14, this used to fail with |
| // "signal: illegal instruction" more than half the time. |
| func TestExecHelper(t *testing.T) { |
| if os.Getenv("GO_WANT_HELPER_PROCESS") != "2" { |
| return |
| } |
| |
| // We don't have to worry about restoring these values. |
| // We are in a child process that only runs this test, |
| // and we are going to call syscall.Exec anyhow. |
| os.Setenv("GO_WANT_HELPER_PROCESS", "3") |
| |
| stop := time.Now().Add(time.Second) |
| for i := 0; i < 100; i++ { |
| go func(i int) { |
| r := rand.New(rand.NewSource(int64(i))) |
| for time.Now().Before(stop) { |
| r.Uint64() |
| } |
| }(i) |
| } |
| |
| time.Sleep(10 * time.Millisecond) |
| |
| argv := []string{os.Args[0], "-test.run=TestExecHelper"} |
| syscall.Exec(os.Args[0], argv, os.Environ()) |
| |
| t.Error("syscall.Exec returned") |
| } |
| |
| // Test that rlimit values are restored by exec. |
| func TestRlimitRestored(t *testing.T) { |
| if os.Getenv("GO_WANT_HELPER_PROCESS") != "" { |
| fmt.Println(syscall.OrigRlimitNofile().Cur) |
| os.Exit(0) |
| } |
| |
| orig := syscall.OrigRlimitNofile() |
| if orig.Cur == 0 { |
| t.Skip("skipping test because rlimit not adjusted at startup") |
| } |
| |
| executable, err := os.Executable() |
| if err != nil { |
| executable = os.Args[0] |
| } |
| |
| cmd := testenv.Command(t, executable, "-test.run=TestRlimitRestored") |
| cmd = testenv.CleanCmdEnv(cmd) |
| cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") |
| |
| out, err := cmd.CombinedOutput() |
| if len(out) > 0 { |
| t.Logf("%s", out) |
| } |
| if err != nil { |
| t.Fatalf("subprocess failed: %v", err) |
| } |
| s := string(bytes.TrimSpace(out)) |
| v, err := strconv.ParseUint(s, 10, 64) |
| if err != nil { |
| t.Fatalf("could not parse %q as number: %v", s, v) |
| } |
| |
| if v != uint64(orig.Cur) { |
| t.Errorf("exec rlimit = %d, want %d", v, orig) |
| } |
| } |