blob: e92abae6753f9b9fc2e9b8642f093678760e9295 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 signals
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"syscall"
"testing"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/rpc"
"v.io/v23/services/appcycle"
"v.io/v23/vtrace"
"v.io/x/ref/lib/mgmt"
"v.io/x/ref/lib/security/securityflag"
"v.io/x/ref/services/device"
"v.io/x/ref/test"
"v.io/x/ref/test/modules"
_ "v.io/x/ref/runtime/factories/generic"
)
//go:generate jiri test generate
func stopLoop(stop func(), stdin io.Reader, ch chan<- struct{}) {
scanner := bufio.NewScanner(stdin)
for scanner.Scan() {
switch scanner.Text() {
case "close":
close(ch)
return
case "stop":
stop()
}
}
}
func program(stdin io.Reader, stdout io.Writer, signals ...os.Signal) {
ctx, shutdown := test.V23Init()
closeStopLoop := make(chan struct{})
// obtain ac here since stopLoop may execute after shutdown is called below
ac := v23.GetAppCycle(ctx)
go stopLoop(func() { ac.Stop(ctx) }, stdin, closeStopLoop)
wait := ShutdownOnSignals(ctx, signals...)
fmt.Fprintf(stdout, "ready\n")
fmt.Fprintf(stdout, "received signal %s\n", <-wait)
shutdown()
<-closeStopLoop
}
var handleDefaults = modules.Register(func(env *modules.Env, args ...string) error {
program(env.Stdin, env.Stdout)
return nil
}, "handleDefaults")
var handleCustom = modules.Register(func(env *modules.Env, args ...string) error {
program(env.Stdin, env.Stdout, syscall.SIGABRT)
return nil
}, "handleCustom")
var handleCustomWithStop = modules.Register(func(env *modules.Env, args ...string) error {
program(env.Stdin, env.Stdout, STOP, syscall.SIGABRT, syscall.SIGHUP)
return nil
}, "handleCustomWithStop")
var handleDefaultsIgnoreChan = modules.Register(func(env *modules.Env, args ...string) error {
ctx, shutdown := test.V23Init()
defer shutdown()
closeStopLoop := make(chan struct{})
// obtain ac here since stopLoop may execute after shutdown is called below
ac := v23.GetAppCycle(ctx)
go stopLoop(func() { ac.Stop(ctx) }, env.Stdin, closeStopLoop)
ShutdownOnSignals(ctx)
fmt.Fprintf(env.Stdout, "ready\n")
<-closeStopLoop
return nil
}, "handleDefaultsIgnoreChan")
func isSignalInSet(sig os.Signal, set []os.Signal) bool {
for _, s := range set {
if sig == s {
return true
}
}
return false
}
func checkSignalIsDefault(t *testing.T, sig os.Signal) {
if !isSignalInSet(sig, defaultSignals()) {
t.Errorf("signal %s not in default signal set, as expected", sig)
}
}
func checkSignalIsNotDefault(t *testing.T, sig os.Signal) {
if isSignalInSet(sig, defaultSignals()) {
t.Errorf("signal %s unexpectedly in default signal set", sig)
}
}
func newShell(t *testing.T, ctx *context.T, prog modules.Program) (*modules.Shell, modules.Handle) {
sh, err := modules.NewShell(ctx, nil, testing.Verbose(), t)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
handle, err := sh.Start(nil, prog)
if err != nil {
sh.Cleanup(os.Stderr, os.Stderr)
t.Fatalf("unexpected error: %s", err)
return nil, nil
}
return sh, handle
}
// TestCleanShutdownSignal verifies that sending a signal to a child that
// handles it by default causes the child to shut down cleanly.
func TestCleanShutdownSignal(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaults)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
checkSignalIsDefault(t, syscall.SIGINT)
syscall.Kill(h.Pid(), syscall.SIGINT)
h.Expectf("received signal %s", syscall.SIGINT)
fmt.Fprintf(h.Stdin(), "close\n")
h.ExpectEOF()
}
// TestCleanShutdownStop verifies that sending a stop comamnd to a child that
// handles stop commands by default causes the child to shut down cleanly.
func TestCleanShutdownStop(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaults)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
fmt.Fprintf(h.Stdin(), "stop\n")
h.Expectf("received signal %s", v23.LocalStop)
fmt.Fprintf(h.Stdin(), "close\n")
h.ExpectEOF()
}
// TestCleanShutdownStopCustom verifies that sending a stop comamnd to a child
// that handles stop command as part of a custom set of signals handled, causes
// the child to shut down cleanly.
func TestCleanShutdownStopCustom(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleCustomWithStop)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
fmt.Fprintf(h.Stdin(), "stop\n")
h.Expectf("received signal %s", v23.LocalStop)
fmt.Fprintf(h.Stdin(), "close\n")
h.ExpectEOF()
}
func testExitStatus(t *testing.T, h modules.Handle, code int) {
h.ExpectEOF()
_, file, line, _ := runtime.Caller(1)
file = filepath.Base(file)
if got, want := h.Shutdown(os.Stdout, os.Stderr), fmt.Errorf("exit status %d", code); got.Error() != want.Error() {
t.Errorf("%s:%d: got %q, want %q", file, line, got, want)
}
}
// TestStopNoHandler verifies that sending a stop command to a child that does
// not handle stop commands causes the child to exit immediately.
func TestStopNoHandler(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleCustom)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
fmt.Fprintf(h.Stdin(), "stop\n")
testExitStatus(t, h, v23.UnhandledStopExitCode)
}
// TestDoubleSignal verifies that sending a succession of two signals to a child
// that handles these signals by default causes the child to exit immediately
// upon receiving the second signal.
func TestDoubleSignal(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaults)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
checkSignalIsDefault(t, syscall.SIGTERM)
syscall.Kill(h.Pid(), syscall.SIGTERM)
h.Expectf("received signal %s", syscall.SIGTERM)
checkSignalIsDefault(t, syscall.SIGINT)
syscall.Kill(h.Pid(), syscall.SIGINT)
testExitStatus(t, h, DoubleStopExitCode)
}
// TestSignalAndStop verifies that sending a signal followed by a stop command
// to a child that handles these by default causes the child to exit immediately
// upon receiving the stop command.
func TestSignalAndStop(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaults)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
checkSignalIsDefault(t, syscall.SIGTERM)
syscall.Kill(h.Pid(), syscall.SIGTERM)
h.Expectf("received signal %s", syscall.SIGTERM)
fmt.Fprintf(h.Stdin(), "stop\n")
testExitStatus(t, h, DoubleStopExitCode)
}
// TestDoubleStop verifies that sending a succession of stop commands to a child
// that handles stop commands by default causes the child to exit immediately
// upon receiving the second stop command.
func TestDoubleStop(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaults)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
fmt.Fprintf(h.Stdin(), "stop\n")
h.Expectf("received signal %s", v23.LocalStop)
fmt.Fprintf(h.Stdin(), "stop\n")
testExitStatus(t, h, DoubleStopExitCode)
}
// TestSendUnhandledSignal verifies that sending a signal that the child does
// not handle causes the child to exit as per the signal being sent.
func TestSendUnhandledSignal(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaults)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
checkSignalIsNotDefault(t, syscall.SIGABRT)
syscall.Kill(h.Pid(), syscall.SIGABRT)
testExitStatus(t, h, 2)
}
// TestDoubleSignalIgnoreChan verifies that, even if we ignore the channel that
// ShutdownOnSignals returns, sending two signals should still cause the
// process to exit (ensures that there is no dependency in ShutdownOnSignals
// on having a goroutine read from the returned channel).
func TestDoubleSignalIgnoreChan(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleDefaultsIgnoreChan)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
// Even if we ignore the channel that ShutdownOnSignals returns,
// sending two signals should still cause the process to exit.
checkSignalIsDefault(t, syscall.SIGTERM)
syscall.Kill(h.Pid(), syscall.SIGTERM)
checkSignalIsDefault(t, syscall.SIGINT)
syscall.Kill(h.Pid(), syscall.SIGINT)
testExitStatus(t, h, DoubleStopExitCode)
}
// TestHandlerCustomSignal verifies that sending a non-default signal to a
// server that listens for that signal causes the server to shut down cleanly.
func TestHandlerCustomSignal(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, h := newShell(t, ctx, handleCustom)
defer sh.Cleanup(os.Stderr, os.Stderr)
h.Expect("ready")
checkSignalIsNotDefault(t, syscall.SIGABRT)
syscall.Kill(h.Pid(), syscall.SIGABRT)
h.Expectf("received signal %s", syscall.SIGABRT)
fmt.Fprintf(h.Stdin(), "stop\n")
h.ExpectEOF()
}
// TestHandlerCustomSignalWithStop verifies that sending a custom stop signal
// to a server that listens for that signal causes the server to shut down
// cleanly, even when a STOP signal is also among the handled signals.
func TestHandlerCustomSignalWithStop(t *testing.T) {
rootCtx, shutdown := test.V23Init()
defer shutdown()
for _, signal := range []syscall.Signal{syscall.SIGABRT, syscall.SIGHUP} {
ctx, _ := vtrace.WithNewTrace(rootCtx)
sh, h := newShell(t, ctx, handleCustomWithStop)
h.Expect("ready")
checkSignalIsNotDefault(t, signal)
syscall.Kill(h.Pid(), signal)
h.Expectf("received signal %s", signal)
fmt.Fprintf(h.Stdin(), "close\n")
h.ExpectEOF()
sh.Cleanup(os.Stderr, os.Stderr)
}
}
// TestParseSignalsList verifies that ShutdownOnSignals correctly interprets
// the input list of signals.
func TestParseSignalsList(t *testing.T) {
list := []os.Signal{STOP, syscall.SIGTERM}
ShutdownOnSignals(nil, list...)
if !isSignalInSet(syscall.SIGTERM, list) {
t.Errorf("signal %s not in signal set, as expected: %v", syscall.SIGTERM, list)
}
if !isSignalInSet(STOP, list) {
t.Errorf("signal %s not in signal set, as expected: %v", STOP, list)
}
}
type configServer struct {
ch chan<- string
}
func (c *configServer) Set(_ *context.T, _ rpc.ServerCall, key, value string) error {
if key != mgmt.AppCycleManagerConfigKey {
return fmt.Errorf("Unexpected key: %v", key)
}
c.ch <- value
return nil
}
// TestCleanRemoteShutdown verifies that remote shutdown works correctly.
func TestCleanRemoteShutdown(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
sh, err := modules.NewShell(ctx, nil, testing.Verbose(), t)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer sh.Cleanup(os.Stderr, os.Stderr)
ch := make(chan string)
_, server, err := v23.WithNewServer(ctx, "", device.ConfigServer(&configServer{ch}), securityflag.NewAuthorizerOrDie())
if err != nil {
t.Fatalf("Got error: %v", err)
}
configServiceName := server.Status().Endpoints[0].Name()
sh.SetConfigKey(mgmt.ParentNameConfigKey, configServiceName)
sh.SetConfigKey(mgmt.ProtocolConfigKey, "tcp")
sh.SetConfigKey(mgmt.AddressConfigKey, "127.0.0.1:0")
h, err := sh.Start(nil, handleDefaults)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
appCycleName := <-ch
h.Expect("ready")
appCycle := appcycle.AppCycleClient(appCycleName)
stream, err := appCycle.Stop(ctx)
if err != nil {
t.Fatalf("Got error: %v", err)
}
rStream := stream.RecvStream()
if rStream.Advance() || rStream.Err() != nil {
t.Errorf("Expected EOF, got (%v, %v) instead: ", rStream.Value(), rStream.Err())
}
if err := stream.Finish(); err != nil {
t.Fatalf("Got error: %v", err)
}
h.Expectf("received signal %s", v23.RemoteStop)
fmt.Fprintf(h.Stdin(), "close\n")
h.ExpectEOF()
}