fix(cli): preserve live daemon state on probe failures

This commit is contained in:
Dhruv Sharma 2026-05-31 21:52:08 +05:30 committed by harshitsinghbhandari
parent 925e70763d
commit a614462d38
3 changed files with 98 additions and 4 deletions

View File

@ -153,6 +153,81 @@ func TestStopDoesNotSignalUnverifiedReusedPID(t *testing.T) {
}
}
func TestStatusKeepsLiveProbeFailureUnhealthy(t *testing.T) {
cfg := setConfigEnv(t)
if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: closedPort(t), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
t.Fatal(err)
}
out, _, err := executeCLI(t, Deps{
ProcessAlive: func(pid int) bool { return pid == 4242 },
}, "status", "--json")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `"state": "unhealthy"`) {
t.Fatalf("status should keep live probe failures unhealthy:\n%s", out)
}
info, err := runfile.Read(cfg.runFile)
if err != nil {
t.Fatal(err)
}
if info == nil {
t.Fatal("live probe failure should not remove run-file")
}
}
func TestStopRefusesUnverifiedLivePID(t *testing.T) {
cfg := setConfigEnv(t)
if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: closedPort(t), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
t.Fatal(err)
}
var signaled bool
_, _, err := executeCLI(t, Deps{
ProcessAlive: func(pid int) bool { return pid == 4242 },
SignalTerm: func(pid int) error {
signaled = true
return nil
},
}, "stop", "--json")
if err == nil {
t.Fatal("stop should fail when daemon ownership cannot be verified")
}
if signaled {
t.Fatal("stop signaled a PID whose ownership was unknown")
}
info, err := runfile.Read(cfg.runFile)
if err != nil {
t.Fatal(err)
}
if info == nil {
t.Fatal("unverified live PID should remain tracked")
}
}
func TestStartDoesNotSpawnWhenLiveProbeFails(t *testing.T) {
cfg := setConfigEnv(t)
if err := runfile.Write(cfg.runFile, runfile.Info{PID: 4242, Port: closedPort(t), StartedAt: time.Unix(100, 0).UTC()}); err != nil {
t.Fatal(err)
}
var started bool
_, _, err := executeCLI(t, Deps{
ProcessAlive: func(pid int) bool { return pid == 4242 },
StartProcess: func(processStartConfig) (processHandle, error) {
started = true
return processHandle{}, nil
},
}, "start", "--timeout", "1ns", "--json")
if err == nil {
t.Fatal("start should fail instead of spawning over a live unverified PID")
}
if started {
t.Fatal("start spawned while run-file PID was still alive")
}
}
type testConfig struct {
runFile string
dataDir string
@ -203,3 +278,22 @@ func serverPort(t *testing.T, raw string) int {
}
return port
}
func closedPort(t *testing.T) int {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
_, portRaw, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
t.Fatal(err)
}
port, err := strconv.Atoi(portRaw)
if err != nil {
t.Fatal(err)
}
return port
}

View File

@ -89,7 +89,7 @@ func (c *commandContext) inspectDaemon(ctx context.Context) (daemonStatus, error
health, err := c.readProbe(ctx, info.Port, "healthz")
if err != nil {
st.State = "stale"
st.State = "unhealthy"
st.Error = err.Error()
return st, nil
}

View File

@ -62,10 +62,10 @@ func (c *commandContext) stopDaemon(ctx context.Context, opts stopOptions) (daem
return daemonStatus{State: "stopped", RunFile: cfg.RunFilePath, DataDir: cfg.DataDir}, nil
}
if !st.owned {
if err := runfile.Remove(cfg.RunFilePath); err != nil {
return daemonStatus{}, err
if st.Error != "" {
return daemonStatus{}, fmt.Errorf("daemon pid %d is alive but ownership could not be verified: %s", st.PID, st.Error)
}
return daemonStatus{State: "stopped", RunFile: cfg.RunFilePath, DataDir: cfg.DataDir}, nil
return daemonStatus{}, fmt.Errorf("daemon pid %d is alive but ownership could not be verified", st.PID)
}
if err := c.deps.SignalTerm(st.PID); err != nil {