| // 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() |
| } |