github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/updater/watchdog/watchdog_test.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     5  // this source code is governed by the included BSD license.
     6  package watchdog
     7  
     8  import (
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"runtime"
    13  	"sync"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/keybase/client/go/updater/process"
    18  	"github.com/keybase/client/go/updater/util"
    19  	"github.com/keybase/go-logging"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  var testLog = &logging.Logger{Module: "test"}
    25  
    26  func TestWatchMultiple(t *testing.T) {
    27  	procProgram1 := procProgram(t, "testWatch1", "sleep")
    28  	procProgram2 := procProgram(t, "testWatch2", "sleep")
    29  	defer util.RemoveFileAtPath(procProgram1.Path)
    30  	defer util.RemoveFileAtPath(procProgram2.Path)
    31  
    32  	delay := 10 * time.Millisecond
    33  
    34  	err := Watch([]Program{procProgram1, procProgram2}, delay, testLog)
    35  	require.NoError(t, err)
    36  
    37  	matcher1 := process.NewMatcher(procProgram1.Path, process.PathEqual, testLog)
    38  	procs1, err := process.FindProcesses(matcher1, time.Second, 200*time.Millisecond, testLog)
    39  	require.NoError(t, err)
    40  	assert.Equal(t, 1, len(procs1))
    41  
    42  	matcher2 := process.NewMatcher(procProgram2.Path, process.PathEqual, testLog)
    43  	procs2, err := process.FindProcesses(matcher2, time.Second, 200*time.Millisecond, testLog)
    44  	require.NoError(t, err)
    45  	require.Equal(t, 1, len(procs2))
    46  	proc2 := procs2[0]
    47  
    48  	err = process.TerminatePID(proc2.Pid(), time.Millisecond, testLog)
    49  	require.NoError(t, err)
    50  
    51  	time.Sleep(2 * delay)
    52  
    53  	// Check for restart
    54  	procs2After, err := process.FindProcesses(matcher2, time.Second, time.Millisecond, testLog)
    55  	require.NoError(t, err)
    56  	require.Equal(t, 1, len(procs2After))
    57  }
    58  
    59  // TestTerminateBeforeWatch checks to make sure any existing processes are
    60  // terminated before a process is monitored.
    61  func TestTerminateBeforeWatch(t *testing.T) {
    62  	procProgram := procProgram(t, "testTerminateBeforeWatch", "sleep")
    63  	defer util.RemoveFileAtPath(procProgram.Path)
    64  
    65  	matcher := process.NewMatcher(procProgram.Path, process.PathEqual, testLog)
    66  
    67  	// Launch program (so we can test it gets terminated on watch)
    68  	err := exec.Command(procProgram.Path, procProgram.Args...).Start()
    69  	require.NoError(t, err)
    70  
    71  	procsBefore, err := process.FindProcesses(matcher, time.Second, time.Millisecond, testLog)
    72  	require.NoError(t, err)
    73  	require.Equal(t, 1, len(procsBefore))
    74  	pidBefore := procsBefore[0].Pid()
    75  	t.Logf("Pid before: %d", pidBefore)
    76  
    77  	// Start watching
    78  	err = Watch([]Program{procProgram}, 10*time.Millisecond, testLog)
    79  	require.NoError(t, err)
    80  
    81  	// Check again, and make sure it's a new process
    82  	procsAfter, err := process.FindProcesses(matcher, time.Second, time.Millisecond, testLog)
    83  	require.NoError(t, err)
    84  	require.Equal(t, 1, len(procsAfter))
    85  	pidAfter := procsAfter[0].Pid()
    86  	t.Logf("Pid after: %d", pidAfter)
    87  
    88  	assert.NotEqual(t, pidBefore, pidAfter)
    89  }
    90  
    91  // TestTerminateBeforeWatchRace verifies protection from the following scenario:
    92  //
    93  //	watchdog1 starts up
    94  //	watchdog1 looks up existing processes to terminate and sees none
    95  //	watchdog2 starts up
    96  //	watchdog2 looks up existing processes to terminate and sees watchdog1
    97  //	watchdog1 starts PROGRAM
    98  //	watchdog1 receives kill signal from watchdog2 and dies
    99  //	watchdog2 starts a second PROGRAM
   100  //	PROGRAM has a bad time
   101  //
   102  // The test doesn't protect us from the race condition generically, rather only when:
   103  //
   104  //	(1) PROGRAM is only started by a watchdog, and
   105  //	(2) PROGRAM and watchdog share a path to the same executable. When a
   106  //		watchdog looks up existing processes to terminate, it needs to be able
   107  //		to find another watchdog.
   108  func TestTerminateBeforeWatchRace(t *testing.T) {
   109  	var err error
   110  	// set up a bunch of iterations of the same program
   111  	programName := "TestTerminateBeforeWatchRace"
   112  	otherIterations := make([]Program, 6)
   113  	for i := 0; i < 6; i++ {
   114  		otherIterations[i] = procProgram(t, programName, "sleep")
   115  	}
   116  	mainProgram := procProgram(t, programName, "sleep")
   117  	defer util.RemoveFileAtPath(mainProgram.Path)
   118  	blocker := make(chan struct{})
   119  	go func() {
   120  		for _, p := range otherIterations[:3] {
   121  			_ = exec.Command(p.Path, p.Args...).Start()
   122  		}
   123  		blocker <- struct{}{}
   124  		for _, p := range otherIterations[3:] {
   125  			_ = exec.Command(p.Path, p.Args...).Start()
   126  		}
   127  	}()
   128  
   129  	// block until we definitely have something to kill
   130  	<-blocker
   131  	err = Watch([]Program{mainProgram}, 10*time.Millisecond, testLog)
   132  	require.NoError(t, err)
   133  
   134  	// Check and make sure there's only one of these processes running
   135  	matcher := process.NewMatcher(mainProgram.Path, process.PathEqual, testLog)
   136  	procsAfter, err := process.FindProcesses(matcher, time.Second, time.Millisecond, testLog)
   137  	require.NoError(t, err)
   138  	require.Equal(t, 1, len(procsAfter))
   139  }
   140  
   141  func TestExitOnSuccess(t *testing.T) {
   142  	procProgram := procProgram(t, "testExitOnSuccess", "echo")
   143  	procProgram.ExitOn = ExitOnSuccess
   144  	defer util.RemoveFileAtPath(procProgram.Path)
   145  
   146  	err := Watch([]Program{procProgram}, 0, testLog)
   147  	require.NoError(t, err)
   148  
   149  	time.Sleep(50 * time.Millisecond)
   150  
   151  	matcher := process.NewMatcher(procProgram.Path, process.PathEqual, testLog)
   152  	procsAfter, err := process.WaitForExit(matcher, 500*time.Millisecond, 50*time.Millisecond, testLog)
   153  	require.NoError(t, err)
   154  	assert.Equal(t, 0, len(procsAfter))
   155  }
   156  
   157  func procTestPath(name string) (string, string) {
   158  	// Copy test executable to tmp
   159  	if runtime.GOOS == "windows" {
   160  		return filepath.Join(os.Getenv("GOPATH"), "bin", "test.exe"), filepath.Join(os.TempDir(), name+".exe")
   161  	}
   162  	return filepath.Join(os.Getenv("GOPATH"), "bin", "test"), filepath.Join(os.TempDir(), name)
   163  }
   164  
   165  // procProgram returns a testable unique program at a temporary location
   166  func procProgram(t *testing.T, name string, testCommand string) Program {
   167  	path, procPath := procTestPath(name)
   168  	err := util.CopyFile(path, procPath, testLog)
   169  	require.NoError(t, err)
   170  	err = os.Chmod(procPath, 0777)
   171  	require.NoError(t, err)
   172  	// Temp dir might have symlinks in which case we need the eval'ed path
   173  	procPath, err = filepath.EvalSymlinks(procPath)
   174  	require.NoError(t, err)
   175  	return Program{
   176  		Path: procPath,
   177  		Args: []string{testCommand},
   178  		Name: name,
   179  	}
   180  }
   181  
   182  // TestExitAllOnSuccess verifies that a program with ExitAllOnSuccess that exits cleanly
   183  // will also cause a clean exit on another program which has been restarted by the watchdog.
   184  func TestExitAllOnSuccess(t *testing.T) {
   185  	t.Skip("flakey :(")
   186  	// This test is slow and I'm sorry about that.
   187  	sleepTimeInTest := 10000 // 10 seconds
   188  	exiter := procProgram(t, "testExitAllOnSuccess", "sleep")
   189  	defer util.RemoveFileAtPath(exiter.Path)
   190  	exiter.ExitOn = ExitAllOnSuccess
   191  	testProgram := procProgram(t, "alice", "sleep")
   192  	defer util.RemoveFileAtPath(testProgram.Path)
   193  
   194  	getProgramPID := func(program Program) int {
   195  		matcher := process.NewMatcher(program.Path, process.PathEqual, testLog)
   196  		procs, err := process.FindProcesses(matcher, time.Second, 100*time.Millisecond, testLog)
   197  		require.NoError(t, err)
   198  		require.Equal(t, 1, len(procs))
   199  		proc := procs[0]
   200  		return proc.Pid()
   201  	}
   202  
   203  	// watch two programs, 1 that will exit cleanly and 1 that won't
   204  	programs := []Program{exiter, testProgram}
   205  	err := Watch(programs, 0, testLog)
   206  	require.NoError(t, err)
   207  
   208  	// bounce the testProgram halfway through the sleep interval
   209  	firstPid := getProgramPID(testProgram)
   210  	require.NotEqual(t, 0, firstPid)
   211  	time.Sleep(time.Duration(sleepTimeInTest/2) * time.Second)
   212  	err = process.TerminatePID(firstPid, time.Millisecond, testLog)
   213  	require.NoError(t, err)
   214  	time.Sleep(100 * time.Millisecond)
   215  	secondPid := getProgramPID(testProgram)
   216  	require.NotEqual(t, 0, secondPid)
   217  	require.NotEqual(t, firstPid, secondPid)
   218  
   219  	// sleep until the exiter program should have exited cleanly
   220  	// and triggered the exit of everything else
   221  	time.Sleep(time.Duration((sleepTimeInTest/2)+1) * time.Second)
   222  
   223  	assertProgramEnded := func(program Program) {
   224  		matcher := process.NewMatcher(program.Path, process.PathEqual, testLog)
   225  		procs, err := process.FindProcesses(matcher, time.Second, 100*time.Millisecond, testLog)
   226  		require.NoError(t, err)
   227  		require.Equal(t, 0, len(procs))
   228  	}
   229  
   230  	assertProgramEnded(exiter)
   231  	assertProgramEnded(testProgram)
   232  }
   233  
   234  func TestWatchdogExitAllRace(t *testing.T) {
   235  	t.Skip("flakey :(")
   236  	exiter := procProgram(t, "TestWatchdogExitAllRace", "sleep")
   237  	defer util.RemoveFileAtPath(exiter.Path)
   238  	exiter.ExitOn = ExitAllOnSuccess
   239  	procProgram1 := procProgram(t, "alice", "sleep")
   240  	defer util.RemoveFileAtPath(procProgram1.Path)
   241  	procProgram2 := procProgram(t, "bob", "sleep")
   242  	defer util.RemoveFileAtPath(procProgram2.Path)
   243  
   244  	assertOneProcessIsRunning := func(p Program) {
   245  		matcher := process.NewMatcher(p.Path, process.PathEqual, testLog)
   246  		procs, err := process.FindProcesses(matcher, time.Second, 200*time.Millisecond, testLog)
   247  		require.NoError(t, err)
   248  		assert.Equal(t, 1, len(procs))
   249  	}
   250  	assertOneProcessOfEachProgramIsRunning := func() {
   251  		assertOneProcessIsRunning(exiter)
   252  		assertOneProcessIsRunning(procProgram1)
   253  		assertOneProcessIsRunning(procProgram2)
   254  	}
   255  
   256  	// spin up three watchdogs at the same time with the same three programs
   257  	var wg sync.WaitGroup
   258  	for i := 0; i < 3; i++ {
   259  		wg.Add(1)
   260  		go func() {
   261  			defer wg.Done()
   262  			err := Watch([]Program{exiter, procProgram1, procProgram2}, 0, testLog)
   263  			require.NoError(t, err)
   264  		}()
   265  	}
   266  	wg.Wait()
   267  	assertOneProcessOfEachProgramIsRunning()
   268  	time.Sleep(500 * time.Millisecond)
   269  	assertOneProcessOfEachProgramIsRunning()
   270  
   271  	err := Watch([]Program{exiter, procProgram1, procProgram2}, 0, testLog)
   272  	require.NoError(t, err)
   273  	assertOneProcessOfEachProgramIsRunning()
   274  }