github.com/ice-blockchain/go/src@v0.0.0-20240403114104-1564d284e521/os/signal/signal_cgo_test.go (about) 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 //go:build (darwin || dragonfly || freebsd || (linux && !android) || netbsd || openbsd) && cgo 6 7 // Note that this test does not work on Solaris: issue #22849. 8 // Don't run the test on Android because at least some versions of the 9 // C library do not define the posix_openpt function. 10 11 package signal_test 12 13 import ( 14 "context" 15 "encoding/binary" 16 "fmt" 17 "internal/syscall/unix" 18 "internal/testenv" 19 "internal/testpty" 20 "os" 21 "os/signal" 22 "runtime" 23 "strconv" 24 "syscall" 25 "testing" 26 "time" 27 ) 28 29 const ( 30 ptyFD = 3 // child end of pty. 31 controlFD = 4 // child end of control pipe. 32 ) 33 34 // TestTerminalSignal tests that read from a pseudo-terminal does not return an 35 // error if the process is SIGSTOP'd and put in the background during the read. 36 // 37 // This test simulates stopping a Go process running in a shell with ^Z and 38 // then resuming with `fg`. 39 // 40 // This is a regression test for https://go.dev/issue/22838. On Darwin, PTY 41 // reads return EINTR when this occurs, and Go should automatically retry. 42 func TestTerminalSignal(t *testing.T) { 43 // This test simulates stopping a Go process running in a shell with ^Z 44 // and then resuming with `fg`. This sounds simple, but is actually 45 // quite complicated. 46 // 47 // In principle, what we are doing is: 48 // 1. Creating a new PTY parent/child FD pair. 49 // 2. Create a child that is in the foreground process group of the PTY, and read() from that process. 50 // 3. Stop the child with ^Z. 51 // 4. Take over as foreground process group of the PTY from the parent. 52 // 5. Make the child foreground process group again. 53 // 6. Continue the child. 54 // 55 // On Darwin, step 4 results in the read() returning EINTR once the 56 // process continues. internal/poll should automatically retry the 57 // read. 58 // 59 // These steps are complicated by the rules around foreground process 60 // groups. A process group cannot be foreground if it is "orphaned", 61 // unless it masks SIGTTOU. i.e., to be foreground the process group 62 // must have a parent process group in the same session or mask SIGTTOU 63 // (which we do). An orphaned process group cannot receive 64 // terminal-generated SIGTSTP at all. 65 // 66 // Achieving this requires three processes total: 67 // - Top-level process: this is the main test process and creates the 68 // pseudo-terminal. 69 // - GO_TEST_TERMINAL_SIGNALS=1: This process creates a new process 70 // group and session. The PTY is the controlling terminal for this 71 // session. This process masks SIGTTOU, making it eligible to be a 72 // foreground process group. This process will take over as foreground 73 // from subprocess 2 (step 4 above). 74 // - GO_TEST_TERMINAL_SIGNALS=2: This process create a child process 75 // group of subprocess 1, and is the original foreground process group 76 // for the PTY. This subprocess is the one that is SIGSTOP'd. 77 78 if runtime.GOOS == "dragonfly" { 79 t.Skip("skipping: wait hangs on dragonfly; see https://go.dev/issue/56132") 80 } 81 82 scale := 1 83 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 84 if sc, err := strconv.Atoi(s); err == nil { 85 scale = sc 86 } 87 } 88 pause := time.Duration(scale) * 10 * time.Millisecond 89 90 lvl := os.Getenv("GO_TEST_TERMINAL_SIGNALS") 91 switch lvl { 92 case "": 93 // Main test process, run code below. 94 break 95 case "1": 96 runSessionLeader(t, pause) 97 panic("unreachable") 98 case "2": 99 runStoppingChild() 100 panic("unreachable") 101 default: 102 fmt.Fprintf(os.Stderr, "unknown subprocess level %s\n", lvl) 103 os.Exit(1) 104 } 105 106 t.Parallel() 107 108 pty, procTTYName, err := testpty.Open() 109 if err != nil { 110 ptyErr := err.(*testpty.PtyError) 111 if ptyErr.FuncName == "posix_openpt" && ptyErr.Errno == syscall.EACCES { 112 t.Skip("posix_openpt failed with EACCES, assuming chroot and skipping") 113 } 114 t.Fatal(err) 115 } 116 defer pty.Close() 117 procTTY, err := os.OpenFile(procTTYName, os.O_RDWR, 0) 118 if err != nil { 119 t.Fatal(err) 120 } 121 defer procTTY.Close() 122 123 // Control pipe. GO_TEST_TERMINAL_SIGNALS=2 send the PID of 124 // GO_TEST_TERMINAL_SIGNALS=3 here. After SIGSTOP, it also writes a 125 // byte to indicate that the foreground cycling is complete. 126 controlR, controlW, err := os.Pipe() 127 if err != nil { 128 t.Fatal(err) 129 } 130 131 var ( 132 ctx = context.Background() 133 cmdArgs = []string{"-test.run=^TestTerminalSignal$"} 134 ) 135 if deadline, ok := t.Deadline(); ok { 136 d := time.Until(deadline) 137 var cancel context.CancelFunc 138 ctx, cancel = context.WithTimeout(ctx, d) 139 t.Cleanup(cancel) 140 141 // We run the subprocess with an additional 20% margin to allow it to fail 142 // and clean up gracefully if it times out. 143 cmdArgs = append(cmdArgs, fmt.Sprintf("-test.timeout=%v", d*5/4)) 144 } 145 146 cmd := testenv.CommandContext(t, ctx, os.Args[0], cmdArgs...) 147 cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=1") 148 cmd.Stdin = os.Stdin 149 cmd.Stdout = os.Stdout // for logging 150 cmd.Stderr = os.Stderr 151 cmd.ExtraFiles = []*os.File{procTTY, controlW} 152 cmd.SysProcAttr = &syscall.SysProcAttr{ 153 Setsid: true, 154 Setctty: true, 155 Ctty: ptyFD, 156 } 157 158 if err := cmd.Start(); err != nil { 159 t.Fatal(err) 160 } 161 162 if err := procTTY.Close(); err != nil { 163 t.Errorf("closing procTTY: %v", err) 164 } 165 166 if err := controlW.Close(); err != nil { 167 t.Errorf("closing controlW: %v", err) 168 } 169 170 // Wait for first child to send the second child's PID. 171 b := make([]byte, 8) 172 n, err := controlR.Read(b) 173 if err != nil { 174 t.Fatalf("error reading child pid: %v\n", err) 175 } 176 if n != 8 { 177 t.Fatalf("unexpected short read n = %d\n", n) 178 } 179 pid := binary.LittleEndian.Uint64(b[:]) 180 process, err := os.FindProcess(int(pid)) 181 if err != nil { 182 t.Fatalf("unable to find child process: %v", err) 183 } 184 185 // Wait for the third child to write a byte indicating that it is 186 // entering the read. 187 b = make([]byte, 1) 188 _, err = pty.Read(b) 189 if err != nil { 190 t.Fatalf("error reading from child: %v", err) 191 } 192 193 // Give the program time to enter the read call. 194 // It doesn't matter much if we occasionally don't wait long enough; 195 // we won't be testing what we want to test, but the overall test 196 // will pass. 197 time.Sleep(pause) 198 199 t.Logf("Sending ^Z...") 200 201 // Send a ^Z to stop the program. 202 if _, err := pty.Write([]byte{26}); err != nil { 203 t.Fatalf("writing ^Z to pty: %v", err) 204 } 205 206 // Wait for subprocess 1 to cycle the foreground process group. 207 if _, err := controlR.Read(b); err != nil { 208 t.Fatalf("error reading readiness: %v", err) 209 } 210 211 t.Logf("Sending SIGCONT...") 212 213 // Restart the stopped program. 214 if err := process.Signal(syscall.SIGCONT); err != nil { 215 t.Fatalf("Signal(SIGCONT) got err %v want nil", err) 216 } 217 218 // Write some data for the program to read, which should cause it to 219 // exit. 220 if _, err := pty.Write([]byte{'\n'}); err != nil { 221 t.Fatalf("writing %q to pty: %v", "\n", err) 222 } 223 224 t.Logf("Waiting for exit...") 225 226 if err = cmd.Wait(); err != nil { 227 t.Errorf("subprogram failed: %v", err) 228 } 229 } 230 231 // GO_TEST_TERMINAL_SIGNALS=1 subprocess above. 232 func runSessionLeader(t *testing.T, pause time.Duration) { 233 // "Attempts to use tcsetpgrp() from a process which is a 234 // member of a background process group on a fildes associated 235 // with its controlling terminal shall cause the process group 236 // to be sent a SIGTTOU signal. If the calling thread is 237 // blocking SIGTTOU signals or the process is ignoring SIGTTOU 238 // signals, the process shall be allowed to perform the 239 // operation, and no signal is sent." 240 // -https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetpgrp.html 241 // 242 // We are changing the terminal to put us in the foreground, so 243 // we must ignore SIGTTOU. We are also an orphaned process 244 // group (see above), so we must mask SIGTTOU to be eligible to 245 // become foreground at all. 246 signal.Ignore(syscall.SIGTTOU) 247 248 pty := os.NewFile(ptyFD, "pty") 249 controlW := os.NewFile(controlFD, "control-pipe") 250 251 var ( 252 ctx = context.Background() 253 cmdArgs = []string{"-test.run=^TestTerminalSignal$"} 254 ) 255 if deadline, ok := t.Deadline(); ok { 256 d := time.Until(deadline) 257 var cancel context.CancelFunc 258 ctx, cancel = context.WithTimeout(ctx, d) 259 t.Cleanup(cancel) 260 261 // We run the subprocess with an additional 20% margin to allow it to fail 262 // and clean up gracefully if it times out. 263 cmdArgs = append(cmdArgs, fmt.Sprintf("-test.timeout=%v", d*5/4)) 264 } 265 266 cmd := testenv.CommandContext(t, ctx, os.Args[0], cmdArgs...) 267 cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=2") 268 cmd.Stdin = os.Stdin 269 cmd.Stdout = os.Stdout 270 cmd.Stderr = os.Stderr 271 cmd.ExtraFiles = []*os.File{pty} 272 cmd.SysProcAttr = &syscall.SysProcAttr{ 273 Foreground: true, 274 Ctty: ptyFD, 275 } 276 if err := cmd.Start(); err != nil { 277 fmt.Fprintf(os.Stderr, "error starting second subprocess: %v\n", err) 278 os.Exit(1) 279 } 280 281 fn := func() error { 282 var b [8]byte 283 binary.LittleEndian.PutUint64(b[:], uint64(cmd.Process.Pid)) 284 _, err := controlW.Write(b[:]) 285 if err != nil { 286 return fmt.Errorf("error writing child pid: %w", err) 287 } 288 289 // Wait for stop. 290 var status syscall.WaitStatus 291 for { 292 _, err = syscall.Wait4(cmd.Process.Pid, &status, syscall.WUNTRACED, nil) 293 if err != syscall.EINTR { 294 break 295 } 296 } 297 if err != nil { 298 return fmt.Errorf("error waiting for stop: %w", err) 299 } 300 301 if !status.Stopped() { 302 return fmt.Errorf("unexpected wait status: %v", status) 303 } 304 305 // Take TTY. 306 pgrp := int32(syscall.Getpgrp()) // assume that pid_t is int32 307 if err := unix.Tcsetpgrp(ptyFD, pgrp); err != nil { 308 return fmt.Errorf("error setting tty process group: %w", err) 309 } 310 311 // Give the kernel time to potentially wake readers and have 312 // them return EINTR (darwin does this). 313 time.Sleep(pause) 314 315 // Give TTY back. 316 pid := int32(cmd.Process.Pid) // assume that pid_t is int32 317 if err := unix.Tcsetpgrp(ptyFD, pid); err != nil { 318 return fmt.Errorf("error setting tty process group back: %w", err) 319 } 320 321 // Report that we are done and SIGCONT can be sent. Note that 322 // the actual byte we send doesn't matter. 323 if _, err := controlW.Write(b[:1]); err != nil { 324 return fmt.Errorf("error writing readiness: %w", err) 325 } 326 327 return nil 328 } 329 330 err := fn() 331 if err != nil { 332 fmt.Fprintf(os.Stderr, "session leader error: %v\n", err) 333 cmd.Process.Kill() 334 // Wait for exit below. 335 } 336 337 werr := cmd.Wait() 338 if werr != nil { 339 fmt.Fprintf(os.Stderr, "error running second subprocess: %v\n", err) 340 } 341 342 if err != nil || werr != nil { 343 os.Exit(1) 344 } 345 346 os.Exit(0) 347 } 348 349 // GO_TEST_TERMINAL_SIGNALS=2 subprocess above. 350 func runStoppingChild() { 351 pty := os.NewFile(ptyFD, "pty") 352 353 var b [1]byte 354 if _, err := pty.Write(b[:]); err != nil { 355 fmt.Fprintf(os.Stderr, "error writing byte to PTY: %v\n", err) 356 os.Exit(1) 357 } 358 359 _, err := pty.Read(b[:]) 360 if err != nil { 361 fmt.Fprintln(os.Stderr, err) 362 os.Exit(1) 363 } 364 if b[0] == '\n' { 365 // This is what we expect 366 fmt.Println("read newline") 367 } else { 368 fmt.Fprintf(os.Stderr, "read 1 unexpected byte: %q\n", b) 369 os.Exit(1) 370 } 371 os.Exit(0) 372 }