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 }