| // 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 (darwin || dragonfly || freebsd || (linux && !android) || netbsd || openbsd) && cgo |
| |
| // Note that this test does not work on Solaris: issue #22849. |
| // Don't run the test on Android because at least some versions of the |
| // C library do not define the posix_openpt function. |
| |
| package signal_test |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "fmt" |
| "io" |
| "io/fs" |
| "os" |
| "os/exec" |
| ptypkg "os/signal/internal/pty" |
| "strconv" |
| "strings" |
| "sync" |
| "syscall" |
| "testing" |
| "time" |
| ) |
| |
| func TestTerminalSignal(t *testing.T) { |
| const enteringRead = "test program entering read" |
| if os.Getenv("GO_TEST_TERMINAL_SIGNALS") != "" { |
| var b [1]byte |
| fmt.Println(enteringRead) |
| n, err := os.Stdin.Read(b[:]) |
| if n == 1 { |
| if b[0] == '\n' { |
| // This is what we expect |
| fmt.Println("read newline") |
| } else { |
| fmt.Printf("read 1 byte: %q\n", b) |
| } |
| } else { |
| fmt.Printf("read %d bytes\n", n) |
| } |
| if err != nil { |
| fmt.Println(err) |
| os.Exit(1) |
| } |
| os.Exit(0) |
| } |
| |
| t.Parallel() |
| |
| // The test requires a shell that uses job control. |
| bash, err := exec.LookPath("bash") |
| if err != nil { |
| t.Skipf("could not find bash: %v", err) |
| } |
| |
| scale := 1 |
| if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { |
| if sc, err := strconv.Atoi(s); err == nil { |
| scale = sc |
| } |
| } |
| pause := time.Duration(scale) * 10 * time.Millisecond |
| wait := time.Duration(scale) * 5 * time.Second |
| |
| // The test only fails when using a "slow device," in this |
| // case a pseudo-terminal. |
| |
| pty, procTTYName, err := ptypkg.Open() |
| if err != nil { |
| ptyErr := err.(*ptypkg.PtyError) |
| if ptyErr.FuncName == "posix_openpt" && ptyErr.Errno == syscall.EACCES { |
| t.Skip("posix_openpt failed with EACCES, assuming chroot and skipping") |
| } |
| t.Fatal(err) |
| } |
| defer pty.Close() |
| procTTY, err := os.OpenFile(procTTYName, os.O_RDWR, 0) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer procTTY.Close() |
| |
| // Start an interactive shell. |
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| defer cancel() |
| cmd := exec.CommandContext(ctx, bash, "--norc", "--noprofile", "-i") |
| // Clear HISTFILE so that we don't read or clobber the user's bash history. |
| cmd.Env = append(os.Environ(), "HISTFILE=") |
| cmd.Stdin = procTTY |
| cmd.Stdout = procTTY |
| cmd.Stderr = procTTY |
| cmd.SysProcAttr = &syscall.SysProcAttr{ |
| Setsid: true, |
| Setctty: true, |
| Ctty: 0, |
| } |
| |
| if err := cmd.Start(); err != nil { |
| t.Fatal(err) |
| } |
| |
| if err := procTTY.Close(); err != nil { |
| t.Errorf("closing procTTY: %v", err) |
| } |
| |
| progReady := make(chan bool) |
| sawPrompt := make(chan bool, 10) |
| const prompt = "prompt> " |
| |
| // Read data from pty in the background. |
| var wg sync.WaitGroup |
| wg.Add(1) |
| defer wg.Wait() |
| go func() { |
| defer wg.Done() |
| input := bufio.NewReader(pty) |
| var line, handled []byte |
| for { |
| b, err := input.ReadByte() |
| if err != nil { |
| if len(line) > 0 || len(handled) > 0 { |
| t.Logf("%q", append(handled, line...)) |
| } |
| if perr, ok := err.(*fs.PathError); ok { |
| err = perr.Err |
| } |
| // EOF means pty is closed. |
| // EIO means child process is done. |
| // "file already closed" means deferred close of pty has happened. |
| if err != io.EOF && err != syscall.EIO && !strings.Contains(err.Error(), "file already closed") { |
| t.Logf("error reading from pty: %v", err) |
| } |
| return |
| } |
| |
| line = append(line, b) |
| |
| if b == '\n' { |
| t.Logf("%q", append(handled, line...)) |
| line = nil |
| handled = nil |
| continue |
| } |
| |
| if bytes.Contains(line, []byte(enteringRead)) { |
| close(progReady) |
| handled = append(handled, line...) |
| line = nil |
| } else if bytes.Contains(line, []byte(prompt)) && !bytes.Contains(line, []byte("PS1=")) { |
| sawPrompt <- true |
| handled = append(handled, line...) |
| line = nil |
| } |
| } |
| }() |
| |
| // Set the bash prompt so that we can see it. |
| if _, err := pty.Write([]byte("PS1='" + prompt + "'\n")); err != nil { |
| t.Fatalf("setting prompt: %v", err) |
| } |
| select { |
| case <-sawPrompt: |
| case <-time.After(wait): |
| t.Fatal("timed out waiting for shell prompt") |
| } |
| |
| // Start a small program that reads from stdin |
| // (namely the code at the top of this function). |
| if _, err := pty.Write([]byte("GO_TEST_TERMINAL_SIGNALS=1 " + os.Args[0] + " -test.run=TestTerminalSignal\n")); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Wait for the program to print that it is starting. |
| select { |
| case <-progReady: |
| case <-time.After(wait): |
| t.Fatal("timed out waiting for program to start") |
| } |
| |
| // Give the program time to enter the read call. |
| // It doesn't matter much if we occasionally don't wait long enough; |
| // we won't be testing what we want to test, but the overall test |
| // will pass. |
| time.Sleep(pause) |
| |
| // Send a ^Z to stop the program. |
| if _, err := pty.Write([]byte{26}); err != nil { |
| t.Fatalf("writing ^Z to pty: %v", err) |
| } |
| |
| // Wait for the program to stop and return to the shell. |
| select { |
| case <-sawPrompt: |
| case <-time.After(wait): |
| t.Fatal("timed out waiting for shell prompt") |
| } |
| |
| // Restart the stopped program. |
| if _, err := pty.Write([]byte("fg\n")); err != nil { |
| t.Fatalf("writing %q to pty: %v", "fg", err) |
| } |
| |
| // Give the process time to restart. |
| // This is potentially racy: if the process does not restart |
| // quickly enough then the byte we send will go to bash rather |
| // than the program. Unfortunately there isn't anything we can |
| // look for to know that the program is running again. |
| // bash will print the program name, but that happens before it |
| // restarts the program. |
| time.Sleep(10 * pause) |
| |
| // Write some data for the program to read, |
| // which should cause it to exit. |
| if _, err := pty.Write([]byte{'\n'}); err != nil { |
| t.Fatalf("writing %q to pty: %v", "\n", err) |
| } |
| |
| // Wait for the program to exit. |
| select { |
| case <-sawPrompt: |
| case <-time.After(wait): |
| t.Fatal("timed out waiting for shell prompt") |
| } |
| |
| // Exit the shell with the program's exit status. |
| if _, err := pty.Write([]byte("exit $?\n")); err != nil { |
| t.Fatalf("writing %q to pty: %v", "exit", err) |
| } |
| |
| if err = cmd.Wait(); err != nil { |
| t.Errorf("subprogram failed: %v", err) |
| } |
| } |