From 43a123c1ae6613b3efeed291fa552ecd909d3acf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 21:23:18 +0200 Subject: Adding upstream version 1.20.14. Signed-off-by: Daniel Baumann --- src/os/signal/doc.go | 232 +++++++++ src/os/signal/example_test.go | 38 ++ src/os/signal/example_unix_test.go | 47 ++ src/os/signal/sig.s | 8 + src/os/signal/signal.go | 334 +++++++++++++ src/os/signal/signal_cgo_test.go | 350 +++++++++++++ src/os/signal/signal_linux_test.go | 42 ++ src/os/signal/signal_plan9.go | 64 +++ src/os/signal/signal_plan9_test.go | 167 +++++++ src/os/signal/signal_test.go | 932 +++++++++++++++++++++++++++++++++++ src/os/signal/signal_unix.go | 62 +++ src/os/signal/signal_windows_test.go | 99 ++++ 12 files changed, 2375 insertions(+) create mode 100644 src/os/signal/doc.go create mode 100644 src/os/signal/example_test.go create mode 100644 src/os/signal/example_unix_test.go create mode 100644 src/os/signal/sig.s create mode 100644 src/os/signal/signal.go create mode 100644 src/os/signal/signal_cgo_test.go create mode 100644 src/os/signal/signal_linux_test.go create mode 100644 src/os/signal/signal_plan9.go create mode 100644 src/os/signal/signal_plan9_test.go create mode 100644 src/os/signal/signal_test.go create mode 100644 src/os/signal/signal_unix.go create mode 100644 src/os/signal/signal_windows_test.go (limited to 'src/os/signal') diff --git a/src/os/signal/doc.go b/src/os/signal/doc.go new file mode 100644 index 0000000..1b9f40d --- /dev/null +++ b/src/os/signal/doc.go @@ -0,0 +1,232 @@ +// 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. + +/* +Package signal implements access to incoming signals. + +Signals are primarily used on Unix-like systems. For the use of this +package on Windows and Plan 9, see below. + +# Types of signals + +The signals SIGKILL and SIGSTOP may not be caught by a program, and +therefore cannot be affected by this package. + +Synchronous signals are signals triggered by errors in program +execution: SIGBUS, SIGFPE, and SIGSEGV. These are only considered +synchronous when caused by program execution, not when sent using +os.Process.Kill or the kill program or some similar mechanism. In +general, except as discussed below, Go programs will convert a +synchronous signal into a run-time panic. + +The remaining signals are asynchronous signals. They are not +triggered by program errors, but are instead sent from the kernel or +from some other program. + +Of the asynchronous signals, the SIGHUP signal is sent when a program +loses its controlling terminal. The SIGINT signal is sent when the +user at the controlling terminal presses the interrupt character, +which by default is ^C (Control-C). The SIGQUIT signal is sent when +the user at the controlling terminal presses the quit character, which +by default is ^\ (Control-Backslash). In general you can cause a +program to simply exit by pressing ^C, and you can cause it to exit +with a stack dump by pressing ^\. + +# Default behavior of signals in Go programs + +By default, a synchronous signal is converted into a run-time panic. A +SIGHUP, SIGINT, or SIGTERM signal causes the program to exit. A +SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT, or SIGSYS signal +causes the program to exit with a stack dump. A SIGTSTP, SIGTTIN, or +SIGTTOU signal gets the system default behavior (these signals are +used by the shell for job control). The SIGPROF signal is handled +directly by the Go runtime to implement runtime.CPUProfile. Other +signals will be caught but no action will be taken. + +If the Go program is started with either SIGHUP or SIGINT ignored +(signal handler set to SIG_IGN), they will remain ignored. + +If the Go program is started with a non-empty signal mask, that will +generally be honored. However, some signals are explicitly unblocked: +the synchronous signals, SIGILL, SIGTRAP, SIGSTKFLT, SIGCHLD, SIGPROF, +and, on Linux, signals 32 (SIGCANCEL) and 33 (SIGSETXID) +(SIGCANCEL and SIGSETXID are used internally by glibc). Subprocesses +started by os.Exec, or by the os/exec package, will inherit the +modified signal mask. + +# Changing the behavior of signals in Go programs + +The functions in this package allow a program to change the way Go +programs handle signals. + +Notify disables the default behavior for a given set of asynchronous +signals and instead delivers them over one or more registered +channels. Specifically, it applies to the signals SIGHUP, SIGINT, +SIGQUIT, SIGABRT, and SIGTERM. It also applies to the job control +signals SIGTSTP, SIGTTIN, and SIGTTOU, in which case the system +default behavior does not occur. It also applies to some signals that +otherwise cause no action: SIGUSR1, SIGUSR2, SIGPIPE, SIGALRM, +SIGCHLD, SIGCONT, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGWINCH, +SIGIO, SIGPWR, SIGSYS, SIGINFO, SIGTHR, SIGWAITING, SIGLWP, SIGFREEZE, +SIGTHAW, SIGLOST, SIGXRES, SIGJVM1, SIGJVM2, and any real time signals +used on the system. Note that not all of these signals are available +on all systems. + +If the program was started with SIGHUP or SIGINT ignored, and Notify +is called for either signal, a signal handler will be installed for +that signal and it will no longer be ignored. If, later, Reset or +Ignore is called for that signal, or Stop is called on all channels +passed to Notify for that signal, the signal will once again be +ignored. Reset will restore the system default behavior for the +signal, while Ignore will cause the system to ignore the signal +entirely. + +If the program is started with a non-empty signal mask, some signals +will be explicitly unblocked as described above. If Notify is called +for a blocked signal, it will be unblocked. If, later, Reset is +called for that signal, or Stop is called on all channels passed to +Notify for that signal, the signal will once again be blocked. + +# SIGPIPE + +When a Go program writes to a broken pipe, the kernel will raise a +SIGPIPE signal. + +If the program has not called Notify to receive SIGPIPE signals, then +the behavior depends on the file descriptor number. A write to a +broken pipe on file descriptors 1 or 2 (standard output or standard +error) will cause the program to exit with a SIGPIPE signal. A write +to a broken pipe on some other file descriptor will take no action on +the SIGPIPE signal, and the write will fail with an EPIPE error. + +If the program has called Notify to receive SIGPIPE signals, the file +descriptor number does not matter. The SIGPIPE signal will be +delivered to the Notify channel, and the write will fail with an EPIPE +error. + +This means that, by default, command line programs will behave like +typical Unix command line programs, while other programs will not +crash with SIGPIPE when writing to a closed network connection. + +# Go programs that use cgo or SWIG + +In a Go program that includes non-Go code, typically C/C++ code +accessed using cgo or SWIG, Go's startup code normally runs first. It +configures the signal handlers as expected by the Go runtime, before +the non-Go startup code runs. If the non-Go startup code wishes to +install its own signal handlers, it must take certain steps to keep Go +working well. This section documents those steps and the overall +effect changes to signal handler settings by the non-Go code can have +on Go programs. In rare cases, the non-Go code may run before the Go +code, in which case the next section also applies. + +If the non-Go code called by the Go program does not change any signal +handlers or masks, then the behavior is the same as for a pure Go +program. + +If the non-Go code installs any signal handlers, it must use the +SA_ONSTACK flag with sigaction. Failing to do so is likely to cause +the program to crash if the signal is received. Go programs routinely +run with a limited stack, and therefore set up an alternate signal +stack. + +If the non-Go code installs a signal handler for any of the +synchronous signals (SIGBUS, SIGFPE, SIGSEGV), then it should record +the existing Go signal handler. If those signals occur while +executing Go code, it should invoke the Go signal handler (whether the +signal occurs while executing Go code can be determined by looking at +the PC passed to the signal handler). Otherwise some Go run-time +panics will not occur as expected. + +If the non-Go code installs a signal handler for any of the +asynchronous signals, it may invoke the Go signal handler or not as it +chooses. Naturally, if it does not invoke the Go signal handler, the +Go behavior described above will not occur. This can be an issue with +the SIGPROF signal in particular. + +The non-Go code should not change the signal mask on any threads +created by the Go runtime. If the non-Go code starts new threads of +its own, it may set the signal mask as it pleases. + +If the non-Go code starts a new thread, changes the signal mask, and +then invokes a Go function in that thread, the Go runtime will +automatically unblock certain signals: the synchronous signals, +SIGILL, SIGTRAP, SIGSTKFLT, SIGCHLD, SIGPROF, SIGCANCEL, and +SIGSETXID. When the Go function returns, the non-Go signal mask will +be restored. + +If the Go signal handler is invoked on a non-Go thread not running Go +code, the handler generally forwards the signal to the non-Go code, as +follows. If the signal is SIGPROF, the Go handler does +nothing. Otherwise, the Go handler removes itself, unblocks the +signal, and raises it again, to invoke any non-Go handler or default +system handler. If the program does not exit, the Go handler then +reinstalls itself and continues execution of the program. + +If a SIGPIPE signal is received, the Go program will invoke the +special handling described above if the SIGPIPE is received on a Go +thread. If the SIGPIPE is received on a non-Go thread the signal will +be forwarded to the non-Go handler, if any; if there is none the +default system handler will cause the program to terminate. + +# Non-Go programs that call Go code + +When Go code is built with options like -buildmode=c-shared, it will +be run as part of an existing non-Go program. The non-Go code may +have already installed signal handlers when the Go code starts (that +may also happen in unusual cases when using cgo or SWIG; in that case, +the discussion here applies). For -buildmode=c-archive the Go runtime +will initialize signals at global constructor time. For +-buildmode=c-shared the Go runtime will initialize signals when the +shared library is loaded. + +If the Go runtime sees an existing signal handler for the SIGCANCEL or +SIGSETXID signals (which are used only on Linux), it will turn on +the SA_ONSTACK flag and otherwise keep the signal handler. + +For the synchronous signals and SIGPIPE, the Go runtime will install a +signal handler. It will save any existing signal handler. If a +synchronous signal arrives while executing non-Go code, the Go runtime +will invoke the existing signal handler instead of the Go signal +handler. + +Go code built with -buildmode=c-archive or -buildmode=c-shared will +not install any other signal handlers by default. If there is an +existing signal handler, the Go runtime will turn on the SA_ONSTACK +flag and otherwise keep the signal handler. If Notify is called for an +asynchronous signal, a Go signal handler will be installed for that +signal. If, later, Reset is called for that signal, the original +handling for that signal will be reinstalled, restoring the non-Go +signal handler if any. + +Go code built without -buildmode=c-archive or -buildmode=c-shared will +install a signal handler for the asynchronous signals listed above, +and save any existing signal handler. If a signal is delivered to a +non-Go thread, it will act as described above, except that if there is +an existing non-Go signal handler, that handler will be installed +before raising the signal. + +# Windows + +On Windows a ^C (Control-C) or ^BREAK (Control-Break) normally cause +the program to exit. If Notify is called for os.Interrupt, ^C or ^BREAK +will cause os.Interrupt to be sent on the channel, and the program will +not exit. If Reset is called, or Stop is called on all channels passed +to Notify, then the default behavior will be restored. + +Additionally, if Notify is called, and Windows sends CTRL_CLOSE_EVENT, +CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT to the process, Notify will +return syscall.SIGTERM. Unlike Control-C and Control-Break, Notify does +not change process behavior when either CTRL_CLOSE_EVENT, +CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT is received - the process will +still get terminated unless it exits. But receiving syscall.SIGTERM will +give the process an opportunity to clean up before termination. + +# Plan 9 + +On Plan 9, signals have type syscall.Note, which is a string. Calling +Notify with a syscall.Note will cause that value to be sent on the +channel when that string is posted as a note. +*/ +package signal diff --git a/src/os/signal/example_test.go b/src/os/signal/example_test.go new file mode 100644 index 0000000..ecefc75 --- /dev/null +++ b/src/os/signal/example_test.go @@ -0,0 +1,38 @@ +// Copyright 2013 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. + +package signal_test + +import ( + "fmt" + "os" + "os/signal" +) + +func ExampleNotify() { + // Set up channel on which to send signal notifications. + // We must use a buffered channel or risk missing the signal + // if we're not ready to receive when the signal is sent. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + // Block until a signal is received. + s := <-c + fmt.Println("Got signal:", s) +} + +func ExampleNotify_allSignals() { + // Set up channel on which to send signal notifications. + // We must use a buffered channel or risk missing the signal + // if we're not ready to receive when the signal is sent. + c := make(chan os.Signal, 1) + + // Passing no signals to Notify means that + // all signals will be sent to the channel. + signal.Notify(c) + + // Block until any signal is received. + s := <-c + fmt.Println("Got signal:", s) +} diff --git a/src/os/signal/example_unix_test.go b/src/os/signal/example_unix_test.go new file mode 100644 index 0000000..b7047ac --- /dev/null +++ b/src/os/signal/example_unix_test.go @@ -0,0 +1,47 @@ +// Copyright 2020 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 signal_test + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "time" +) + +// This example passes a context with a signal to tell a blocking function that +// it should abandon its work after a signal is received. +func ExampleNotifyContext() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + p, err := os.FindProcess(os.Getpid()) + if err != nil { + log.Fatal(err) + } + + // On a Unix-like system, pressing Ctrl+C on a keyboard sends a + // SIGINT signal to the process of the program in execution. + // + // This example simulates that by sending a SIGINT signal to itself. + if err := p.Signal(os.Interrupt); err != nil { + log.Fatal(err) + } + + select { + case <-time.After(time.Second): + fmt.Println("missed signal") + case <-ctx.Done(): + fmt.Println(ctx.Err()) // prints "context canceled" + stop() // stop receiving signal notifications as soon as possible. + } + + // Output: + // context canceled +} diff --git a/src/os/signal/sig.s b/src/os/signal/sig.s new file mode 100644 index 0000000..12833a8 --- /dev/null +++ b/src/os/signal/sig.s @@ -0,0 +1,8 @@ +// Copyright 2012 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. + +// The runtime package uses //go:linkname to push a few functions into this +// package but we still need a .s file so the Go tool does not pass -complete +// to the go tool compile so the latter does not complain about Go functions +// with no bodies. diff --git a/src/os/signal/signal.go b/src/os/signal/signal.go new file mode 100644 index 0000000..4250a7e --- /dev/null +++ b/src/os/signal/signal.go @@ -0,0 +1,334 @@ +// Copyright 2012 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. + +package signal + +import ( + "context" + "os" + "sync" +) + +var handlers struct { + sync.Mutex + // Map a channel to the signals that should be sent to it. + m map[chan<- os.Signal]*handler + // Map a signal to the number of channels receiving it. + ref [numSig]int64 + // Map channels to signals while the channel is being stopped. + // Not a map because entries live here only very briefly. + // We need a separate container because we need m to correspond to ref + // at all times, and we also need to keep track of the *handler + // value for a channel being stopped. See the Stop function. + stopping []stopping +} + +type stopping struct { + c chan<- os.Signal + h *handler +} + +type handler struct { + mask [(numSig + 31) / 32]uint32 +} + +func (h *handler) want(sig int) bool { + return (h.mask[sig/32]>>uint(sig&31))&1 != 0 +} + +func (h *handler) set(sig int) { + h.mask[sig/32] |= 1 << uint(sig&31) +} + +func (h *handler) clear(sig int) { + h.mask[sig/32] &^= 1 << uint(sig&31) +} + +// Stop relaying the signals, sigs, to any channels previously registered to +// receive them and either reset the signal handlers to their original values +// (action=disableSignal) or ignore the signals (action=ignoreSignal). +func cancel(sigs []os.Signal, action func(int)) { + handlers.Lock() + defer handlers.Unlock() + + remove := func(n int) { + var zerohandler handler + + for c, h := range handlers.m { + if h.want(n) { + handlers.ref[n]-- + h.clear(n) + if h.mask == zerohandler.mask { + delete(handlers.m, c) + } + } + } + + action(n) + } + + if len(sigs) == 0 { + for n := 0; n < numSig; n++ { + remove(n) + } + } else { + for _, s := range sigs { + remove(signum(s)) + } + } +} + +// Ignore causes the provided signals to be ignored. If they are received by +// the program, nothing will happen. Ignore undoes the effect of any prior +// calls to Notify for the provided signals. +// If no signals are provided, all incoming signals will be ignored. +func Ignore(sig ...os.Signal) { + cancel(sig, ignoreSignal) +} + +// Ignored reports whether sig is currently ignored. +func Ignored(sig os.Signal) bool { + sn := signum(sig) + return sn >= 0 && signalIgnored(sn) +} + +var ( + // watchSignalLoopOnce guards calling the conditionally + // initialized watchSignalLoop. If watchSignalLoop is non-nil, + // it will be run in a goroutine lazily once Notify is invoked. + // See Issue 21576. + watchSignalLoopOnce sync.Once + watchSignalLoop func() +) + +// Notify causes package signal to relay incoming signals to c. +// If no signals are provided, all incoming signals will be relayed to c. +// Otherwise, just the provided signals will. +// +// Package signal will not block sending to c: the caller must ensure +// that c has sufficient buffer space to keep up with the expected +// signal rate. For a channel used for notification of just one signal value, +// a buffer of size 1 is sufficient. +// +// It is allowed to call Notify multiple times with the same channel: +// each call expands the set of signals sent to that channel. +// The only way to remove signals from the set is to call Stop. +// +// It is allowed to call Notify multiple times with different channels +// and the same signals: each channel receives copies of incoming +// signals independently. +func Notify(c chan<- os.Signal, sig ...os.Signal) { + if c == nil { + panic("os/signal: Notify using nil channel") + } + + handlers.Lock() + defer handlers.Unlock() + + h := handlers.m[c] + if h == nil { + if handlers.m == nil { + handlers.m = make(map[chan<- os.Signal]*handler) + } + h = new(handler) + handlers.m[c] = h + } + + add := func(n int) { + if n < 0 { + return + } + if !h.want(n) { + h.set(n) + if handlers.ref[n] == 0 { + enableSignal(n) + + // The runtime requires that we enable a + // signal before starting the watcher. + watchSignalLoopOnce.Do(func() { + if watchSignalLoop != nil { + go watchSignalLoop() + } + }) + } + handlers.ref[n]++ + } + } + + if len(sig) == 0 { + for n := 0; n < numSig; n++ { + add(n) + } + } else { + for _, s := range sig { + add(signum(s)) + } + } +} + +// Reset undoes the effect of any prior calls to Notify for the provided +// signals. +// If no signals are provided, all signal handlers will be reset. +func Reset(sig ...os.Signal) { + cancel(sig, disableSignal) +} + +// Stop causes package signal to stop relaying incoming signals to c. +// It undoes the effect of all prior calls to Notify using c. +// When Stop returns, it is guaranteed that c will receive no more signals. +func Stop(c chan<- os.Signal) { + handlers.Lock() + + h := handlers.m[c] + if h == nil { + handlers.Unlock() + return + } + delete(handlers.m, c) + + for n := 0; n < numSig; n++ { + if h.want(n) { + handlers.ref[n]-- + if handlers.ref[n] == 0 { + disableSignal(n) + } + } + } + + // Signals will no longer be delivered to the channel. + // We want to avoid a race for a signal such as SIGINT: + // it should be either delivered to the channel, + // or the program should take the default action (that is, exit). + // To avoid the possibility that the signal is delivered, + // and the signal handler invoked, and then Stop deregisters + // the channel before the process function below has a chance + // to send it on the channel, put the channel on a list of + // channels being stopped and wait for signal delivery to + // quiesce before fully removing it. + + handlers.stopping = append(handlers.stopping, stopping{c, h}) + + handlers.Unlock() + + signalWaitUntilIdle() + + handlers.Lock() + + for i, s := range handlers.stopping { + if s.c == c { + handlers.stopping = append(handlers.stopping[:i], handlers.stopping[i+1:]...) + break + } + } + + handlers.Unlock() +} + +// Wait until there are no more signals waiting to be delivered. +// Defined by the runtime package. +func signalWaitUntilIdle() + +func process(sig os.Signal) { + n := signum(sig) + if n < 0 { + return + } + + handlers.Lock() + defer handlers.Unlock() + + for c, h := range handlers.m { + if h.want(n) { + // send but do not block for it + select { + case c <- sig: + default: + } + } + } + + // Avoid the race mentioned in Stop. + for _, d := range handlers.stopping { + if d.h.want(n) { + select { + case d.c <- sig: + default: + } + } + } +} + +// NotifyContext returns a copy of the parent context that is marked done +// (its Done channel is closed) when one of the listed signals arrives, +// when the returned stop function is called, or when the parent context's +// Done channel is closed, whichever happens first. +// +// The stop function unregisters the signal behavior, which, like signal.Reset, +// may restore the default behavior for a given signal. For example, the default +// behavior of a Go program receiving os.Interrupt is to exit. Calling +// NotifyContext(parent, os.Interrupt) will change the behavior to cancel +// the returned context. Future interrupts received will not trigger the default +// (exit) behavior until the returned stop function is called. +// +// The stop function releases resources associated with it, so code should +// call stop as soon as the operations running in this Context complete and +// signals no longer need to be diverted to the context. +func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) { + ctx, cancel := context.WithCancel(parent) + c := &signalCtx{ + Context: ctx, + cancel: cancel, + signals: signals, + } + c.ch = make(chan os.Signal, 1) + Notify(c.ch, c.signals...) + if ctx.Err() == nil { + go func() { + select { + case <-c.ch: + c.cancel() + case <-c.Done(): + } + }() + } + return c, c.stop +} + +type signalCtx struct { + context.Context + + cancel context.CancelFunc + signals []os.Signal + ch chan os.Signal +} + +func (c *signalCtx) stop() { + c.cancel() + Stop(c.ch) +} + +type stringer interface { + String() string +} + +func (c *signalCtx) String() string { + var buf []byte + // We know that the type of c.Context is context.cancelCtx, and we know that the + // String method of cancelCtx returns a string that ends with ".WithCancel". + name := c.Context.(stringer).String() + name = name[:len(name)-len(".WithCancel")] + buf = append(buf, "signal.NotifyContext("+name...) + if len(c.signals) != 0 { + buf = append(buf, ", ["...) + for i, s := range c.signals { + buf = append(buf, s.String()...) + if i != len(c.signals)-1 { + buf = append(buf, ' ') + } + } + buf = append(buf, ']') + } + buf = append(buf, ')') + return string(buf) +} diff --git a/src/os/signal/signal_cgo_test.go b/src/os/signal/signal_cgo_test.go new file mode 100644 index 0000000..ac59215 --- /dev/null +++ b/src/os/signal/signal_cgo_test.go @@ -0,0 +1,350 @@ +// 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 ( + "context" + "encoding/binary" + "fmt" + "internal/testpty" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "syscall" + "testing" + "time" + "unsafe" +) + +const ( + ptyFD = 3 // child end of pty. + controlFD = 4 // child end of control pipe. +) + +// TestTerminalSignal tests that read from a pseudo-terminal does not return an +// error if the process is SIGSTOP'd and put in the background during the read. +// +// This test simulates stopping a Go process running in a shell with ^Z and +// then resuming with `fg`. +// +// This is a regression test for https://go.dev/issue/22838. On Darwin, PTY +// reads return EINTR when this occurs, and Go should automatically retry. +func TestTerminalSignal(t *testing.T) { + // This test simulates stopping a Go process running in a shell with ^Z + // and then resuming with `fg`. This sounds simple, but is actually + // quite complicated. + // + // In principle, what we are doing is: + // 1. Creating a new PTY parent/child FD pair. + // 2. Create a child that is in the foreground process group of the PTY, and read() from that process. + // 3. Stop the child with ^Z. + // 4. Take over as foreground process group of the PTY from the parent. + // 5. Make the child foreground process group again. + // 6. Continue the child. + // + // On Darwin, step 4 results in the read() returning EINTR once the + // process continues. internal/poll should automatically retry the + // read. + // + // These steps are complicated by the rules around foreground process + // groups. A process group cannot be foreground if it is "orphaned", + // unless it masks SIGTTOU. i.e., to be foreground the process group + // must have a parent process group in the same session or mask SIGTTOU + // (which we do). An orphaned process group cannot receive + // terminal-generated SIGTSTP at all. + // + // Achieving this requires three processes total: + // - Top-level process: this is the main test process and creates the + // pseudo-terminal. + // - GO_TEST_TERMINAL_SIGNALS=1: This process creates a new process + // group and session. The PTY is the controlling terminal for this + // session. This process masks SIGTTOU, making it eligible to be a + // foreground process group. This process will take over as foreground + // from subprocess 2 (step 4 above). + // - GO_TEST_TERMINAL_SIGNALS=2: This process create a child process + // group of subprocess 1, and is the original foreground process group + // for the PTY. This subprocess is the one that is SIGSTOP'd. + + if runtime.GOOS == "dragonfly" { + t.Skip("skipping: wait hangs on dragonfly; see https://go.dev/issue/56132") + } + + 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 + + lvl := os.Getenv("GO_TEST_TERMINAL_SIGNALS") + switch lvl { + case "": + // Main test process, run code below. + break + case "1": + runSessionLeader(pause) + panic("unreachable") + case "2": + runStoppingChild() + panic("unreachable") + default: + fmt.Fprintf(os.Stderr, "unknown subprocess level %s\n", lvl) + os.Exit(1) + } + + t.Parallel() + + pty, procTTYName, err := testpty.Open() + if err != nil { + ptyErr := err.(*testpty.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() + + // Control pipe. GO_TEST_TERMINAL_SIGNALS=2 send the PID of + // GO_TEST_TERMINAL_SIGNALS=3 here. After SIGSTOP, it also writes a + // byte to indicate that the foreground cycling is complete. + controlR, controlW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestTerminalSignal") + cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=1") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout // for logging + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{procTTY, controlW} + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + Setctty: true, + Ctty: ptyFD, + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + if err := procTTY.Close(); err != nil { + t.Errorf("closing procTTY: %v", err) + } + + if err := controlW.Close(); err != nil { + t.Errorf("closing controlW: %v", err) + } + + // Wait for first child to send the second child's PID. + b := make([]byte, 8) + n, err := controlR.Read(b) + if err != nil { + t.Fatalf("error reading child pid: %v\n", err) + } + if n != 8 { + t.Fatalf("unexpected short read n = %d\n", n) + } + pid := binary.LittleEndian.Uint64(b[:]) + process, err := os.FindProcess(int(pid)) + if err != nil { + t.Fatalf("unable to find child process: %v", err) + } + + // Wait for the third child to write a byte indicating that it is + // entering the read. + b = make([]byte, 1) + _, err = pty.Read(b) + if err != nil { + t.Fatalf("error reading from child: %v", err) + } + + // 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) + + t.Logf("Sending ^Z...") + + // 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 subprocess 1 to cycle the foreground process group. + if _, err := controlR.Read(b); err != nil { + t.Fatalf("error reading readiness: %v", err) + } + + t.Logf("Sending SIGCONT...") + + // Restart the stopped program. + if err := process.Signal(syscall.SIGCONT); err != nil { + t.Fatalf("Signal(SIGCONT) got err %v want nil", err) + } + + // 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) + } + + t.Logf("Waiting for exit...") + + if err = cmd.Wait(); err != nil { + t.Errorf("subprogram failed: %v", err) + } +} + +// GO_TEST_TERMINAL_SIGNALS=1 subprocess above. +func runSessionLeader(pause time.Duration) { + // "Attempts to use tcsetpgrp() from a process which is a + // member of a background process group on a fildes associated + // with its controlling terminal shall cause the process group + // to be sent a SIGTTOU signal. If the calling thread is + // blocking SIGTTOU signals or the process is ignoring SIGTTOU + // signals, the process shall be allowed to perform the + // operation, and no signal is sent." + // -https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetpgrp.html + // + // We are changing the terminal to put us in the foreground, so + // we must ignore SIGTTOU. We are also an orphaned process + // group (see above), so we must mask SIGTTOU to be eligible to + // become foreground at all. + signal.Ignore(syscall.SIGTTOU) + + pty := os.NewFile(ptyFD, "pty") + controlW := os.NewFile(controlFD, "control-pipe") + + // Slightly shorter timeout than in the parent. + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestTerminalSignal") + cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=2") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{pty} + cmd.SysProcAttr = &syscall.SysProcAttr{ + Foreground: true, + Ctty: ptyFD, + } + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting second subprocess: %v\n", err) + os.Exit(1) + } + + fn := func() error { + var b [8]byte + binary.LittleEndian.PutUint64(b[:], uint64(cmd.Process.Pid)) + _, err := controlW.Write(b[:]) + if err != nil { + return fmt.Errorf("error writing child pid: %w", err) + } + + // Wait for stop. + var status syscall.WaitStatus + var errno syscall.Errno + for { + _, _, errno = syscall.Syscall6(syscall.SYS_WAIT4, uintptr(cmd.Process.Pid), uintptr(unsafe.Pointer(&status)), syscall.WUNTRACED, 0, 0, 0) + if errno != syscall.EINTR { + break + } + } + if errno != 0 { + return fmt.Errorf("error waiting for stop: %w", errno) + } + + if !status.Stopped() { + return fmt.Errorf("unexpected wait status: %v", status) + } + + // Take TTY. + pgrp := int32(syscall.Getpgrp()) // assume that pid_t is int32 + _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, ptyFD, syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pgrp))) + if errno != 0 { + return fmt.Errorf("error setting tty process group: %w", errno) + } + + // Give the kernel time to potentially wake readers and have + // them return EINTR (darwin does this). + time.Sleep(pause) + + // Give TTY back. + pid := int32(cmd.Process.Pid) // assume that pid_t is int32 + _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, ptyFD, syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pid))) + if errno != 0 { + return fmt.Errorf("error setting tty process group back: %w", errno) + } + + // Report that we are done and SIGCONT can be sent. Note that + // the actual byte we send doesn't matter. + if _, err := controlW.Write(b[:1]); err != nil { + return fmt.Errorf("error writing readiness: %w", err) + } + + return nil + } + + err := fn() + if err != nil { + fmt.Fprintf(os.Stderr, "session leader error: %v\n", err) + cmd.Process.Kill() + // Wait for exit below. + } + + werr := cmd.Wait() + if werr != nil { + fmt.Fprintf(os.Stderr, "error running second subprocess: %v\n", err) + } + + if err != nil || werr != nil { + os.Exit(1) + } + + os.Exit(0) +} + +// GO_TEST_TERMINAL_SIGNALS=2 subprocess above. +func runStoppingChild() { + pty := os.NewFile(ptyFD, "pty") + + var b [1]byte + if _, err := pty.Write(b[:]); err != nil { + fmt.Fprintf(os.Stderr, "error writing byte to PTY: %v\n", err) + os.Exit(1) + } + + _, err := pty.Read(b[:]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if b[0] == '\n' { + // This is what we expect + fmt.Println("read newline") + } else { + fmt.Fprintf(os.Stderr, "read 1 unexpected byte: %q\n", b) + os.Exit(1) + } + os.Exit(0) +} diff --git a/src/os/signal/signal_linux_test.go b/src/os/signal/signal_linux_test.go new file mode 100644 index 0000000..f70f108 --- /dev/null +++ b/src/os/signal/signal_linux_test.go @@ -0,0 +1,42 @@ +// Copyright 2020 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 linux + +package signal + +import ( + "os" + "syscall" + "testing" + "time" +) + +const prSetKeepCaps = 8 + +// This test validates that syscall.AllThreadsSyscall() can reliably +// reach all 'm' (threads) of the nocgo runtime even when one thread +// is blocked waiting to receive signals from the kernel. This monitors +// for a regression vs. the fix for #43149. +func TestAllThreadsSyscallSignals(t *testing.T) { + if _, _, err := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, prSetKeepCaps, 0, 0); err == syscall.ENOTSUP { + t.Skip("AllThreadsSyscall disabled with cgo") + } + + sig := make(chan os.Signal, 1) + Notify(sig, os.Interrupt) + + for i := 0; i <= 100; i++ { + if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, prSetKeepCaps, uintptr(i&1), 0); errno != 0 { + t.Fatalf("[%d] failed to set KEEP_CAPS=%d: %v", i, i&1, errno) + } + } + + select { + case <-time.After(10 * time.Millisecond): + case <-sig: + t.Fatal("unexpected signal") + } + Stop(sig) +} diff --git a/src/os/signal/signal_plan9.go b/src/os/signal/signal_plan9.go new file mode 100644 index 0000000..7d48715 --- /dev/null +++ b/src/os/signal/signal_plan9.go @@ -0,0 +1,64 @@ +// Copyright 2012 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. + +package signal + +import ( + "os" + "syscall" +) + +var sigtab = make(map[os.Signal]int) + +// Defined by the runtime package. +func signal_disable(uint32) +func signal_enable(uint32) +func signal_ignore(uint32) +func signal_ignored(uint32) bool +func signal_recv() string + +func init() { + watchSignalLoop = loop +} + +func loop() { + for { + process(syscall.Note(signal_recv())) + } +} + +const numSig = 256 + +func signum(sig os.Signal) int { + switch sig := sig.(type) { + case syscall.Note: + n, ok := sigtab[sig] + if !ok { + n = len(sigtab) + 1 + if n > numSig { + return -1 + } + sigtab[sig] = n + } + return n + default: + return -1 + } +} + +func enableSignal(sig int) { + signal_enable(uint32(sig)) +} + +func disableSignal(sig int) { + signal_disable(uint32(sig)) +} + +func ignoreSignal(sig int) { + signal_ignore(uint32(sig)) +} + +func signalIgnored(sig int) bool { + return signal_ignored(uint32(sig)) +} diff --git a/src/os/signal/signal_plan9_test.go b/src/os/signal/signal_plan9_test.go new file mode 100644 index 0000000..8357199 --- /dev/null +++ b/src/os/signal/signal_plan9_test.go @@ -0,0 +1,167 @@ +// Copyright 2009 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. + +package signal + +import ( + "internal/itoa" + "os" + "runtime" + "syscall" + "testing" + "time" +) + +func waitSig(t *testing.T, c <-chan os.Signal, sig os.Signal) { + select { + case s := <-c: + if s != sig { + t.Fatalf("signal was %v, want %v", s, sig) + } + case <-time.After(1 * time.Second): + t.Fatalf("timeout waiting for %v", sig) + } +} + +// Test that basic signal handling works. +func TestSignal(t *testing.T) { + // Ask for hangup + c := make(chan os.Signal, 1) + Notify(c, syscall.Note("hangup")) + defer Stop(c) + + // Send this process a hangup + t.Logf("hangup...") + postNote(syscall.Getpid(), "hangup") + waitSig(t, c, syscall.Note("hangup")) + + // Ask for everything we can get. + c1 := make(chan os.Signal, 1) + Notify(c1) + + // Send this process an alarm + t.Logf("alarm...") + postNote(syscall.Getpid(), "alarm") + waitSig(t, c1, syscall.Note("alarm")) + + // Send two more hangups, to make sure that + // they get delivered on c1 and that not reading + // from c does not block everything. + t.Logf("hangup...") + postNote(syscall.Getpid(), "hangup") + waitSig(t, c1, syscall.Note("hangup")) + t.Logf("hangup...") + postNote(syscall.Getpid(), "hangup") + waitSig(t, c1, syscall.Note("hangup")) + + // The first SIGHUP should be waiting for us on c. + waitSig(t, c, syscall.Note("hangup")) +} + +func TestStress(t *testing.T) { + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + done := make(chan bool) + finished := make(chan bool) + go func() { + sig := make(chan os.Signal, 1) + Notify(sig, syscall.Note("alarm")) + defer Stop(sig) + Loop: + for { + select { + case <-sig: + case <-done: + break Loop + } + } + finished <- true + }() + go func() { + Loop: + for { + select { + case <-done: + break Loop + default: + postNote(syscall.Getpid(), "alarm") + runtime.Gosched() + } + } + finished <- true + }() + time.Sleep(dur) + close(done) + <-finished + <-finished + // When run with 'go test -cpu=1,2,4' alarm from this test can slip + // into subsequent TestSignal() causing failure. + // Sleep for a while to reduce the possibility of the failure. + time.Sleep(10 * time.Millisecond) +} + +// Test that Stop cancels the channel's registrations. +func TestStop(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + sigs := []string{ + "alarm", + "hangup", + } + + for _, sig := range sigs { + // Send the signal. + // If it's alarm, we should not see it. + // If it's hangup, maybe we'll die. Let the flag tell us what to do. + if sig != "hangup" { + postNote(syscall.Getpid(), sig) + } + time.Sleep(100 * time.Millisecond) + + // Ask for signal + c := make(chan os.Signal, 1) + Notify(c, syscall.Note(sig)) + defer Stop(c) + + // Send this process that signal + postNote(syscall.Getpid(), sig) + waitSig(t, c, syscall.Note(sig)) + + Stop(c) + select { + case s := <-c: + t.Fatalf("unexpected signal %v", s) + case <-time.After(100 * time.Millisecond): + // nothing to read - good + } + + // Send the signal. + // If it's alarm, we should not see it. + // If it's hangup, maybe we'll die. Let the flag tell us what to do. + if sig != "hangup" { + postNote(syscall.Getpid(), sig) + } + + select { + case s := <-c: + t.Fatalf("unexpected signal %v", s) + case <-time.After(100 * time.Millisecond): + // nothing to read - good + } + } +} + +func postNote(pid int, note string) error { + f, err := os.OpenFile("/proc/"+itoa.Itoa(pid)+"/note", os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write([]byte(note)) + return err +} diff --git a/src/os/signal/signal_test.go b/src/os/signal/signal_test.go new file mode 100644 index 0000000..61a98c1 --- /dev/null +++ b/src/os/signal/signal_test.go @@ -0,0 +1,932 @@ +// Copyright 2009 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 signal + +import ( + "bytes" + "context" + "flag" + "fmt" + "internal/testenv" + "os" + "os/exec" + "runtime" + "runtime/trace" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "time" +) + +// settleTime is an upper bound on how long we expect signals to take to be +// delivered. Lower values make the test faster, but also flakier — especially +// on heavily loaded systems. +// +// The current value is set based on flakes observed in the Go builders. +var settleTime = 100 * time.Millisecond + +// fatalWaitingTime is an absurdly long time to wait for signals to be +// delivered but, using it, we (hopefully) eliminate test flakes on the +// build servers. See #46736 for discussion. +var fatalWaitingTime = 30 * time.Second + +func init() { + if testenv.Builder() == "solaris-amd64-oraclerel" { + // The solaris-amd64-oraclerel builder has been observed to time out in + // TestNohup even with a 250ms settle time. + // + // Use a much longer settle time on that builder to try to suss out whether + // the test is flaky due to builder slowness (which may mean we need a + // longer GO_TEST_TIMEOUT_SCALE) or due to a dropped signal (which may + // instead need a test-skip and upstream bug filed against the Solaris + // kernel). + // + // See https://golang.org/issue/33174. + settleTime = 5 * time.Second + } else if runtime.GOOS == "linux" && strings.HasPrefix(runtime.GOARCH, "ppc64") { + // Older linux kernels seem to have some hiccups delivering the signal + // in a timely manner on ppc64 and ppc64le. When running on a + // ppc64le/ubuntu 16.04/linux 4.4 host the time can vary quite + // substantially even on a idle system. 5 seconds is twice any value + // observed when running 10000 tests on such a system. + settleTime = 5 * time.Second + } else if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + if scale, err := strconv.Atoi(s); err == nil { + settleTime *= time.Duration(scale) + } + } +} + +func waitSig(t *testing.T, c <-chan os.Signal, sig os.Signal) { + t.Helper() + waitSig1(t, c, sig, false) +} +func waitSigAll(t *testing.T, c <-chan os.Signal, sig os.Signal) { + t.Helper() + waitSig1(t, c, sig, true) +} + +func waitSig1(t *testing.T, c <-chan os.Signal, sig os.Signal, all bool) { + t.Helper() + + // Sleep multiple times to give the kernel more tries to + // deliver the signal. + start := time.Now() + timer := time.NewTimer(settleTime / 10) + defer timer.Stop() + // If the caller notified for all signals on c, filter out SIGURG, + // which is used for runtime preemption and can come at unpredictable times. + // General user code should filter out all unexpected signals instead of just + // SIGURG, but since os/signal is tightly coupled to the runtime it seems + // appropriate to be stricter here. + for time.Since(start) < fatalWaitingTime { + select { + case s := <-c: + if s == sig { + return + } + if !all || s != syscall.SIGURG { + t.Fatalf("signal was %v, want %v", s, sig) + } + case <-timer.C: + timer.Reset(settleTime / 10) + } + } + t.Fatalf("timeout after %v waiting for %v", fatalWaitingTime, sig) +} + +// quiesce waits until we can be reasonably confident that all pending signals +// have been delivered by the OS. +func quiesce() { + // The kernel will deliver a signal as a thread returns + // from a syscall. If the only active thread is sleeping, + // and the system is busy, the kernel may not get around + // to waking up a thread to catch the signal. + // We try splitting up the sleep to give the kernel + // many chances to deliver the signal. + start := time.Now() + for time.Since(start) < settleTime { + time.Sleep(settleTime / 10) + } +} + +// Test that basic signal handling works. +func TestSignal(t *testing.T) { + // Ask for SIGHUP + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + defer Stop(c) + + // Send this process a SIGHUP + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSig(t, c, syscall.SIGHUP) + + // Ask for everything we can get. The buffer size has to be + // more than 1, since the runtime might send SIGURG signals. + // Using 10 is arbitrary. + c1 := make(chan os.Signal, 10) + Notify(c1) + // Stop relaying the SIGURG signals. See #49724 + Reset(syscall.SIGURG) + defer Stop(c1) + + // Send this process a SIGWINCH + t.Logf("sigwinch...") + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + waitSigAll(t, c1, syscall.SIGWINCH) + + // Send two more SIGHUPs, to make sure that + // they get delivered on c1 and that not reading + // from c does not block everything. + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSigAll(t, c1, syscall.SIGHUP) + t.Logf("sighup...") + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSigAll(t, c1, syscall.SIGHUP) + + // The first SIGHUP should be waiting for us on c. + waitSig(t, c, syscall.SIGHUP) +} + +func TestStress(t *testing.T) { + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + + sig := make(chan os.Signal, 1) + Notify(sig, syscall.SIGUSR1) + + go func() { + stop := time.After(dur) + for { + select { + case <-stop: + // Allow enough time for all signals to be delivered before we stop + // listening for them. + quiesce() + Stop(sig) + // According to its documentation, “[w]hen Stop returns, it in + // guaranteed that c will receive no more signals.” So we can safely + // close sig here: if there is a send-after-close race here, that is a + // bug in Stop and we would like to detect it. + close(sig) + return + + default: + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + runtime.Gosched() + } + } + }() + + for range sig { + // Receive signals until the sender closes sig. + } +} + +func testCancel(t *testing.T, ignore bool) { + // Ask to be notified on c1 when a SIGWINCH is received. + c1 := make(chan os.Signal, 1) + Notify(c1, syscall.SIGWINCH) + defer Stop(c1) + + // Ask to be notified on c2 when a SIGHUP is received. + c2 := make(chan os.Signal, 1) + Notify(c2, syscall.SIGHUP) + defer Stop(c2) + + // Send this process a SIGWINCH and wait for notification on c1. + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + waitSig(t, c1, syscall.SIGWINCH) + + // Send this process a SIGHUP and wait for notification on c2. + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + waitSig(t, c2, syscall.SIGHUP) + + // Ignore, or reset the signal handlers for, SIGWINCH and SIGHUP. + // Either way, this should undo both calls to Notify above. + if ignore { + Ignore(syscall.SIGWINCH, syscall.SIGHUP) + // Don't bother deferring a call to Reset: it is documented to undo Notify, + // but its documentation says nothing about Ignore, and (as of the time of + // writing) it empirically does not undo an Ignore. + } else { + Reset(syscall.SIGWINCH, syscall.SIGHUP) + } + + // Send this process a SIGWINCH. It should be ignored. + syscall.Kill(syscall.Getpid(), syscall.SIGWINCH) + + // If ignoring, Send this process a SIGHUP. It should be ignored. + if ignore { + syscall.Kill(syscall.Getpid(), syscall.SIGHUP) + } + + quiesce() + + select { + case s := <-c1: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + select { + case s := <-c2: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + // One or both of the signals may have been blocked for this process + // by the calling process. + // Discard any queued signals now to avoid interfering with other tests. + Notify(c1, syscall.SIGWINCH) + Notify(c2, syscall.SIGHUP) + quiesce() +} + +// Test that Reset cancels registration for listed signals on all channels. +func TestReset(t *testing.T) { + testCancel(t, false) +} + +// Test that Ignore cancels registration for listed signals on all channels. +func TestIgnore(t *testing.T) { + testCancel(t, true) +} + +// Test that Ignored correctly detects changes to the ignored status of a signal. +func TestIgnored(t *testing.T) { + // Ask to be notified on SIGWINCH. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGWINCH) + + // If we're being notified, then the signal should not be ignored. + if Ignored(syscall.SIGWINCH) { + t.Errorf("expected SIGWINCH to not be ignored.") + } + Stop(c) + Ignore(syscall.SIGWINCH) + + // We're no longer paying attention to this signal. + if !Ignored(syscall.SIGWINCH) { + t.Errorf("expected SIGWINCH to be ignored when explicitly ignoring it.") + } + + Reset() +} + +var checkSighupIgnored = flag.Bool("check_sighup_ignored", false, "if true, TestDetectNohup will fail if SIGHUP is not ignored.") + +// Test that Ignored(SIGHUP) correctly detects whether it is being run under nohup. +func TestDetectNohup(t *testing.T) { + if *checkSighupIgnored { + if !Ignored(syscall.SIGHUP) { + t.Fatal("SIGHUP is not ignored.") + } else { + t.Log("SIGHUP is ignored.") + } + } else { + defer Reset() + // Ugly: ask for SIGHUP so that child will not have no-hup set + // even if test is running under nohup environment. + // We have no intention of reading from c. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + if out, err := exec.Command(os.Args[0], "-test.run=TestDetectNohup", "-check_sighup_ignored").CombinedOutput(); err == nil { + t.Errorf("ran test with -check_sighup_ignored and it succeeded: expected failure.\nOutput:\n%s", out) + } + Stop(c) + + // Again, this time with nohup, assuming we can find it. + _, err := os.Stat("/usr/bin/nohup") + if err != nil { + t.Skip("cannot find nohup; skipping second half of test") + } + Ignore(syscall.SIGHUP) + os.Remove("nohup.out") + out, err := exec.Command("/usr/bin/nohup", os.Args[0], "-test.run=TestDetectNohup", "-check_sighup_ignored").CombinedOutput() + + data, _ := os.ReadFile("nohup.out") + os.Remove("nohup.out") + if err != nil { + // nohup doesn't work on new LUCI darwin builders due to the + // type of launchd service the test run under. See + // https://go.dev/issue/63875. + if runtime.GOOS == "darwin" && strings.Contains(string(out), "nohup: can't detach from console: Inappropriate ioctl for device") { + t.Skip("Skipping nohup test due to darwin builder limitation. See https://go.dev/issue/63875.") + } + + t.Errorf("ran test with -check_sighup_ignored under nohup and it failed: expected success.\nError: %v\nOutput:\n%s%s", err, out, data) + } + } +} + +var ( + sendUncaughtSighup = flag.Int("send_uncaught_sighup", 0, "send uncaught SIGHUP during TestStop") + dieFromSighup = flag.Bool("die_from_sighup", false, "wait to die from uncaught SIGHUP") +) + +// Test that Stop cancels the channel's registrations. +func TestStop(t *testing.T) { + sigs := []syscall.Signal{ + syscall.SIGWINCH, + syscall.SIGHUP, + syscall.SIGUSR1, + } + + for _, sig := range sigs { + sig := sig + t.Run(fmt.Sprint(sig), func(t *testing.T) { + // When calling Notify with a specific signal, + // independent signals should not interfere with each other, + // and we end up needing to wait for signals to quiesce a lot. + // Test the three different signals concurrently. + t.Parallel() + + // If the signal is not ignored, send the signal before registering a + // channel to verify the behavior of the default Go handler. + // If it's SIGWINCH or SIGUSR1 we should not see it. + // If it's SIGHUP, maybe we'll die. Let the flag tell us what to do. + mayHaveBlockedSignal := false + if !Ignored(sig) && (sig != syscall.SIGHUP || *sendUncaughtSighup == 1) { + syscall.Kill(syscall.Getpid(), sig) + quiesce() + + // We don't know whether sig is blocked for this process; see + // https://golang.org/issue/38165. Assume that it could be. + mayHaveBlockedSignal = true + } + + // Ask for signal + c := make(chan os.Signal, 1) + Notify(c, sig) + + // Send this process the signal again. + syscall.Kill(syscall.Getpid(), sig) + waitSig(t, c, sig) + + if mayHaveBlockedSignal { + // We may have received a queued initial signal in addition to the one + // that we sent after Notify. If so, waitSig may have observed that + // initial signal instead of the second one, and we may need to wait for + // the second signal to clear. Do that now. + quiesce() + select { + case <-c: + default: + } + } + + // Stop watching for the signal and send it again. + // If it's SIGHUP, maybe we'll die. Let the flag tell us what to do. + Stop(c) + if sig != syscall.SIGHUP || *sendUncaughtSighup == 2 { + syscall.Kill(syscall.Getpid(), sig) + quiesce() + + select { + case s := <-c: + t.Errorf("unexpected signal %v", s) + default: + // nothing to read - good + } + + // If we're going to receive a signal, it has almost certainly been + // received by now. However, it may have been blocked for this process — + // we don't know. Explicitly unblock it and wait for it to clear now. + Notify(c, sig) + quiesce() + Stop(c) + } + }) + } +} + +// Test that when run under nohup, an uncaught SIGHUP does not kill the program. +func TestNohup(t *testing.T) { + // When run without nohup, the test should crash on an uncaught SIGHUP. + // When run under nohup, the test should ignore uncaught SIGHUPs, + // because the runtime is not supposed to be listening for them. + // Either way, TestStop should still be able to catch them when it wants them + // and then when it stops wanting them, the original behavior should resume. + // + // send_uncaught_sighup=1 sends the SIGHUP before starting to listen for SIGHUPs. + // send_uncaught_sighup=2 sends the SIGHUP after no longer listening for SIGHUPs. + // + // Both should fail without nohup and succeed with nohup. + + t.Run("uncaught", func(t *testing.T) { + // Ugly: ask for SIGHUP so that child will not have no-hup set + // even if test is running under nohup environment. + // We have no intention of reading from c. + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + t.Cleanup(func() { Stop(c) }) + + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + subTimeout = time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for propagating output. + } + for i := 1; i <= 2; i++ { + i := i + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + args := []string{ + "-test.v", + "-test.run=TestStop", + "-send_uncaught_sighup=" + strconv.Itoa(i), + "-die_from_sighup", + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := testenv.Command(t, os.Args[0], args...).CombinedOutput() + + if err == nil { + t.Errorf("ran test with -send_uncaught_sighup=%d and it succeeded: expected failure.\nOutput:\n%s", i, out) + } else { + t.Logf("test with -send_uncaught_sighup=%d failed as expected.\nError: %v\nOutput:\n%s", i, err, out) + } + }) + } + }) + + t.Run("nohup", func(t *testing.T) { + // Skip the nohup test below when running in tmux on darwin, since nohup + // doesn't work correctly there. See issue #5135. + if runtime.GOOS == "darwin" && os.Getenv("TMUX") != "" { + t.Skip("Skipping nohup test due to running in tmux on darwin") + } + + // Again, this time with nohup, assuming we can find it. + _, err := exec.LookPath("nohup") + if err != nil { + t.Skip("cannot find nohup; skipping second half of test") + } + + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + subTimeout = time.Until(deadline) + subTimeout -= subTimeout / 10 // Leave 10% headroom for propagating output. + } + for i := 1; i <= 2; i++ { + i := i + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + // POSIX specifies that nohup writes to a file named nohup.out if standard + // output is a terminal. However, for an exec.Cmd, standard output is + // not a terminal — so we don't need to read or remove that file (and, + // indeed, cannot even create it if the current user is unable to write to + // GOROOT/src, such as when GOROOT is installed and owned by root). + + args := []string{ + os.Args[0], + "-test.v", + "-test.run=TestStop", + "-send_uncaught_sighup=" + strconv.Itoa(i), + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := testenv.Command(t, "nohup", args...).CombinedOutput() + + if err != nil { + // nohup doesn't work on new LUCI darwin builders due to the + // type of launchd service the test run under. See + // https://go.dev/issue/63875. + if runtime.GOOS == "darwin" && strings.Contains(string(out), "nohup: can't detach from console: Inappropriate ioctl for device") { + // TODO(go.dev/issue/63799): A false-positive in vet reports a + // t.Skip here as invalid. Switch back to t.Skip once fixed. + t.Logf("Skipping nohup test due to darwin builder limitation. See https://go.dev/issue/63875.") + return + } + + t.Errorf("ran test with -send_uncaught_sighup=%d under nohup and it failed: expected success.\nError: %v\nOutput:\n%s", i, err, out) + } else { + t.Logf("ran test with -send_uncaught_sighup=%d under nohup.\nOutput:\n%s", i, out) + } + }) + } + }) +} + +// Test that SIGCONT works (issue 8953). +func TestSIGCONT(t *testing.T) { + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGCONT) + defer Stop(c) + syscall.Kill(syscall.Getpid(), syscall.SIGCONT) + waitSig(t, c, syscall.SIGCONT) +} + +// Test race between stopping and receiving a signal (issue 14571). +func TestAtomicStop(t *testing.T) { + if os.Getenv("GO_TEST_ATOMIC_STOP") != "" { + atomicStopTestProgram(t) + t.Fatal("atomicStopTestProgram returned") + } + + testenv.MustHaveExec(t) + + // Call Notify for SIGINT before starting the child process. + // That ensures that SIGINT is not ignored for the child. + // This is necessary because if SIGINT is ignored when a + // Go program starts, then it remains ignored, and closing + // the last notification channel for SIGINT will switch it + // back to being ignored. In that case the assumption of + // atomicStopTestProgram, that it will either die from SIGINT + // or have it be reported, breaks down, as there is a third + // option: SIGINT might be ignored. + cs := make(chan os.Signal, 1) + Notify(cs, syscall.SIGINT) + defer Stop(cs) + + const execs = 10 + for i := 0; i < execs; i++ { + timeout := "0" + if deadline, ok := t.Deadline(); ok { + timeout = time.Until(deadline).String() + } + cmd := exec.Command(os.Args[0], "-test.run=TestAtomicStop", "-test.timeout="+timeout) + cmd.Env = append(os.Environ(), "GO_TEST_ATOMIC_STOP=1") + out, err := cmd.CombinedOutput() + if err == nil { + if len(out) > 0 { + t.Logf("iteration %d: output %s", i, out) + } + } else { + t.Logf("iteration %d: exit status %q: output: %s", i, err, out) + } + + lost := bytes.Contains(out, []byte("lost signal")) + if lost { + t.Errorf("iteration %d: lost signal", i) + } + + // The program should either die due to SIGINT, + // or exit with success without printing "lost signal". + if err == nil { + if len(out) > 0 && !lost { + t.Errorf("iteration %d: unexpected output", i) + } + } else { + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("iteration %d: error (%v) has type %T; expected exec.ExitError", i, err, err) + } else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok { + t.Errorf("iteration %d: error.Sys (%v) has type %T; expected syscall.WaitStatus", i, ee.Sys(), ee.Sys()) + } else if !ws.Signaled() || ws.Signal() != syscall.SIGINT { + t.Errorf("iteration %d: got exit status %v; expected SIGINT", i, ee) + } + } + } +} + +// atomicStopTestProgram is run in a subprocess by TestAtomicStop. +// It tries to trigger a signal delivery race. This function should +// either catch a signal or die from it. +func atomicStopTestProgram(t *testing.T) { + // This test won't work if SIGINT is ignored here. + if Ignored(syscall.SIGINT) { + fmt.Println("SIGINT is ignored") + os.Exit(1) + } + + const tries = 10 + + timeout := 2 * time.Second + if deadline, ok := t.Deadline(); ok { + // Give each try an equal slice of the deadline, with one slice to spare for + // cleanup. + timeout = time.Until(deadline) / (tries + 1) + } + + pid := syscall.Getpid() + printed := false + for i := 0; i < tries; i++ { + cs := make(chan os.Signal, 1) + Notify(cs, syscall.SIGINT) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + Stop(cs) + }() + + syscall.Kill(pid, syscall.SIGINT) + + // At this point we should either die from SIGINT or + // get a notification on cs. If neither happens, we + // dropped the signal. It is given 2 seconds to + // deliver, as needed for gccgo on some loaded test systems. + + select { + case <-cs: + case <-time.After(timeout): + if !printed { + fmt.Print("lost signal on tries:") + printed = true + } + fmt.Printf(" %d", i) + } + + wg.Wait() + } + if printed { + fmt.Print("\n") + } + + os.Exit(0) +} + +func TestTime(t *testing.T) { + // Test that signal works fine when we are in a call to get time, + // which on some platforms is using VDSO. See issue #34391. + dur := 3 * time.Second + if testing.Short() { + dur = 100 * time.Millisecond + } + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4)) + + sig := make(chan os.Signal, 1) + Notify(sig, syscall.SIGUSR1) + + stop := make(chan struct{}) + go func() { + for { + select { + case <-stop: + // Allow enough time for all signals to be delivered before we stop + // listening for them. + quiesce() + Stop(sig) + // According to its documentation, “[w]hen Stop returns, it in + // guaranteed that c will receive no more signals.” So we can safely + // close sig here: if there is a send-after-close race, that is a bug in + // Stop and we would like to detect it. + close(sig) + return + + default: + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + runtime.Gosched() + } + } + }() + + done := make(chan struct{}) + go func() { + for range sig { + // Receive signals until the sender closes sig. + } + close(done) + }() + + t0 := time.Now() + for t1 := t0; t1.Sub(t0) < dur; t1 = time.Now() { + } // hammering on getting time + + close(stop) + <-done +} + +var ( + checkNotifyContext = flag.Bool("check_notify_ctx", false, "if true, TestNotifyContext will fail if SIGINT is not received.") + ctxNotifyTimes = flag.Int("ctx_notify_times", 1, "number of times a SIGINT signal should be received") +) + +func TestNotifyContextNotifications(t *testing.T) { + if *checkNotifyContext { + ctx, _ := NotifyContext(context.Background(), syscall.SIGINT) + // We want to make sure not to be calling Stop() internally on NotifyContext() when processing a received signal. + // Being able to wait for a number of received system signals allows us to do so. + var wg sync.WaitGroup + n := *ctxNotifyTimes + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + wg.Done() + }() + } + wg.Wait() + <-ctx.Done() + fmt.Println("received SIGINT") + // Sleep to give time to simultaneous signals to reach the process. + // These signals must be ignored given stop() is not called on this code. + // We want to guarantee a SIGINT doesn't cause a premature termination of the program. + time.Sleep(settleTime) + return + } + + t.Parallel() + testCases := []struct { + name string + n int // number of times a SIGINT should be notified. + }{ + {"once", 1}, + {"multiple", 10}, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var subTimeout time.Duration + if deadline, ok := t.Deadline(); ok { + timeout := time.Until(deadline) + if timeout < 2*settleTime { + t.Fatalf("starting test with less than %v remaining", 2*settleTime) + } + subTimeout = timeout - (timeout / 10) // Leave 10% headroom for cleaning up subprocess. + } + + args := []string{ + "-test.v", + "-test.run=TestNotifyContextNotifications$", + "-check_notify_ctx", + fmt.Sprintf("-ctx_notify_times=%d", tc.n), + } + if subTimeout != 0 { + args = append(args, fmt.Sprintf("-test.timeout=%v", subTimeout)) + } + out, err := exec.Command(os.Args[0], args...).CombinedOutput() + if err != nil { + t.Errorf("ran test with -check_notify_ctx_notification and it failed with %v.\nOutput:\n%s", err, out) + } + if want := []byte("received SIGINT\n"); !bytes.Contains(out, want) { + t.Errorf("got %q, wanted %q", out, want) + } + }) + } +} + +func TestNotifyContextStop(t *testing.T) { + Ignore(syscall.SIGHUP) + if !Ignored(syscall.SIGHUP) { + t.Errorf("expected SIGHUP to be ignored when explicitly ignoring it.") + } + + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGHUP) + defer stop() + + // If we're being notified, then the signal should not be ignored. + if Ignored(syscall.SIGHUP) { + t.Errorf("expected SIGHUP to not be ignored.") + } + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [hangup])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, wanted %q", got, want) + } + + stop() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for context to be done after calling stop") + } +} + +func TestNotifyContextCancelParent(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + cancelParent() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for parent context to be canceled") + } +} + +func TestNotifyContextPrematureCancelParent(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + + cancelParent() // Prematurely cancel context before calling NotifyContext. + c, stop := NotifyContext(parent, syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background.WithCancel, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("timed out waiting for parent context to be canceled") + } +} + +func TestNotifyContextSimultaneousStop(t *testing.T) { + c, stop := NotifyContext(context.Background(), syscall.SIGINT) + defer stop() + + if want, got := "signal.NotifyContext(context.Background, [interrupt])", fmt.Sprint(c); want != got { + t.Errorf("c.String() = %q, want %q", got, want) + } + + var wg sync.WaitGroup + n := 10 + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + stop() + wg.Done() + }() + } + wg.Wait() + select { + case <-c.Done(): + if got := c.Err(); got != context.Canceled { + t.Errorf("c.Err() = %q, want %q", got, context.Canceled) + } + case <-time.After(time.Second): + t.Errorf("expected context to be canceled") + } +} + +func TestNotifyContextStringer(t *testing.T) { + parent, cancelParent := context.WithCancel(context.Background()) + defer cancelParent() + c, stop := NotifyContext(parent, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) + defer stop() + + want := `signal.NotifyContext(context.Background.WithCancel, [hangup interrupt terminated])` + if got := fmt.Sprint(c); got != want { + t.Errorf("c.String() = %q, want %q", got, want) + } +} + +// #44193 test signal handling while stopping and starting the world. +func TestSignalTrace(t *testing.T) { + done := make(chan struct{}) + quit := make(chan struct{}) + c := make(chan os.Signal, 1) + Notify(c, syscall.SIGHUP) + + // Source and sink for signals busy loop unsynchronized with + // trace starts and stops. We are ultimately validating that + // signals and runtime.(stop|start)TheWorldGC are compatible. + go func() { + defer close(done) + defer Stop(c) + pid := syscall.Getpid() + for { + select { + case <-quit: + return + default: + syscall.Kill(pid, syscall.SIGHUP) + } + waitSig(t, c, syscall.SIGHUP) + } + }() + + for i := 0; i < 100; i++ { + buf := new(bytes.Buffer) + if err := trace.Start(buf); err != nil { + t.Fatalf("[%d] failed to start tracing: %v", i, err) + } + time.After(1 * time.Microsecond) + trace.Stop() + size := buf.Len() + if size == 0 { + t.Fatalf("[%d] trace is empty", i) + } + } + close(quit) + <-done +} diff --git a/src/os/signal/signal_unix.go b/src/os/signal/signal_unix.go new file mode 100644 index 0000000..772175a --- /dev/null +++ b/src/os/signal/signal_unix.go @@ -0,0 +1,62 @@ +// Copyright 2012 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 || (js && wasm) || windows + +package signal + +import ( + "os" + "syscall" +) + +// Defined by the runtime package. +func signal_disable(uint32) +func signal_enable(uint32) +func signal_ignore(uint32) +func signal_ignored(uint32) bool +func signal_recv() uint32 + +func loop() { + for { + process(syscall.Signal(signal_recv())) + } +} + +func init() { + watchSignalLoop = loop +} + +const ( + numSig = 65 // max across all systems +) + +func signum(sig os.Signal) int { + switch sig := sig.(type) { + case syscall.Signal: + i := int(sig) + if i < 0 || i >= numSig { + return -1 + } + return i + default: + return -1 + } +} + +func enableSignal(sig int) { + signal_enable(uint32(sig)) +} + +func disableSignal(sig int) { + signal_disable(uint32(sig)) +} + +func ignoreSignal(sig int) { + signal_ignore(uint32(sig)) +} + +func signalIgnored(sig int) bool { + return signal_ignored(uint32(sig)) +} diff --git a/src/os/signal/signal_windows_test.go b/src/os/signal/signal_windows_test.go new file mode 100644 index 0000000..02803e5 --- /dev/null +++ b/src/os/signal/signal_windows_test.go @@ -0,0 +1,99 @@ +// Copyright 2012 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. + +package signal + +import ( + "internal/testenv" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" +) + +func sendCtrlBreak(t *testing.T, pid int) { + d, e := syscall.LoadDLL("kernel32.dll") + if e != nil { + t.Fatalf("LoadDLL: %v\n", e) + } + p, e := d.FindProc("GenerateConsoleCtrlEvent") + if e != nil { + t.Fatalf("FindProc: %v\n", e) + } + r, _, e := p.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid)) + if r == 0 { + t.Fatalf("GenerateConsoleCtrlEvent: %v\n", e) + } +} + +func TestCtrlBreak(t *testing.T) { + // create source file + const source = ` +package main + +import ( + "log" + "os" + "os/signal" + "time" +) + + +func main() { + c := make(chan os.Signal, 10) + signal.Notify(c) + select { + case s := <-c: + if s != os.Interrupt { + log.Fatalf("Wrong signal received: got %q, want %q\n", s, os.Interrupt) + } + case <-time.After(3 * time.Second): + log.Fatalf("Timeout waiting for Ctrl+Break\n") + } +} +` + tmp := t.TempDir() + + // write ctrlbreak.go + name := filepath.Join(tmp, "ctlbreak") + src := name + ".go" + f, err := os.Create(src) + if err != nil { + t.Fatalf("Failed to create %v: %v", src, err) + } + defer f.Close() + f.Write([]byte(source)) + + // compile it + exe := name + ".exe" + defer os.Remove(exe) + o, err := exec.Command(testenv.GoToolPath(t), "build", "-o", exe, src).CombinedOutput() + if err != nil { + t.Fatalf("Failed to compile: %v\n%v", err, string(o)) + } + + // run it + cmd := exec.Command(exe) + var buf strings.Builder + cmd.Stdout = &buf + cmd.Stderr = &buf + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } + err = cmd.Start() + if err != nil { + t.Fatalf("Start failed: %v", err) + } + go func() { + time.Sleep(1 * time.Second) + sendCtrlBreak(t, cmd.Process.Pid) + }() + err = cmd.Wait() + if err != nil { + t.Fatalf("Program exited with error: %v\n%v", err, buf.String()) + } +} -- cgit v1.2.3