Home | History | Annotate | Download | only in signal
      1 // Copyright 2017 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 // +build darwin dragonfly freebsd linux,!android netbsd openbsd
      6 // +build cgo
      7 
      8 // Note that this test does not work on Solaris: issue #22849.
      9 // Don't run the test on Android because at least some versions of the
     10 // C library do not define the posix_openpt function.
     11 
     12 package signal_test
     13 
     14 import (
     15 	"bufio"
     16 	"bytes"
     17 	"context"
     18 	"fmt"
     19 	"io"
     20 	"os"
     21 	"os/exec"
     22 	"os/signal/internal/pty"
     23 	"strconv"
     24 	"strings"
     25 	"syscall"
     26 	"testing"
     27 	"time"
     28 )
     29 
     30 func TestTerminalSignal(t *testing.T) {
     31 	const enteringRead = "test program entering read"
     32 	if os.Getenv("GO_TEST_TERMINAL_SIGNALS") != "" {
     33 		var b [1]byte
     34 		fmt.Println(enteringRead)
     35 		n, err := os.Stdin.Read(b[:])
     36 		if n == 1 {
     37 			if b[0] == '\n' {
     38 				// This is what we expect
     39 				fmt.Println("read newline")
     40 			} else {
     41 				fmt.Printf("read 1 byte: %q\n", b)
     42 			}
     43 		} else {
     44 			fmt.Printf("read %d bytes\n", n)
     45 		}
     46 		if err != nil {
     47 			fmt.Println(err)
     48 			os.Exit(1)
     49 		}
     50 		os.Exit(0)
     51 	}
     52 
     53 	t.Parallel()
     54 
     55 	// The test requires a shell that uses job control.
     56 	bash, err := exec.LookPath("bash")
     57 	if err != nil {
     58 		t.Skipf("could not find bash: %v", err)
     59 	}
     60 
     61 	scale := 1
     62 	if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
     63 		if sc, err := strconv.Atoi(s); err == nil {
     64 			scale = sc
     65 		}
     66 	}
     67 	pause := time.Duration(scale) * 10 * time.Millisecond
     68 	wait := time.Duration(scale) * 5 * time.Second
     69 
     70 	// The test only fails when using a "slow device," in this
     71 	// case a pseudo-terminal.
     72 
     73 	master, sname, err := pty.Open()
     74 	if err != nil {
     75 		ptyErr := err.(*pty.PtyError)
     76 		if ptyErr.FuncName == "posix_openpt" && ptyErr.Errno == syscall.EACCES {
     77 			t.Skip("posix_openpt failed with EACCES, assuming chroot and skipping")
     78 		}
     79 		t.Fatal(err)
     80 	}
     81 	defer master.Close()
     82 	slave, err := os.OpenFile(sname, os.O_RDWR, 0)
     83 	if err != nil {
     84 		t.Fatal(err)
     85 	}
     86 	defer slave.Close()
     87 
     88 	// Start an interactive shell.
     89 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
     90 	defer cancel()
     91 	cmd := exec.CommandContext(ctx, bash, "--norc", "--noprofile", "-i")
     92 	cmd.Stdin = slave
     93 	cmd.Stdout = slave
     94 	cmd.Stderr = slave
     95 	cmd.SysProcAttr = &syscall.SysProcAttr{
     96 		Setsid:  true,
     97 		Setctty: true,
     98 		Ctty:    int(slave.Fd()),
     99 	}
    100 
    101 	if err := cmd.Start(); err != nil {
    102 		t.Fatal(err)
    103 	}
    104 
    105 	if err := slave.Close(); err != nil {
    106 		t.Errorf("closing slave: %v", err)
    107 	}
    108 
    109 	progReady := make(chan bool)
    110 	sawPrompt := make(chan bool, 10)
    111 	const prompt = "prompt> "
    112 
    113 	// Read data from master in the background.
    114 	go func() {
    115 		input := bufio.NewReader(master)
    116 		var line, handled []byte
    117 		for {
    118 			b, err := input.ReadByte()
    119 			if err != nil {
    120 				if len(line) > 0 || len(handled) > 0 {
    121 					t.Logf("%q", append(handled, line...))
    122 				}
    123 				if perr, ok := err.(*os.PathError); ok {
    124 					err = perr.Err
    125 				}
    126 				// EOF means master is closed.
    127 				// EIO means child process is done.
    128 				// "file already closed" means deferred close of master has happened.
    129 				if err != io.EOF && err != syscall.EIO && !strings.Contains(err.Error(), "file already closed") {
    130 					t.Logf("error reading from master: %v", err)
    131 				}
    132 				return
    133 			}
    134 
    135 			line = append(line, b)
    136 
    137 			if b == '\n' {
    138 				t.Logf("%q", append(handled, line...))
    139 				line = nil
    140 				handled = nil
    141 				continue
    142 			}
    143 
    144 			if bytes.Contains(line, []byte(enteringRead)) {
    145 				close(progReady)
    146 				handled = append(handled, line...)
    147 				line = nil
    148 			} else if bytes.Contains(line, []byte(prompt)) && !bytes.Contains(line, []byte("PS1=")) {
    149 				sawPrompt <- true
    150 				handled = append(handled, line...)
    151 				line = nil
    152 			}
    153 		}
    154 	}()
    155 
    156 	// Set the bash prompt so that we can see it.
    157 	if _, err := master.Write([]byte("PS1='" + prompt + "'\n")); err != nil {
    158 		t.Fatalf("setting prompt: %v", err)
    159 	}
    160 	select {
    161 	case <-sawPrompt:
    162 	case <-time.After(wait):
    163 		t.Fatal("timed out waiting for shell prompt")
    164 	}
    165 
    166 	// Start a small program that reads from stdin
    167 	// (namely the code at the top of this function).
    168 	if _, err := master.Write([]byte("GO_TEST_TERMINAL_SIGNALS=1 " + os.Args[0] + " -test.run=TestTerminalSignal\n")); err != nil {
    169 		t.Fatal(err)
    170 	}
    171 
    172 	// Wait for the program to print that it is starting.
    173 	select {
    174 	case <-progReady:
    175 	case <-time.After(wait):
    176 		t.Fatal("timed out waiting for program to start")
    177 	}
    178 
    179 	// Give the program time to enter the read call.
    180 	// It doesn't matter much if we occasionally don't wait long enough;
    181 	// we won't be testing what we want to test, but the overall test
    182 	// will pass.
    183 	time.Sleep(pause)
    184 
    185 	// Send a ^Z to stop the program.
    186 	if _, err := master.Write([]byte{26}); err != nil {
    187 		t.Fatalf("writing ^Z to pty: %v", err)
    188 	}
    189 
    190 	// Wait for the program to stop and return to the shell.
    191 	select {
    192 	case <-sawPrompt:
    193 	case <-time.After(wait):
    194 		t.Fatal("timed out waiting for shell prompt")
    195 	}
    196 
    197 	// Restart the stopped program.
    198 	if _, err := master.Write([]byte("fg\n")); err != nil {
    199 		t.Fatalf("writing %q to pty: %v", "fg", err)
    200 	}
    201 
    202 	// Give the process time to restart.
    203 	// This is potentially racy: if the process does not restart
    204 	// quickly enough then the byte we send will go to bash rather
    205 	// than the program. Unfortunately there isn't anything we can
    206 	// look for to know that the program is running again.
    207 	// bash will print the program name, but that happens before it
    208 	// restarts the program.
    209 	time.Sleep(10 * pause)
    210 
    211 	// Write some data for the program to read,
    212 	// which should cause it to exit.
    213 	if _, err := master.Write([]byte{'\n'}); err != nil {
    214 		t.Fatalf("writing %q to pty: %v", "\n", err)
    215 	}
    216 
    217 	// Wait for the program to exit.
    218 	select {
    219 	case <-sawPrompt:
    220 	case <-time.After(wait):
    221 		t.Fatal("timed out waiting for shell prompt")
    222 	}
    223 
    224 	// Exit the shell with the program's exit status.
    225 	if _, err := master.Write([]byte("exit $?\n")); err != nil {
    226 		t.Fatalf("writing %q to pty: %v", "exit", err)
    227 	}
    228 
    229 	if err = cmd.Wait(); err != nil {
    230 		t.Errorf("subprogram failed: %v", err)
    231 	}
    232 }
    233