Repository: tetratelabs/getenvoy
Pull request #269 (2021-06-07 03:06:02)

Simplifies hooks into shutdown-only
Before, we had several types of hooks, but most were only needed to
support tee'ing stdout and stderr into log files. This centralizes that
logic and renames things around "shutdown hooks" which are easier to
understand and congruent with envoy console output.

Ex.
```
[2021-06-07 10:07:43.912][3272041][info][main] [source/server/server.cc:790] shutting down server instance
```


Patch:
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index 79648c3..d8376a9 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -16,6 +16,7 @@ package cmd
 
 import (
 	"fmt"
+	"io"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -24,7 +25,7 @@ import (
 	"github.com/urfave/cli/v2"
 
 	"github.com/tetratelabs/getenvoy/internal/envoy"
-	"github.com/tetratelabs/getenvoy/internal/envoy/debug"
+	"github.com/tetratelabs/getenvoy/internal/envoy/shutdown"
 	"github.com/tetratelabs/getenvoy/internal/globals"
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
@@ -34,7 +35,7 @@ func NewRunCmd(o *globals.GlobalOpts) *cli.Command {
 	var envoyVersion string
 	cmd := &cli.Command{
 		Name:            "run",
-		Usage:           "Run Envoy with the given [arguments...], collecting process state on termination",
+		Usage:           "Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C",
 		ArgsUsage:       "[arguments...]",
 		SkipFlagParsing: true,
 		Description: `To run Envoy, execute ` + "`getenvoy run -c your_envoy_config.yaml`" + `. This
@@ -44,9 +45,10 @@ Envoy runs in the current directory and interprets the '[arguments...]'.
 The first version in the below is run, controllable by the "use" command:
 ` + fmt.Sprintf("```\n%s\n```", envoy.VersionUsageList()) + `
 
-getenvoy generates files, e.g. logs, into ` + "`$GETENVOY_HOME/runs/$epochtime`" + `.
-These archive into ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`" + ` upon termination.
-`,
+While Envoy is running, the run directory (` + "`$GETENVOY_HOME/runs/$epochtime`" + `)
+includes minimally "stdout.log" and "stderr.log". On Ctrl-C, shutdown hooks
+write troubleshooting files, including admin endpoints, network and process
+state. Upon exit, this archives as ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`",
 		Before: func(context *cli.Context) error {
 			if err := os.MkdirAll(o.HomeDir, 0750); err != nil {
 				return NewValidationError(err.Error())
@@ -71,11 +73,24 @@ These archive into ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`" + ` upon termin
 			}
 			r := envoy.NewRuntime(&o.RunOpts)
 
-			r.Out = c.App.Writer
-			r.Err = c.App.ErrWriter
+			stdoutLog, err := os.OpenFile(filepath.Join(r.GetRunDir(), "stdout.log"), os.O_CREATE|os.O_WRONLY, 0600)
+			if err != nil {
+				return fmt.Errorf("couldn't create stdout log file: %w", err)
+			}
+			defer stdoutLog.Close() //nolint
+			r.Out = io.MultiWriter(c.App.Writer, stdoutLog)
+
+			stderrLog, err := os.OpenFile(filepath.Join(r.GetRunDir(), "stderr.log"), os.O_CREATE|os.O_WRONLY, 0600)
+			if err != nil {
+				return fmt.Errorf("couldn't create stderr log file: %w", err)
+			}
+			defer stderrLog.Close() //nolint
+			r.Err = io.MultiWriter(c.App.ErrWriter, stderrLog)
 
-			for _, err := range debug.EnableAll(r) {
-				fmt.Fprintln(r.Out, "failed to enable debug option:", err) //nolint
+			for _, enableShutdownHook := range shutdown.EnableHooks {
+				if err := enableShutdownHook(r); err != nil {
+					fmt.Fprintln(r.Out, "failed to enable shutdown hook:", err) //nolint
+				}
 			}
 
 			return r.Run(c.Context, c.Args().Slice())
@@ -100,7 +115,7 @@ func initializeRunOpts(o *globals.GlobalOpts, p, v string) error {
 		runID := strconv.FormatInt(time.Now().UnixNano(), 10)
 		runDir := filepath.Join(filepath.Join(o.HomeDir, "runs"), runID)
 
-		// When the directory is implicitly generated, we should create it to avoid late errors.
+		// Eagerly create the run dir, so that errors raise early
 		if err := os.MkdirAll(runDir, 0750); err != nil {
 			return NewValidationError("unable to create working directory %q, so we cannot run envoy", runDir)
 		}
diff --git a/internal/envoy/debug/log.go b/internal/envoy/debug/log.go
deleted file mode 100644
index 7521816..0000000
--- a/internal/envoy/debug/log.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2019 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package debug
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-
-	"github.com/tetratelabs/getenvoy/internal/envoy"
-)
-
-// enableEnvoyLogCollection is a preset option that registers collection of Envoy access logs and stderr
-func enableEnvoyLogCollection(r *envoy.Runtime) error {
-	logsDir := filepath.Join(r.GetRunDir(), "logs")
-	if err := os.MkdirAll(logsDir, 0750); err != nil {
-		return fmt.Errorf("unable to create directory %q, so no logs will be captured: %w", logsDir, err)
-	}
-	e := envoyLogCollection{r, logsDir}
-	r.RegisterPreStart(e.captureStdout)
-	r.RegisterPreStart(e.captureStderr)
-	return nil
-}
-
-type envoyLogCollection struct {
-	r       *envoy.Runtime
-	logsDir string
-}
-
-func (e *envoyLogCollection) captureStdout() error {
-	f, err := createLogFile(filepath.Join(e.logsDir, "access.log"))
-	if err != nil {
-		return err
-	}
-	e.r.RegisterPostTermination(f.Close)
-	e.r.SetStdout(func(w io.Writer) io.Writer {
-		if w == nil {
-			return f
-		}
-		return io.MultiWriter(w, f)
-	})
-	return nil
-}
-
-func (e *envoyLogCollection) captureStderr() error {
-	f, err := createLogFile(filepath.Join(e.logsDir, "error.log"))
-	if err != nil {
-		return err
-	}
-	e.r.RegisterPostTermination(f.Close)
-	e.r.SetStderr(func(w io.Writer) io.Writer {
-		if w == nil {
-			return f
-		}
-		return io.MultiWriter(w, f)
-	})
-	return nil
-}
-
-func createLogFile(path string) (*os.File, error) {
-	// #nosec -> logs can be written anywhere
-	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
-	if err != nil {
-		return nil, fmt.Errorf("unable to open file to write logs to %v: %w", path, err)
-	}
-	return f, nil
-}
diff --git a/internal/envoy/run.go b/internal/envoy/run.go
index 4710d4a..38f64fe 100644
--- a/internal/envoy/run.go
+++ b/internal/envoy/run.go
@@ -17,7 +17,6 @@ package envoy
 import (
 	"context"
 	"fmt"
-	"io"
 	"os/exec"
 	"os/signal"
 	"strings"
@@ -25,17 +24,17 @@ import (
 	"time"
 )
 
-// Run execs the binary at the path with the args passed. It is a blocking function that can be terminated via SIGINT.
+// Run execs the binary at the path with the args passed. It is a blocking function that can be shutdown via SIGINT.
 func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
-	// We can't use CommandContext even if that seems correct here. The reason is that we need to invoke preTerminate
-	// handlers, and they expect the process to still be running. For example, this allows admin API hooks.
+	// We can't use CommandContext even if that seems correct here. The reason is that we need to invoke shutdown hooks,
+	// and they expect the process to still be running. For example, this allows admin API hooks.
 	cmd := exec.Command(r.opts.EnvoyPath, args...) // #nosec -> users can run whatever binary they like!
 	cmd.Stdout = r.Out
 	cmd.Stderr = r.Err
 	cmd.SysProcAttr = sysProcAttr()
 	r.cmd = cmd
 
-	// suppress any error and replace it with the envoy exit status when > 1
+	// Suppress any error and replace it with the envoy exit status when > 1
 	defer func() {
 		if cmd.ProcessState != nil && cmd.ProcessState.ExitCode() > 0 {
 			if err != nil {
@@ -45,8 +44,7 @@ func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
 		}
 	}()
 
-	err = r.handlePreStart()
-	if err != nil {
+	if err := r.ensureAdminAddressPath(); err != nil {
 		return err
 	}
 
@@ -61,7 +59,7 @@ func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
 	defer waitCancel()
 	r.FakeInterrupt = sigCancel
 
-	// wait in a goroutine. We may need to kill the process if a signal occurs first.
+	// Wait in a goroutine. We may need to kill the process if a signal occurs first.
 	go func() {
 		defer waitCancel()
 		_ = r.cmd.Wait() // Envoy logs like "caught SIGINT" or "caught ENVOY_SIGTERM", so we don't repeat logging here.
@@ -71,16 +69,16 @@ func (r *Runtime) Run(ctx context.Context, args []string) (err error) {
 
 	// Block until we receive SIGINT or are canceled because Envoy has died
 	<-sigCtx.Done()
-	if cmd.ProcessState != nil {
-		return r.handlePostTermination()
+	if cmd.ProcessState != nil && !r.opts.DontArchiveRunDir {
+		return r.archiveRunDir()
 	}
 
-	r.handleTermination()
+	r.handleShutdown()
 
 	// Block until the process is complete. This ensures file descriptors are closed.
 	<-waitCtx.Done()
 
-	return r.handlePostTermination()
+	return r.archiveRunDir()
 }
 
 // awaitAdminAddress waits up to 2 seconds for the admin address to be available and logs it.
@@ -95,18 +93,3 @@ func awaitAdminAddress(sigCtx context.Context, r *Runtime) {
 		time.Sleep(200 * time.Millisecond)
 	}
 }
-
-// SetStdout writes the stdout of Envoy to the passed writer
-func (r *Runtime) SetStdout(fn func(io.Writer) io.Writer) {
-	r.cmd.Stdout = fn(r.cmd.Stdout)
-}
-
-// SetStderr writes the stderr of Envoy to the passed writer
-func (r *Runtime) SetStderr(fn func(io.Writer) io.Writer) {
-	r.cmd.Stderr = fn(r.cmd.Stderr)
-}
-
-// AppendArgs appends the passed args to the child process' args
-func (r *Runtime) AppendArgs(args []string) {
-	r.cmd.Args = append(r.cmd.Args, args...)
-}
diff --git a/internal/envoy/runtime.go b/internal/envoy/runtime.go
index 2d1c023..6d10d13 100644
--- a/internal/envoy/runtime.go
+++ b/internal/envoy/runtime.go
@@ -16,12 +16,12 @@ package envoy
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"io"
 	"net"
 	"os"
 	"os/exec"
+	"path/filepath"
 
 	"github.com/tetratelabs/getenvoy/internal/globals"
 )
@@ -46,7 +46,7 @@ type Runtime struct {
 	// End-to-end tests should kill the getenvoy process to achieve the same.
 	FakeInterrupt context.CancelFunc
 
-	preStart, preTermination, postTermination []func() error
+	shutdownHooks []func() error
 }
 
 // GetRunDir returns the run-specific directory files can be written to.
@@ -54,8 +54,31 @@ func (r *Runtime) GetRunDir() string {
 	return r.opts.RunDir
 }
 
+// ensureAdminAddressPath sets the "--admin-address-path" flag so that it can be used in /ready checks. If a value
+// already exists, it will be returned. Otherwise, the flag will be set to the file "admin-address.txt" in the
+// run directory. We don't use the working directory as sometimes that is a source directory.
+//
+// Notably, this allows ephemeral admin ports via bootstrap configuration admin/port_value=0 (minimum Envoy 1.12 for macOS support)
+func (r *Runtime) ensureAdminAddressPath() error {
+	args := r.cmd.Args
+	flag := `--admin-address-path`
+	for i, a := range args {
+		if a == flag {
+			if i+1 == len(args) || args[i+1] == "" {
+				return fmt.Errorf(`missing value to argument %q`, flag)
+			}
+			r.adminAddressPath = args[i+1]
+			return nil
+		}
+	}
+	// Envoy's run directory is mutable, so it is fine to write the admin address there.
+	r.adminAddressPath = filepath.Join(r.opts.RunDir, "admin-address.txt")
+	r.cmd.Args = append(r.cmd.Args, flag, r.adminAddressPath)
+	return nil
+}
+
 // GetAdminAddress returns the current admin address in host:port format, or empty if not yet available.
-// Exported for debug.EnableEnvoyAdminDataCollection, which is always on due to debug.EnableAll.
+// Exported for shutdown.enableEnvoyAdminDataCollection, which is always on due to shutdown.EnableHooks.
 func (r *Runtime) GetAdminAddress() (string, error) {
 	if r.adminAddress != "" { // We don't expect the admin address to change once written, so cache it.
 		return r.adminAddress, nil
@@ -70,11 +93,3 @@ func (r *Runtime) GetAdminAddress() (string, error) {
 	r.adminAddress = string(adminAddress)
 	return r.adminAddress, nil
 }
-
-// GetEnvoyPid returns the pid of the child process
-func (r *Runtime) GetEnvoyPid() (int, error) {
-	if r.cmd == nil || r.cmd.Process == nil {
-		return 0, errors.New("envoy process not yet started")
-	}
-	return r.cmd.Process.Pid, nil
-}
diff --git a/internal/envoy/termination.go b/internal/envoy/shutdown.go
similarity index 55%
rename from internal/envoy/termination.go
rename to internal/envoy/shutdown.go
index 29d5d04..3074e5b 100644
--- a/internal/envoy/termination.go
+++ b/internal/envoy/shutdown.go
@@ -23,54 +23,41 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/tar"
 )
 
-func (r *Runtime) handleTermination() {
-	defer r.interruptEnvoy() // Ensure the SIGINT forwards to Envoy even if a pre-termination hook panics
+// RegisterShutdownHook registers the passed functions to be run after Envoy has started
+// and just before GetEnvoy instructs Envoy to exit
+func (r *Runtime) RegisterShutdownHook(f ...func() error) {
+	r.shutdownHooks = append(r.shutdownHooks, f...)
+}
+
+func (r *Runtime) handleShutdown() {
+	defer r.interruptEnvoy() // Ensure the SIGINT forwards to Envoy even if a shutdown hook panics
 
-	fmt.Fprintln(r.Out, "invoking pre-termination hooks") //nolint
-	// Execute all registered preTermination functions
-	for _, f := range r.preTermination {
+	fmt.Fprintln(r.Out, "invoking shutdown hooks") //nolint
+	for _, f := range r.shutdownHooks {
 		if err := f(); err != nil {
-			fmt.Fprintln(r.Out, "failed pre-termination hook:", err) //nolint
+			fmt.Fprintln(r.Out, "failed shutdown hook:", err) //nolint
 		}
 	}
 }
 
 func (r *Runtime) interruptEnvoy() {
 	p := r.cmd.Process
-	fmt.Fprintln(r.Out, "stopping envoy") //nolint
+	fmt.Fprintln(r.Out, "shutting down envoy") //nolint
 	_ = p.Signal(syscall.SIGINT)
 }
 
-func (r *Runtime) handlePostTermination() error {
-	for _, f := range r.postTermination {
-		if err := f(); err != nil {
-			fmt.Fprintln(r.Out, "failed post-termination hook:", err) //nolint
-		}
-	}
-
+func (r *Runtime) archiveRunDir() error {
 	if r.opts.DontArchiveRunDir {
 		return nil
 	}
 
 	// Given ~/.getenvoy/debug/1620955405964267000
-	dirName := filepath.Dir(r.GetRunDir())                  // ~/.getenvoy/debug
+	dirName := filepath.Dir(r.GetRunDir())                  // ~/.getenvoy/runs
 	baseName := filepath.Base(r.GetRunDir())                // 1620955405964267000
-	targzName := filepath.Join(dirName, baseName+".tar.gz") // ~/.getenvoy/debug/1620955405964267000.tar.gz
+	targzName := filepath.Join(dirName, baseName+".tar.gz") // ~/.getenvoy/runs/1620955405964267000.tar.gz
 
 	if err := tar.TarGz(targzName, r.GetRunDir()); err != nil {
 		return fmt.Errorf("unable to archive run directory %v: %w", r.GetRunDir(), err)
 	}
 	return os.RemoveAll(r.GetRunDir())
 }
-
-// RegisterPreTermination registers the passed functions to be run after Envoy has started
-// and just before GetEnvoy instructs Envoy to terminate
-func (r *Runtime) RegisterPreTermination(f ...func() error) {
-	r.preTermination = append(r.preTermination, f...)
-}
-
-// RegisterPostTermination registers the passed functions to be run after Envoy has terminated
-// and just before GetEnvoy archives the run directory.
-func (r *Runtime) RegisterPostTermination(f ...func() error) {
-	r.postTermination = append(r.postTermination, f...)
-}
diff --git a/internal/envoy/debug/admin.go b/internal/envoy/shutdown/admin.go
similarity index 97%
rename from internal/envoy/debug/admin.go
rename to internal/envoy/shutdown/admin.go
index d1811b9..04907be 100644
--- a/internal/envoy/debug/admin.go
+++ b/internal/envoy/shutdown/admin.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package debug
+package shutdown
 
 import (
 	"fmt"
@@ -39,7 +39,7 @@ var adminAPIPaths = map[string]string{
 // enableEnvoyAdminDataCollection is a preset option that registers collection of Envoy Admin API information
 func enableEnvoyAdminDataCollection(r *envoy.Runtime) error {
 	e := envoyAdminDataCollection{r.GetAdminAddress, r.GetRunDir()}
-	r.RegisterPreTermination(e.retrieveAdminAPIData)
+	r.RegisterShutdownHook(e.retrieveAdminAPIData)
 	return nil
 }
 
diff --git a/internal/envoy/debug/node.go b/internal/envoy/shutdown/node.go
similarity index 97%
rename from internal/envoy/debug/node.go
rename to internal/envoy/shutdown/node.go
index 7e347d7..87a94bc 100644
--- a/internal/envoy/debug/node.go
+++ b/internal/envoy/shutdown/node.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package debug
+package shutdown
 
 import (
 	"context"
@@ -37,9 +37,9 @@ func enableNodeCollection(r *envoy.Runtime) error {
 		return fmt.Errorf("unable to create directory %q, so node data will not be captured: %w", nodeDir, err)
 	}
 	n := nodeCollection{nodeDir}
-	r.RegisterPreTermination(n.ps)
-	r.RegisterPreTermination(n.networkInterfaces)
-	r.RegisterPreTermination(n.activeConnections)
+	r.RegisterShutdownHook(n.ps)
+	r.RegisterShutdownHook(n.networkInterfaces)
+	r.RegisterShutdownHook(n.activeConnections)
 	return nil
 }
 
diff --git a/internal/envoy/debug/debug.go b/internal/envoy/shutdown/shutdown.go
similarity index 79%
rename from internal/envoy/debug/debug.go
rename to internal/envoy/shutdown/shutdown.go
index 78f62c3..b1ab395 100644
--- a/internal/envoy/debug/debug.go
+++ b/internal/envoy/shutdown/shutdown.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package debug
+package shutdown
 
 import (
 	"context"
@@ -24,20 +24,8 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/envoy"
 )
 
-// EnableAll enables all debug options and returns a possibly empty list of errors.
-func EnableAll(r *envoy.Runtime) []error {
-	var errs []error
-	for _, debug := range []func(*envoy.Runtime) error{
-		enableEnvoyAdminDataCollection,
-		enableEnvoyLogCollection,
-		enableNodeCollection,
-	} {
-		if err := debug(r); err != nil {
-			errs = append(errs, err)
-		}
-	}
-	return errs
-}
+// EnableHooks is a list of functions that enable shutdown hooks
+var EnableHooks = []func(*envoy.Runtime) error{enableEnvoyAdminDataCollection, enableNodeCollection}
 
 // wrapError wraps an error from using "gopsutil" or returns nil on "not implemented yet".
 // We don't err on unimplemented because we don't want to disturb users for unresolvable reasons.
diff --git a/internal/envoy/start.go b/internal/envoy/start.go
deleted file mode 100644
index 493ce61..0000000
--- a/internal/envoy/start.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2019 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package envoy
-
-import (
-	"fmt"
-	"path/filepath"
-)
-
-func (r *Runtime) handlePreStart() error {
-	// Execute all registered preStart functions
-	for _, f := range r.preStart {
-		if err := f(); err != nil {
-			return err
-		}
-	}
-	// Add admin path after we know the debug store location and other functions apply.
-	// NOTE: getenvoy isn't supported as a library, so we don't really need to worry about preStart hooks conflicting.
-	// If we are able to know the debug path sooner, we could refactor this later to be more direct.
-	return r.ensureAdminAddressPath()
-}
-
-// RegisterPreStart registers the passed functions to be run before Envoy has started
-func (r *Runtime) RegisterPreStart(f ...func() error) {
-	r.preStart = append(r.preStart, f...)
-}
-
-// ensureAdminAddressPath sets the "--admin-address-path" flag so that it can be used in /ready checks. If a value
-// already exists, it will be returned. Otherwise, the flag will be set to the file "admin-address.txt" in the
-// run directory. We don't use the working directory as sometimes that is a source directory.
-//
-// Notably, this allows ephemeral admin ports via bootstrap configuration admin/port_value=0 (minimum Envoy 1.12 for macOS support)
-func (r *Runtime) ensureAdminAddressPath() error {
-	args := r.cmd.Args
-	flag := `--admin-address-path`
-	for i, a := range args {
-		if a == flag {
-			if i+1 == len(args) || args[i+1] == "" {
-				return fmt.Errorf(`missing value to argument %q`, flag)
-			}
-			r.adminAddressPath = args[i+1]
-			return nil
-		}
-	}
-	// Envoy's run directory is mutable, so it is fine to write the admin address there.
-	r.adminAddressPath = filepath.Join(r.opts.RunDir, "admin-address.txt")
-	r.cmd.Args = append(r.cmd.Args, flag, r.adminAddressPath)
-	return nil
-}
diff --git a/internal/globals/globals.go b/internal/globals/globals.go
index c05b767..b821c7c 100644
--- a/internal/globals/globals.go
+++ b/internal/globals/globals.go
@@ -24,10 +24,10 @@ import (
 type RunOpts struct {
 	// EnvoyPath is the exec.Cmd path to "envoy". Defaults to "$HomeDir/versions/$version/bin/envoy"
 	EnvoyPath string
-	// RunDir is the location any generated configuration or debug files are written.
+	// RunDir is the location any generated files are written.
 	// This is not Envoy's working directory, which remains the same as the $PWD of getenvoy.
 	//
-	// Upon termination, this directory is archived as "../$(basename $RunDir).tar.gz"
+	// Upon shutdown, this directory is archived as "../$(basename $RunDir).tar.gz"
 	// Defaults to "$HomeDir/runs/$epochtime"
 	RunDir string
 	// DontArchiveRunDir is used in testing and prevents archiving the RunDir
diff --git a/site/usage.md b/site/usage.md
index 9f3e578..56d9d7a 100644
--- a/site/usage.md
+++ b/site/usage.md
@@ -15,7 +15,7 @@ your JSON first: https://getenvoy.io/envoy-versions-schema.json
 | Name | Usage |
 | ---- | ----- |
 | help | Shows how to use a [command] |
-| run | Run Envoy with the given [arguments...], collecting process state on termination |
+| run | Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C |
 | versions | List Envoy versions |
 | use | Sets the current [version] used by the "run" command, installing as necessary |
 | --version, -v | Print the version of GetEnvoy |


Test patch:
diff --git a/e2e/command_test.go b/e2e/command_test.go
index 61f1197..9cef846 100644
--- a/e2e/command_test.go
+++ b/e2e/command_test.go
@@ -53,7 +53,7 @@ func (b *cmdBuilder) exec() (string, string, error) {
 	return stdout.String(), stderr.String(), err
 }
 
-func (b *cmdBuilder) start(t *testing.T, terminateTimeout time.Duration) (io.Reader, io.Reader, func()) {
+func (b *cmdBuilder) start(t *testing.T, shutdownTimeout time.Duration) (io.Reader, io.Reader, func()) {
 	stdout := newSyncBuffer()
 	stderr := newSyncBuffer()
 	b.cmd.Stdout = io.MultiWriter(os.Stdout, stdout) // we want to see full `getenvoy` output in the test log
@@ -68,13 +68,13 @@ func (b *cmdBuilder) start(t *testing.T, terminateTimeout time.Duration) (io.Rea
 
 	return stdout, stderr, func() {
 		err := b.cmd.Process.Signal(syscall.SIGTERM)
-		require.NoError(t, err, `error terminating [%v]`, b.cmd)
+		require.NoError(t, err, `error shutting down [%v]`, b.cmd)
 
 		select {
 		case err := <-errc:
 			require.NoError(t, err, `error running [%v]`, b.cmd)
-		case <-time.After(terminateTimeout):
-			t.Fatal(fmt.Sprintf("getenvoy command didn't exit gracefully within %s", terminateTimeout))
+		case <-time.After(shutdownTimeout):
+			t.Fatal(fmt.Sprintf("getenvoy command didn't exit gracefully within %s", shutdownTimeout))
 		}
 	}
 }
diff --git a/e2e/getenvoy_run_test.go b/e2e/getenvoy_run_test.go
index bf36d8a..9ccc8f3 100644
--- a/e2e/getenvoy_run_test.go
+++ b/e2e/getenvoy_run_test.go
@@ -30,7 +30,7 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 )
 
-const terminateTimeout = 2 * time.Minute
+const shutdownTimeout = 2 * time.Minute
 
 // TestGetEnvoyRun runs the equivalent of "getenvoy run"
 //
@@ -41,22 +41,22 @@ func TestGetEnvoyRun(t *testing.T) {
 	// Below is the minimal config needed to run envoy
 	c := getEnvoy(`run`, "--config-yaml", "admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}")
 
-	stdout, stderr, terminate := c.start(t, terminateTimeout)
+	stdout, stderr, shutdown := c.start(t, shutdownTimeout)
 
 	// The underlying call is conditional to ensure errors that raise before we stop the server, stop it.
-	deferredTerminate := terminate
+	deferredShutdown := shutdown
 	defer func() {
-		if deferredTerminate != nil {
-			deferredTerminate()
+		if deferredShutdown != nil {
+			deferredShutdown()
 		}
 	}()
 
 	runDir := requireRunDir(t, stdout, c)
 	requireEnvoyReady(t, runDir, stderr, c)
 
-	log.Printf(`stopping Envoy after running [%v]`, c)
-	terminate()
-	deferredTerminate = nil
+	log.Printf(`shutting down Envoy after running [%v]`, c)
+	shutdown()
+	deferredShutdown = nil
 
 	verifyDebugDump(t, runDir, c)
 }
@@ -76,8 +76,8 @@ func TestGetEnvoyRun_StaticFilesystem(t *testing.T) {
 	require.NoError(t, os.WriteFile("response.txt", responseFromWorkingDirectory, 0600))
 	c := getEnvoy(`run`, "-c", "envoy.yaml")
 
-	stdout, stderr, terminate := c.start(t, terminateTimeout)
-	defer terminate()
+	stdout, stderr, shutdown := c.start(t, shutdownTimeout)
+	defer shutdown()
 
 	runDir := requireRunDir(t, stdout, c)
 	admin := requireEnvoyReady(t, runDir, stderr, c)
@@ -130,15 +130,15 @@ func verifyDebugDump(t *testing.T, workingDir string, c interface{}) {
 	defer os.Remove(runArchive) //nolint
 
 	src, err := os.Open(runArchive)
-	require.NoError(t, err, "error opening %s after stopping [%v]", runArchive, c)
+	require.NoError(t, err, "error opening %s after shutdown [%v]", runArchive, c)
 	err = tar.Untar(workingDir, src)
-	require.NoError(t, err, "error restoring %s from %s after stopping [%v]", workingDir, runArchive, c)
+	require.NoError(t, err, "error restoring %s from %s after shutdown [%v]", workingDir, runArchive, c)
 
 	// ensure the minimum contents exist
-	for _, filename := range []string{"config_dump.json", "stats.json"} {
+	for _, filename := range []string{"stdout.log", "stderr.log", "config_dump.json", "stats.json"} {
 		path := filepath.Join(workingDir, filename)
 		f, err := os.Stat(path)
-		require.NoError(t, err, `run archive %s doesn't contain %s after stopping [%v]`, runArchive, filename, c)
-		require.NotEmpty(t, f.Size(), `%s was empty after stopping [%v]`, filename, c)
+		require.NoError(t, err, `run archive %s doesn't contain %s after shutdown [%v]`, runArchive, filename, c)
+		require.NotEmpty(t, f.Size(), `%s was empty after shutdown [%v]`, filename, c)
 	}
 }
diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go
index a5462aa..a5b160c 100644
--- a/internal/cmd/help_test.go
+++ b/internal/cmd/help_test.go
@@ -43,9 +43,12 @@ func TestGetEnvoyHelp(t *testing.T) {
 				expected = fmt.Sprintf("getenvoy_%s_help.txt", command)
 			}
 
-			want, err := os.ReadFile(filepath.Join("testdata", expected))
+			bytes, err := os.ReadFile(filepath.Join("testdata", expected))
+			want := strings.ReplaceAll(string(bytes), "{VERSION}", version.GetEnvoy)
+			want = strings.ReplaceAll(want, "{ENVOY_VERSION}", version.LastKnownEnvoy)
+
 			require.NoError(t, err)
-			require.Equal(t, strings.ReplaceAll(string(want), "{ENVOY_VERSION}", version.LastKnownEnvoy), stdout.String())
+			require.Equal(t, want, stdout.String())
 		})
 	}
 }
diff --git a/internal/cmd/run_test.go b/internal/cmd/run_test.go
index d2b6521..ea7ad71 100644
--- a/internal/cmd/run_test.go
+++ b/internal/cmd/run_test.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"testing"
@@ -72,6 +73,28 @@ envoy args: %[2]s`, o.EnvoyPath, expectedEnvoyArgs)
 	}
 }
 
+func TestGetEnvoyRun_TeesConsoleToLogs(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
+	c, stdout, stderr := newApp(o)
+	o.Out = io.Discard         // stdout/stderr only includes what envoy writes, not our status messages
+	o.DontArchiveRunDir = true // we need to read-back the log files
+	err := c.Run([]string{"getenvoy", "run"})
+
+	require.NoError(t, err)
+
+	have, err := ioutil.ReadFile(filepath.Join(o.RunDir, "stdout.log"))
+	require.NoError(t, err)
+	require.NotEmpty(t, stdout.String()) // sanity check
+	require.Equal(t, stdout.String(), string(have))
+
+	have, err = ioutil.ReadFile(filepath.Join(o.RunDir, "stderr.log"))
+	require.NoError(t, err)
+	require.NotEmpty(t, stderr.String()) // sanity check
+	require.Equal(t, stderr.String(), string(have))
+}
+
 func TestGetEnvoyRun_ReadsHomeVersionFile(t *testing.T) {
 	o, cleanup := setupTest(t)
 	o.EnvoyVersion = "" // pretend this is an initial setup
diff --git a/internal/cmd/testdata/getenvoy_help.txt b/internal/cmd/testdata/getenvoy_help.txt
index c1f1f34..337c7b4 100644
--- a/internal/cmd/testdata/getenvoy_help.txt
+++ b/internal/cmd/testdata/getenvoy_help.txt
@@ -14,11 +14,11 @@ USAGE:
    your JSON first: https://getenvoy.io/envoy-versions-schema.json
 
 VERSION:
-   dev
+   {VERSION}
 
 COMMANDS:
    help      Shows how to use a [command]
-   run       Run Envoy with the given [arguments...], collecting process state on termination
+   run       Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C
    versions  List Envoy versions
    use       Sets the current [version] used by the "run" command, installing as necessary
 
diff --git a/internal/cmd/testdata/getenvoy_run_help.txt b/internal/cmd/testdata/getenvoy_run_help.txt
index 1229b11..2b5746d 100644
--- a/internal/cmd/testdata/getenvoy_run_help.txt
+++ b/internal/cmd/testdata/getenvoy_run_help.txt
@@ -1,5 +1,5 @@
 NAME:
-   getenvoy run - Run Envoy with the given [arguments...], collecting process state on termination
+   getenvoy run - Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C
 
 USAGE:
    getenvoy run [arguments...]
@@ -14,5 +14,7 @@ DESCRIPTION:
    $ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version
    ```
    
-   getenvoy generates files, e.g. logs, into `$GETENVOY_HOME/runs/$epochtime`.
-   These archive into `$GETENVOY_HOME/runs/$epochtime.tar.gz` upon termination.
+   While Envoy is running, the run directory (`$GETENVOY_HOME/runs/$epochtime`)
+   includes minimally "stdout.log" and "stderr.log". On Ctrl-C, shutdown hooks
+   write troubleshooting files, including admin endpoints, network and process
+   state. Upon exit, this archives as `$GETENVOY_HOME/runs/$epochtime.tar.gz`
diff --git a/internal/envoy/debug/log_test.go b/internal/envoy/envoy_test.go
similarity index 53%
rename from internal/envoy/debug/log_test.go
rename to internal/envoy/envoy_test.go
index 21548ae..d552df4 100644
--- a/internal/envoy/debug/log_test.go
+++ b/internal/envoy/envoy_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 Tetrate
+// Copyright 2021 Tetrate
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,24 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package debug
+package envoy
 
 import (
-	"path/filepath"
 	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 )
 
-func TestEnableEnvoyLogCollection(t *testing.T) {
-	workingDir, removeWorkingDir := morerequire.RequireNewTempDir(t)
-	defer removeWorkingDir()
-
-	runAndTerminateWithDebug(t, workingDir, enableEnvoyLogCollection)
-
-	for _, filename := range []string{"logs/access.log", "logs/error.log"} {
-		require.FileExists(t, filepath.Join(workingDir, filename))
+// RequireEnvoyPid returns the pid of the child process
+func RequireEnvoyPid(t *testing.T, r *Runtime) int {
+	if r.cmd == nil || r.cmd.Process == nil {
+		t.Fatal("envoy process not yet started")
 	}
+	return r.cmd.Process.Pid
 }
diff --git a/internal/envoy/run_test.go b/internal/envoy/run_test.go
index 4b4f9c7..4bc479a 100644
--- a/internal/envoy/run_test.go
+++ b/internal/envoy/run_test.go
@@ -46,28 +46,27 @@ func TestRuntime_Run(t *testing.T) {
 	tests := []struct {
 		name                           string
 		args                           []string
-		terminate                      func()
+		shutdown                       func()
 		expectedStdout, expectedStderr string
 		expectedErr                    string
-		expectedHooks                  []string
+		wantShutdownHook               bool
 	}{
 		{
 			name: "GetEnvoy Ctrl-C",
 			// Don't warn the user when they exited the process
-			expectedStdout: fmt.Sprintln("starting:", fakeEnvoy, adminFlag),
-			expectedStderr: "started\ncaught SIGINT\n",
-			expectedHooks:  []string{"preStart", "preTermination", "postTermination"},
+			expectedStdout:   fmt.Sprintln("starting:", fakeEnvoy, adminFlag),
+			expectedStderr:   "started\ncaught SIGINT\n",
+			wantShutdownHook: true,
 		},
 		// We don't test envoy dying from an external signal as it isn't reported back to the getenvoy process and
 		// Envoy returns exit status zero on anything except kill -9. We can't test kill -9 with a fake shell script.
 		{
 			name:           "Envoy exited with error",
-			terminate:      func() { time.Sleep(time.Millisecond * 100) },
+			shutdown:       func() { time.Sleep(time.Millisecond * 100) },
 			args:           []string{"quiet_exit=3"},
 			expectedStdout: fmt.Sprintln("starting:", fakeEnvoy, "quiet_exit=3", adminFlag),
 			expectedStderr: "started\n",
 			expectedErr:    "envoy exited with status: 3",
-			expectedHooks:  []string{"preStart"},
 		},
 	}
 
@@ -79,17 +78,28 @@ func TestRuntime_Run(t *testing.T) {
 
 			stdout := new(bytes.Buffer)
 			stderr := new(bytes.Buffer)
-			r, hooksCalled := newRuntimeWithMockHooks(t, stdout, stderr, o)
 
-			terminate := tc.terminate
-			if terminate == nil {
-				terminate = interrupt(r)
+			r := envoy.NewRuntime(o)
+			r.Out = stdout
+			r.Err = stderr
+			var haveShutdownHook bool
+			r.RegisterShutdownHook(func() error {
+				pid := envoy.RequireEnvoyPid(t, r)
+				_, err := os.FindProcess(pid)
+				require.NoError(t, err, "shutdownHooks called after process shutdown")
+				haveShutdownHook = true
+				return nil
+			})
+
+			shutdown := tc.shutdown
+			if shutdown == nil {
+				shutdown = interrupt(r)
 			}
 
 			// tee the error stream so we can look for the "started" line without consuming it.
 			errCopy := new(bytes.Buffer)
 			r.Err = io.MultiWriter(r.Err, errCopy)
-			err := test.RequireRunTerminate(t, terminate, r, errCopy, tc.args...)
+			err := test.RequireRun(t, shutdown, r, errCopy, tc.args...)
 
 			if tc.expectedErr == "" {
 				require.NoError(t, err)
@@ -98,7 +108,7 @@ func TestRuntime_Run(t *testing.T) {
 			}
 
 			// Assert appropriate hooks are called
-			require.Equal(t, tc.expectedHooks, *hooksCalled)
+			require.Equal(t, tc.wantShutdownHook, haveShutdownHook)
 
 			// Validate we ran what we thought we did
 			require.Equal(t, tc.expectedStdout, stdout.String())
@@ -125,33 +135,3 @@ func interrupt(r *envoy.Runtime) func() {
 		}
 	}
 }
-
-// This ensures functions are called in the correct order
-func newRuntimeWithMockHooks(t *testing.T, stdout, stderr io.Writer, o *globals.RunOpts) (*envoy.Runtime, *[]string) {
-	r := envoy.NewRuntime(o)
-	r.Out = stdout
-	r.Err = stderr
-	var hooks []string
-	r.RegisterPreStart(func() error {
-		_, err := r.GetEnvoyPid()
-		require.Error(t, err, "preTermination was called after process was started")
-		hooks = append(hooks, "preStart")
-		return nil
-	})
-
-	r.RegisterPreTermination(func() error {
-		pid, err := r.GetEnvoyPid()
-		require.NoError(t, err, "preTermination was called before process was started")
-		_, err = os.FindProcess(pid)
-		require.NoError(t, err, "preTermination was called after process was terminated")
-		hooks = append(hooks, "preTermination")
-		return nil
-	})
-
-	r.RegisterPreTermination(func() error {
-		hooks = append(hooks, "postTermination")
-		return nil
-	})
-
-	return r, &hooks
-}
diff --git a/internal/envoy/start_test.go b/internal/envoy/runtime_test.go
similarity index 65%
rename from internal/envoy/start_test.go
rename to internal/envoy/runtime_test.go
index 85ba1a3..2c7a016 100644
--- a/internal/envoy/start_test.go
+++ b/internal/envoy/runtime_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 Tetrate
+// Copyright 2021 Tetrate
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
 package envoy
 
 import (
-	"errors"
 	"os/exec"
 	"path/filepath"
 	"testing"
@@ -26,65 +25,6 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 )
 
-func TestHandlePreStartReturnsOnFirstError(t *testing.T) {
-	r := NewRuntime(nil)
-	err1, err2 := errors.New("1"), errors.New("2")
-	r.RegisterPreStart(func() error {
-		return err1
-	})
-	r.RegisterPreStart(func() error {
-		return err2
-	})
-
-	actualErr := r.handlePreStart()
-	require.Equal(t, err1, actualErr)
-}
-
-func TestHandlePreStartReturnsError(t *testing.T) {
-	r := NewRuntime(&globals.RunOpts{})
-	first := false
-	err := errors.New("1")
-	r.RegisterPreStart(func() error {
-		first = true
-		return nil
-	})
-	r.RegisterPreStart(func() error {
-		return err
-	})
-
-	actualErr := r.handlePreStart()
-	require.Equal(t, true, first)
-	require.Equal(t, err, actualErr)
-}
-
-func TestHandlePreStartEnsuresAdminAddressPath(t *testing.T) {
-	runDir, removeRunDir := morerequire.RequireNewTempDir(t)
-	defer removeRunDir()
-
-	r := NewRuntime(&globals.RunOpts{RunDir: runDir})
-	r.cmd = exec.Command("envoy")
-
-	// Verify the admin address path set (same assertion as ensureAdminAddressPath)
-	actualErr := r.handlePreStart()
-	require.NoError(t, actualErr)
-	require.Equal(t, []string{"envoy", "--admin-address-path", filepath.Join(runDir, "admin-address.txt")}, r.cmd.Args)
-}
-
-func TestHandlePreStartEnsuresAdminAddressPathLast(t *testing.T) {
-	r := NewRuntime(&globals.RunOpts{})
-	r.cmd = exec.Command("envoy")
-
-	r.RegisterPreStart(func() error {
-		r.AppendArgs([]string{"--admin-address-path", "/tmp/admin.txt"})
-		return nil
-	})
-
-	actualErr := r.handlePreStart()
-	require.NoError(t, actualErr)
-	require.Equal(t, "/tmp/admin.txt", r.adminAddressPath)
-	require.Equal(t, []string{"envoy", "--admin-address-path", "/tmp/admin.txt"}, r.cmd.Args)
-}
-
 func TestEnsureAdminAddressPath(t *testing.T) {
 	runDir, removeRunDir := morerequire.RequireNewTempDir(t)
 	defer removeRunDir()
@@ -130,7 +70,7 @@ func TestEnsureAdminAddressPath(t *testing.T) {
 	}
 }
 
-func TestEnsureAdminAddressPathValidateExisting(t *testing.T) {
+func TestEnsureAdminAddressPath_ValidateExisting(t *testing.T) {
 	tests := []struct {
 		name        string
 		args        []string
diff --git a/internal/envoy/debug/admin_test.go b/internal/envoy/shutdown/admin_test.go
similarity index 82%
rename from internal/envoy/debug/admin_test.go
rename to internal/envoy/shutdown/admin_test.go
index cff03c2..e2cca2f 100644
--- a/internal/envoy/debug/admin_test.go
+++ b/internal/envoy/shutdown/admin_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package debug
+package shutdown
 
 import (
 	"bytes"
@@ -45,7 +45,7 @@ func TestEnableEnvoyAdminDataCollection(t *testing.T) {
 	err := os.WriteFile(adminPath, []byte(mockAdmin.Listener.Addr().String()), 0600)
 	require.NoError(t, err)
 
-	runAndTerminateWithDebug(t, runDir, enableEnvoyAdminDataCollection, `--admin-address-path`, adminPath)
+	runWithShutdownHook(t, runDir, enableEnvoyAdminDataCollection, `--admin-address-path`, adminPath)
 
 	for _, filename := range adminAPIPaths {
 		path := filepath.Join(runDir, filename)
@@ -55,8 +55,8 @@ func TestEnableEnvoyAdminDataCollection(t *testing.T) {
 	}
 }
 
-// runAndTerminateWithDebug is like RequireRunTerminate, except returns a directory populated by the debug plugin.
-func runAndTerminateWithDebug(t *testing.T, runDir string, debug func(r *envoy.Runtime) error, args ...string) error {
+// runWithShutdownHook is like RequireRun, except invokes the hook on shutdown
+func runWithShutdownHook(t *testing.T, runDir string, hook func(r *envoy.Runtime) error, args ...string) error {
 	fakeEnvoy := filepath.Join(runDir, "envoy")
 	morerequire.RequireCaptureScript(t, fakeEnvoy)
 
@@ -66,9 +66,9 @@ func runAndTerminateWithDebug(t *testing.T, runDir string, debug func(r *envoy.R
 	r := envoy.NewRuntime(o)
 	r.Out = io.Discard
 	r.Err = stderr
-	require.NoError(t, debug(r))
+	require.NoError(t, hook(r))
 
-	return test.RequireRunTerminate(t, func() {
+	return test.RequireRun(t, func() {
 		fakeInterrupt := r.FakeInterrupt
 		if fakeInterrupt != nil {
 			fakeInterrupt()
diff --git a/internal/envoy/debug/node_test.go b/internal/envoy/shutdown/node_test.go
similarity index 95%
rename from internal/envoy/debug/node_test.go
rename to internal/envoy/shutdown/node_test.go
index 4bef4f5..ed1b669 100644
--- a/internal/envoy/debug/node_test.go
+++ b/internal/envoy/shutdown/node_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package debug
+package shutdown
 
 import (
 	"encoding/json"
@@ -30,7 +30,7 @@ func TestEnableNodeCollection(t *testing.T) {
 	workingDir, removeWorkingDir := morerequire.RequireNewTempDir(t)
 	defer removeWorkingDir()
 
-	runAndTerminateWithDebug(t, workingDir, enableNodeCollection)
+	runWithShutdownHook(t, workingDir, enableNodeCollection)
 
 	files := [...]string{"node/ps.txt", "node/network_interface.json", "node/connections.json"}
 	for _, file := range files {
diff --git a/internal/test/envoy.go b/internal/test/envoy.go
index a1240aa..8e9ba65 100644
--- a/internal/test/envoy.go
+++ b/internal/test/envoy.go
@@ -30,8 +30,8 @@ type Runner interface {
 	Run(ctx context.Context, args []string) (err error)
 }
 
-// RequireRunTerminate executes Run on the given Runtime and terminates it after starting.
-func RequireRunTerminate(t *testing.T, terminate func(), r Runner, stderr io.Reader, args ...string) (err error) {
+// RequireRun executes Run on the given Runtime and calls shutdown after it started.
+func RequireRun(t *testing.T, shutdown func(), r Runner, stderr io.Reader, args ...string) (err error) {
 	ctx, cancel := context.WithCancel(context.Background())
 	go func() {
 		err = r.Run(ctx, args)
@@ -44,7 +44,7 @@ func RequireRunTerminate(t *testing.T, terminate func(), r Runner, stderr io.Rea
 		return err != nil && strings.Contains(string(b), "started\n")
 	}, 2*time.Second, 100*time.Millisecond, "never started process")
 
-	terminate()
+	shutdown()
 
 	select { // Await run completion
 	case <-time.After(10 * time.Second):