283 lines
7 KiB
Go
283 lines
7 KiB
Go
package proxyprocess
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testLogger is a logger that can be used by tests that require a
|
|
// *log.Logger instance.
|
|
var testLogger = log.New(os.Stderr, "logger: ", log.LstdFlags)
|
|
|
|
// testTempDir returns a temporary directory and a cleanup function.
|
|
func testTempDir(t *testing.T) (string, func()) {
|
|
t.Helper()
|
|
|
|
td, err := ioutil.TempDir("", "test-agent-proxy")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
return td, func() {
|
|
if err := os.RemoveAll(td); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// helperProcessSentinel is a sentinel value that is put as the first
|
|
// argument following "--" and is used to determine if TestHelperProcess
|
|
// should run.
|
|
const helperProcessSentinel = "WANT_HELPER_PROCESS"
|
|
|
|
// helperProcess returns an *exec.Cmd that can be used to execute the
|
|
// TestHelperProcess function below. This can be used to test multi-process
|
|
// interactions.
|
|
func helperProcess(s ...string) (*exec.Cmd, func()) {
|
|
cs := []string{"-test.run=TestHelperProcess", "--", helperProcessSentinel}
|
|
cs = append(cs, s...)
|
|
|
|
cmd := exec.Command(os.Args[0], cs...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
destroy := func() {
|
|
if p := cmd.Process; p != nil {
|
|
p.Kill()
|
|
}
|
|
}
|
|
return cmd, destroy
|
|
}
|
|
|
|
// This is not a real test. This is just a helper process kicked off by tests
|
|
// using the helperProcess helper function.
|
|
func TestHelperProcess(t *testing.T) {
|
|
args := os.Args
|
|
for len(args) > 0 {
|
|
if args[0] == "--" {
|
|
args = args[1:]
|
|
break
|
|
}
|
|
|
|
args = args[1:]
|
|
}
|
|
|
|
if len(args) == 0 || args[0] != helperProcessSentinel {
|
|
return
|
|
}
|
|
|
|
defer os.Exit(0)
|
|
args = args[1:] // strip sentinel value
|
|
cmd, args := args[0], args[1:]
|
|
switch cmd {
|
|
// While running, this creates a file in the given directory (args[0])
|
|
// and deletes it only when it is stopped.
|
|
case "start-stop":
|
|
limitProcessLifetime(2 * time.Minute)
|
|
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
|
defer signal.Stop(ch)
|
|
|
|
path := args[0]
|
|
var data []byte
|
|
data = append(data, []byte(os.Getenv(EnvProxyID))...)
|
|
data = append(data, ':')
|
|
data = append(data, []byte(os.Getenv(EnvProxyToken))...)
|
|
|
|
if err := ioutil.WriteFile(path, data, 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Remove(path)
|
|
|
|
<-ch
|
|
|
|
// Restart writes to a file and keeps running while that file still
|
|
// exists. When that file is removed, this process exits. This can be
|
|
// used to test restarting.
|
|
case "restart":
|
|
limitProcessLifetime(2 * time.Minute)
|
|
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, os.Interrupt)
|
|
defer signal.Stop(ch)
|
|
|
|
// Write the file
|
|
path := args[0]
|
|
if err := ioutil.WriteFile(path, []byte("hello"), 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// While the file still exists, do nothing. When the file no longer
|
|
// exists, we exit.
|
|
for {
|
|
time.Sleep(25 * time.Millisecond)
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ch:
|
|
// We received an interrupt, clean exit
|
|
os.Remove(path)
|
|
break
|
|
|
|
default:
|
|
}
|
|
}
|
|
case "stop-kill":
|
|
limitProcessLifetime(2 * time.Minute)
|
|
|
|
// Setup listeners so it is ignored
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, os.Interrupt)
|
|
defer signal.Stop(ch)
|
|
|
|
path := args[0]
|
|
data := []byte(os.Getenv(EnvProxyToken))
|
|
for {
|
|
if err := ioutil.WriteFile(path, data, 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
time.Sleep(25 * time.Millisecond)
|
|
}
|
|
// Check if the external process can access the enivironmental variables
|
|
case "environ":
|
|
limitProcessLifetime(2 * time.Minute)
|
|
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, os.Interrupt)
|
|
defer signal.Stop(stop)
|
|
|
|
//Get the path for the file to be written to
|
|
path := args[0]
|
|
var data []byte
|
|
|
|
//Get the environmental variables
|
|
envData := os.Environ()
|
|
|
|
//Sort the env data for easier comparison
|
|
sort.Strings(envData)
|
|
for _, envVariable := range envData {
|
|
if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
|
|
continue
|
|
}
|
|
data = append(data, envVariable...)
|
|
data = append(data, "\n"...)
|
|
}
|
|
if err := ioutil.WriteFile(path, data, 0644); err != nil {
|
|
t.Fatalf("[Error] File write failed : %s", err)
|
|
}
|
|
|
|
// Clean up after we receive the signal to exit
|
|
defer os.Remove(path)
|
|
|
|
<-stop
|
|
|
|
case "output":
|
|
limitProcessLifetime(2 * time.Minute)
|
|
|
|
fmt.Fprintf(os.Stdout, "hello stdout\n")
|
|
fmt.Fprintf(os.Stderr, "hello stderr\n")
|
|
|
|
// Sync to be sure it is written out of buffers
|
|
os.Stdout.Sync()
|
|
os.Stderr.Sync()
|
|
|
|
// Output a file to signal we've written to stdout/err
|
|
path := args[0]
|
|
if err := ioutil.WriteFile(path, []byte("hello"), 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
<-make(chan struct{})
|
|
|
|
// Parent runs the given process in a Daemon and then sleeps until the test
|
|
// code kills it. It exists to test that the Daemon-managed child process
|
|
// survives it's parent exiting which we can't test directly without exiting
|
|
// the test process so we need an extra level of indirection. The test code
|
|
// using this must pass a file path as the first argument for the child
|
|
// processes PID to be written and then must take care to clean up that PID
|
|
// later or the child will be left running forever.
|
|
//
|
|
// If the PID file already exists, it will "adopt" the child rather than
|
|
// launch a new one.
|
|
case "parent":
|
|
limitProcessLifetime(2 * time.Minute)
|
|
|
|
// We will write the PID for the child to the file in the first argument
|
|
// then pass rest of args through to command.
|
|
pidFile := args[0]
|
|
|
|
cmd, destroyChild := helperProcess(args[1:]...)
|
|
defer destroyChild()
|
|
|
|
d := &Daemon{
|
|
Command: cmd,
|
|
Logger: testLogger,
|
|
PidPath: pidFile,
|
|
}
|
|
|
|
_, err := os.Stat(pidFile)
|
|
if err == nil {
|
|
// pidFile exists, read it and "adopt" the process
|
|
bs, err := ioutil.ReadFile(pidFile)
|
|
if err != nil {
|
|
log.Printf("Error: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
pid, err := strconv.Atoi(string(bs))
|
|
if err != nil {
|
|
log.Printf("Error: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
// Make a fake snapshot to load
|
|
snapshot := map[string]interface{}{
|
|
"Pid": pid,
|
|
"CommandPath": d.Command.Path,
|
|
"CommandArgs": d.Command.Args,
|
|
"CommandDir": d.Command.Dir,
|
|
"CommandEnv": d.Command.Env,
|
|
"ProxyToken": "",
|
|
}
|
|
d.UnmarshalSnapshot(snapshot)
|
|
}
|
|
|
|
if err := d.Start(); err != nil {
|
|
log.Printf("Error: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Println("Started child")
|
|
|
|
// Wait "forever" (calling test chooses when we exit with signal/Wait to
|
|
// minimize coordination).
|
|
for {
|
|
time.Sleep(time.Hour)
|
|
}
|
|
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// limitProcessLifetime installs a background goroutine that self-exits after
|
|
// the specified duration elapses to prevent leaking processes from tests that
|
|
// may spawn them.
|
|
func limitProcessLifetime(dur time.Duration) {
|
|
go time.AfterFunc(dur, func() {
|
|
os.Exit(99)
|
|
})
|
|
}
|