github.com/immortal/immortal@v0.0.0-20240201195854-d8073cd41019/daemon_test.go (about) 1 package immortal 2 3 import ( 4 "bytes" 5 "crypto/rand" 6 "encoding/base64" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "os" 11 "os/user" 12 "path/filepath" 13 "strings" 14 "sync/atomic" 15 "testing" 16 "time" 17 ) 18 19 func TestDaemonNewCtl(t *testing.T) { 20 dir, err := ioutil.TempDir("", "TestDaemonNewCtl") 21 if err != nil { 22 t.Error(err) 23 } 24 defer os.RemoveAll(dir) 25 cfg := &Config{ 26 Cwd: dir, 27 ctl: dir, 28 } 29 d, err := New(cfg) 30 if err != nil { 31 t.Fatal(err) 32 } 33 if _, err = os.Stat(filepath.Join(dir, "lock")); err != nil { 34 t.Fatal(err) 35 } 36 expect(t, uint32(0), d.lock) 37 expect(t, uint32(0), d.lockOnce) 38 // test lock 39 _, err = New(cfg) 40 if err == nil { 41 t.Error("Expecting error: resource temporarily unavailable") 42 } 43 } 44 45 func TestDaemonNewCtlErr(t *testing.T) { 46 dir, err := ioutil.TempDir("", "TestDaemonNewCtlErr") 47 if err != nil { 48 t.Error(err) 49 } 50 defer os.RemoveAll(dir) 51 cwd, err := os.Getwd() 52 if err != nil { 53 t.Error(err) 54 } 55 defer os.Chdir(cwd) 56 if err := os.Chdir(dir); err != nil { 57 t.Error(err) 58 } 59 os.Chmod(dir, 0000) 60 cfg := &Config{ 61 ctl: dir, 62 } 63 _, err = New(cfg) 64 if err == nil { 65 t.Error("Expecting error") 66 } 67 } 68 69 func TestDaemonNewCtlCwd(t *testing.T) { 70 dir, err := ioutil.TempDir("", "TestDaemonNewCtrlCwd") 71 if err != nil { 72 t.Error(err) 73 } 74 defer os.RemoveAll(dir) 75 cwd, err := os.Getwd() 76 if err != nil { 77 t.Error(err) 78 } 79 defer os.Chdir(cwd) 80 if err := os.Chdir(dir); err != nil { 81 t.Error(err) 82 } 83 cfg := &Config{ 84 ctl: dir, 85 } 86 d, err := New(cfg) 87 if err != nil { 88 t.Error(err) 89 } 90 if _, err = os.Stat(filepath.Join(dir, "lock")); err != nil { 91 t.Fatal(err) 92 } 93 expect(t, uint32(0), d.lock) 94 expect(t, uint32(0), d.lockOnce) 95 // test lock 96 _, err = New(cfg) 97 if err == nil { 98 t.Error("Expecting error: resource temporarily unavailable") 99 } 100 } 101 102 func TestBadUid(t *testing.T) { 103 dir, err := ioutil.TempDir("", "TestBadUid") 104 if err != nil { 105 t.Error(err) 106 } 107 defer os.RemoveAll(dir) 108 cfg := &Config{ 109 command: []string{"go"}, 110 user: &user.User{Uid: "uid", Gid: "0"}, 111 ctl: dir, 112 } 113 d, err := New(cfg) 114 if err != nil { 115 t.Fatal(err) 116 } 117 _, err = d.Run(NewProcess(cfg)) 118 if err == nil { 119 t.Error("Expecting error") 120 } 121 } 122 123 func TestBadGid(t *testing.T) { 124 dir, err := ioutil.TempDir("", "TestBadGid") 125 if err != nil { 126 t.Error(err) 127 } 128 defer os.RemoveAll(dir) 129 cfg := &Config{ 130 command: []string{"go"}, 131 user: &user.User{Uid: "0", Gid: "gid"}, 132 ctl: dir, 133 } 134 d, err := New(cfg) 135 if err != nil { 136 t.Fatal(err) 137 } 138 _, err = d.Run(NewProcess(cfg)) 139 if err == nil { 140 t.Error("Expecting error") 141 } 142 } 143 144 func TestBadPid(t *testing.T) { 145 dir, err := ioutil.TempDir("", "TestBadPid") 146 if err != nil { 147 t.Error(err) 148 } 149 defer os.RemoveAll(dir) 150 cfg := &Config{ 151 command: []string{"go"}, 152 ctl: dir, 153 } 154 d, err := New(cfg) 155 if err != nil { 156 t.Fatal(err) 157 } 158 _, err = d.ReadPidFile("/dev/null/non-existent") 159 if err == nil { 160 t.Error("Expecting error") 161 } 162 } 163 164 func TestUser(t *testing.T) { 165 dir, err := ioutil.TempDir("", "TestUser") 166 if err != nil { 167 t.Error(err) 168 } 169 defer os.RemoveAll(dir) 170 cfg := &Config{ 171 command: []string{"go"}, 172 user: &user.User{Uid: "0", Gid: "0"}, 173 ctl: dir, 174 } 175 d, err := New(cfg) 176 if err != nil { 177 t.Fatal(err) 178 } 179 _, err = d.Run(NewProcess(cfg)) 180 if err == nil { 181 t.Error("Expecting error") 182 } 183 } 184 185 func TestBadWritePidParent(t *testing.T) { 186 dir, err := ioutil.TempDir("", "TestBadWritePidParent") 187 if err != nil { 188 t.Error(err) 189 } 190 defer os.RemoveAll(dir) 191 var mylog bytes.Buffer 192 log.SetOutput(&mylog) 193 log.SetFlags(0) 194 cfg := &Config{ 195 command: []string{"go"}, 196 Pid: Pid{ 197 Parent: "/dev/null/parent.pid", 198 }, 199 ctl: dir, 200 } 201 d, err := New(cfg) 202 if err != nil { 203 t.Fatal(err) 204 } 205 _, err = d.Run(NewProcess(cfg)) 206 if err != nil { 207 t.Fatal(err) 208 } 209 expect(t, "open /dev/null/parent.pid: not a directory", strings.TrimSpace(mylog.String())) 210 } 211 212 func TestBadWritePidChild(t *testing.T) { 213 var mylog bytes.Buffer 214 log.SetOutput(&mylog) 215 log.SetFlags(0) 216 cfg := &Config{ 217 command: []string{"go"}, 218 Pid: Pid{ 219 Child: "/dev/null/child.pid", 220 }, 221 } 222 d, err := New(cfg) 223 if err != nil { 224 t.Fatal(err) 225 } 226 _, err = d.Run(NewProcess(cfg)) 227 if err != nil { 228 t.Fatal(err) 229 } 230 expect(t, "open /dev/null/child.pid: not a directory", strings.TrimSpace(mylog.String())) 231 ctl := &Controller{} 232 err = ctl.PurgeServices(filepath.Join(d.supDir, "immortal.sock")) 233 if err != nil { 234 t.Fatal(err) 235 } 236 } 237 238 func TestSignalsUDOT(t *testing.T) { 239 sdir, err := ioutil.TempDir("", "TestSignalsUDOT") 240 if err != nil { 241 t.Error(err) 242 } 243 defer os.RemoveAll(sdir) 244 tmpfile, err := ioutil.TempFile(sdir, "log.") 245 if err != nil { 246 t.Error(err) 247 } 248 cfg := &Config{ 249 Env: map[string]string{"GO_WANT_HELPER_PROCESS": "signalsUDOT"}, 250 command: []string{os.Args[0]}, 251 Cwd: sdir, 252 ctl: sdir, 253 Pid: Pid{ 254 Parent: filepath.Join(sdir, "parent.pid"), 255 Child: filepath.Join(sdir, "child.pid"), 256 }, 257 Log: Log{ 258 File: tmpfile.Name(), 259 }, 260 } 261 // create new daemon 262 d, err := New(cfg) 263 if err != nil { 264 t.Fatal(err) 265 } 266 267 np := NewProcess(cfg) 268 expect(t, 0, np.Pid()) 269 p, err := d.Run(np) 270 if err != nil { 271 t.Error(err) 272 } 273 274 // create socket 275 if err := d.Listen(); err != nil { 276 t.Fatal(err) 277 } 278 279 // check pids 280 if pid, err := d.ReadPidFile(filepath.Join(sdir, "parent.pid")); err != nil { 281 t.Error(err) 282 } else { 283 expect(t, os.Getpid(), pid) 284 } 285 if pid, err := d.ReadPidFile(filepath.Join(sdir, "child.pid")); err != nil { 286 t.Error(err, pid) 287 } else { 288 expect(t, p.Pid(), pid) 289 } 290 291 // check lock 292 if _, err = os.Stat(filepath.Join(sdir, "immortal.sock")); err != nil { 293 t.Fatal(err) 294 } 295 296 status := &Status{} 297 ctl := &Controller{} 298 signalResponse := &SignalResponse{} 299 if status, err = ctl.GetStatus(filepath.Join(sdir, "immortal.sock")); err != nil { 300 t.Fatal(err) 301 } 302 expect(t, true, strings.HasSuffix(status.Cmd, "/immortal.test")) 303 expect(t, 1, status.Count) 304 305 // http socket client 306 // test "k", process should restart and get a new pid 307 t.Log("testing k") 308 expect(t, p.Pid(), status.Pid) 309 310 if signalResponse, err = ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "k"); err != nil { 311 t.Fatal(err) 312 } 313 expect(t, "", signalResponse.Err) 314 315 // wait for process to finish 316 err = <-p.errch 317 atomic.StoreUint32(&d.lock, d.lockOnce) 318 expect(t, "signal: killed", err.Error()) 319 p, err = d.Run(NewProcess(cfg)) 320 if err != nil { 321 t.Error(err) 322 } 323 324 if status.Pid == p.Pid() { 325 t.Fatalf("Expecting a new pid") 326 } 327 328 // before when not using TestMain 329 // $ pgrep -fl TestHelperProcessSignalsUDO 330 // PID _test/immortal.test -test.run=TestHelperProcessSignalsUDOT -- 331 332 // test "d", (keep it down and don't restart) 333 t.Log("testing d") 334 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "d"); err != nil { 335 t.Fatal(err) 336 } 337 // wait for process to finish 338 err = <-p.errch 339 atomic.StoreUint32(&d.lock, d.lockOnce) 340 expect(t, "signal: terminated", err.Error()) 341 np = NewProcess(cfg) 342 p, err = d.Run(np) 343 if err == nil { 344 t.Error("Expecting an error") 345 } else { 346 close(np.quit) 347 } 348 349 // test "u" 350 t.Log("testing up") 351 go func() { 352 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "up"); err != nil { 353 t.Fatal(err) 354 } 355 }() 356 <-d.run 357 p, err = d.Run(NewProcess(cfg)) 358 if err != nil { 359 t.Error(err) 360 } 361 362 // test "once", process should not restart after going down 363 t.Log("testing once") 364 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "o"); err != nil { 365 t.Fatal(err) 366 } 367 368 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "k"); err != nil { 369 t.Fatal(err) 370 } 371 // wait for process to finish 372 err = <-p.errch 373 atomic.StoreUint32(&d.lock, d.lockOnce) 374 expect(t, "signal: killed", err.Error()) 375 np = NewProcess(cfg) 376 p, err = d.Run(np) 377 if err == nil { 378 t.Error("Expecting an error") 379 } else { 380 close(np.quit) 381 } 382 383 if status, err = ctl.GetStatus(filepath.Join(sdir, "immortal.sock")); err != nil { 384 t.Fatal(err) 385 } 386 expect(t, 3, status.Count) 387 388 // test "u" 389 t.Log("testing u") 390 go func() { 391 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "u"); err != nil { 392 t.Fatal(err) 393 } 394 }() 395 <-d.run 396 p, err = d.Run(NewProcess(cfg)) 397 if err != nil { 398 t.Error(err) 399 } 400 oldPid := p.Pid() 401 402 // test "t" 403 t.Log("testing t") 404 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "t"); err != nil { 405 t.Fatal(err) 406 } 407 err = <-p.errch 408 atomic.StoreUint32(&d.lock, d.lockOnce) 409 expect(t, "signal: terminated", err.Error()) 410 411 // restart to get new pid 412 p, err = d.Run(NewProcess(cfg)) 413 if err != nil { 414 t.Error(err) 415 } 416 if oldPid == p.Pid() { 417 t.Fatal("Expecting a new pid") 418 } 419 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "kill"); err != nil { 420 t.Fatal(err) 421 } 422 err = <-p.errch 423 atomic.StoreUint32(&d.lock, d.lockOnce) 424 expect(t, "signal: killed", err.Error()) 425 426 // test after 427 p, err = d.Run(NewProcess(cfg)) 428 if err != nil { 429 t.Error(err) 430 } 431 432 DONE: 433 for { 434 select { 435 case err := <-p.errch: 436 expect(t, "signal: killed", err.Error()) 437 break DONE 438 case <-time.After(1 * time.Second): 439 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "kill"); err != nil { 440 t.Fatal(err) 441 } 442 } 443 } 444 445 if status, err = ctl.GetStatus(filepath.Join(sdir, "immortal.sock")); err != nil { 446 t.Fatal(err) 447 } 448 expect(t, 6, status.Count) 449 450 // test log content 451 t.Log("testing logfile") 452 content, err := ioutil.ReadFile(tmpfile.Name()) 453 if err != nil { 454 t.Fatal(err) 455 } 456 457 lines := strings.Split(string(content), "\n") 458 expect(t, true, strings.HasSuffix(lines[0], "5D675098-45D7-4089-A72C-3628713EA5BA")) 459 460 // halt 461 if _, err := ctl.SendSignal(filepath.Join(sdir, "immortal.sock"), "halt"); err != nil { 462 t.Fatal(err) 463 } 464 // wait for socket to be close 465 d.wg.Wait() 466 467 err = ctl.PurgeServices(filepath.Join(sdir, "immortal.sock")) 468 if err == nil { 469 t.Fatal(err) 470 } 471 472 // remove log.* file 473 files, err := filepath.Glob(filepath.Join(sdir, "log.*")) 474 if err != nil { 475 t.Fatal(err) 476 } 477 for _, f := range files { 478 if err := os.Remove(f); err != nil { 479 t.Fatal(err) 480 } 481 } 482 // remove child.pid and parent.pid 483 os.Remove(filepath.Join(sdir, "child.pid")) 484 os.Remove(filepath.Join(sdir, "parent.pid")) 485 486 // purgeServices 487 err = ctl.PurgeServices(filepath.Join(sdir, "immortal.sock")) 488 if err != nil { 489 t.Fatal(err) 490 } 491 } 492 493 func TestDaemonNewEnvHOME(t *testing.T) { 494 cfg := &Config{} 495 home := os.Getenv("HOME") 496 defer func() { os.Setenv("HOME", home) }() 497 os.Setenv("HOME", "") 498 expect(t, true, home != os.Getenv("HOME")) 499 d, err := New(cfg) 500 if err != nil { 501 t.Fatal(err) 502 } 503 expect(t, true, strings.HasPrefix(d.supDir, home)) 504 ctl := &Controller{} 505 err = ctl.PurgeServices(filepath.Join(d.supDir, "immortal.sock")) 506 if err != nil { 507 t.Fatal(err) 508 } 509 } 510 511 func TestDaemonConfigFile(t *testing.T) { 512 sdir, err := ioutil.TempDir("", "TestDaemonConfigFile") 513 if err != nil { 514 t.Error(err) 515 } 516 defer os.RemoveAll(sdir) 517 b := make([]byte, 3) 518 _, err = rand.Read(b) 519 if err != nil { 520 fmt.Println("error:", err) 521 return 522 } 523 expectedName := base64.URLEncoding.EncodeToString(b) 524 cfg := &Config{ 525 configFile: filepath.Join(sdir, fmt.Sprintf("%s.yml", expectedName)), 526 } 527 d, err := New(cfg) 528 if err != nil { 529 t.Fatal(err) 530 } 531 expect(t, true, strings.HasSuffix(d.supDir, expectedName)) 532 } 533 534 func TestDaemonFailSdir(t *testing.T) { 535 cfg := &Config{} 536 home := os.Getenv("HOME") 537 defer func() { os.Setenv("HOME", home) }() 538 os.Setenv("HOME", "/dev/null") 539 _, err := New(cfg) 540 if err == nil { 541 t.Fatal(err) 542 } 543 }