// 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. // +build darwin dragonfly freebsd linux,!android netbsd openbsd // +build 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) } }