Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1574 lines
46 KiB
Go

package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/gofrs/flock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/kit/daemon"
)
// requirePOSIXSignals skips the test on platforms without POSIX signal
// semantics, where graceful SIGTERM termination and zombie-based liveness
// checks do not apply.
func requirePOSIXSignals(t *testing.T, reason string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip(reason)
}
}
func setStartProbeTickForTest(t *testing.T, tick time.Duration) {
t.Helper()
require.Positive(t, tick)
old := time.Duration(atomic.SwapInt64(&startProbeTickNanos, int64(tick)))
t.Cleanup(func() {
atomic.StoreInt64(&startProbeTickNanos, int64(old))
})
}
// startSleepProcess starts a long-lived child process and reaps it during
// cleanup. The returned PID is alive for the duration of the test, and once
// signalled the child becomes a zombie that daemon.ProcessAlive still reports
// as alive until cleanup reaps it.
func startSleepProcess(t *testing.T) int {
t.Helper()
return startProcessKilledOnCleanup(t, exec.Command("sleep", "60"))
}
// startTERMIgnoringProcess starts a child that ignores SIGTERM, so it survives
// a graceful stop and drives the force-kill escalation path.
func startTERMIgnoringProcess(t *testing.T) int {
t.Helper()
cmd := exec.Command("sh", "-c", "trap '' TERM; echo ready; exec sleep 60")
stdout, err := cmd.StdoutPipe()
require.NoError(t, err)
require.NoError(t, cmd.Start())
ready, err := bufio.NewReader(stdout).ReadString('\n')
require.NoError(t, err)
require.Equal(t, "ready", strings.TrimSpace(ready))
t.Cleanup(func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
return cmd.Process.Pid
}
// startReapedTERMIgnoringProcess starts a child that has installed its SIGTERM
// disposition before returning. The child is reaped concurrently so a test can
// distinguish a running process from one that stopDaemonProcess force-killed.
func startReapedTERMIgnoringProcess(t *testing.T) (int, <-chan struct{}) {
t.Helper()
cmd := exec.Command("sh", "-c", "trap '' TERM; echo ready; exec sleep 60")
stdout, err := cmd.StdoutPipe()
require.NoError(t, err)
require.NoError(t, cmd.Start())
ready, err := bufio.NewReader(stdout).ReadString('\n')
require.NoError(t, err)
require.Equal(t, "ready", strings.TrimSpace(ready))
reaped := make(chan struct{})
go func() {
_ = cmd.Wait()
close(reaped)
}()
t.Cleanup(func() {
_ = cmd.Process.Kill()
<-reaped
})
return cmd.Process.Pid, reaped
}
func startProcessKilledOnCleanup(t *testing.T, cmd *exec.Cmd) int {
t.Helper()
require.NoError(t, cmd.Start())
t.Cleanup(func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
})
return cmd.Process.Pid
}
// startReapedSleepProcess starts a long-lived child and reaps it concurrently,
// so once the process is signalled it leaves the zombie state and
// daemon.ProcessAlive reports it as dead. The returned channel closes when the
// child has been reaped.
func startReapedSleepProcess(t *testing.T) (int, <-chan struct{}) {
t.Helper()
cmd := exec.Command("sleep", "60")
require.NoError(t, cmd.Start())
reaped := make(chan struct{})
go func() {
_ = cmd.Wait()
close(reaped)
}()
t.Cleanup(func() { _ = cmd.Process.Kill() })
return cmd.Process.Pid, reaped
}
// deadPID returns a PID that daemon.ProcessAlive reports as dead. Reaping a
// just-started process is not portable here: on Windows, OpenProcess can still
// succeed briefly for a terminated process object.
func deadPID(t *testing.T) int {
t.Helper()
for _, pid := range []int{99999999, 999999999, 1 << 30} {
if !daemon.ProcessAlive(pid) {
return pid
}
}
t.Skip("could not find an unused PID for stale runtime test")
return 0
}
// onlyLiveRuntimeRecord asserts exactly one live runtime record exists in dir
// and returns it.
func onlyLiveRuntimeRecord(t *testing.T, dir string) daemon.RuntimeRecord {
t.Helper()
records := liveDaemonRecords(dir)
require.Len(t, records, 1)
return records[0]
}
type testDaemonEndpoint struct {
Host string
Port int
Addr string
}
func writeRuntimeRecordForTest(
dataDir string, rec daemon.RuntimeRecord,
) (string, error) {
if rec.Service == "" {
rec.Service = daemonService
}
if rec.Metadata == nil {
rec.Metadata = map[string]string{}
}
return runtimeStore(dataDir).Write(rec)
}
// writeRuntimeRecordFixture writes rec to dir, failing the test on error and
// registering cleanup that removes the runtime record.
func writeRuntimeRecordFixture(
t *testing.T, dir string, rec daemon.RuntimeRecord,
) string {
t.Helper()
path, err := writeRuntimeRecordForTest(dir, rec)
require.NoError(t, err)
t.Cleanup(func() { RemoveDaemonRuntime(dir) })
return path
}
// runtimeRecordOption mutates a daemon.RuntimeRecord built by
// daemonRuntimeRecord.
type runtimeRecordOption func(*daemon.RuntimeRecord)
func withRuntimeVersion(v string) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) { rec.Version = v }
}
func withRuntimeStartedAt(startedAt time.Time) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) { rec.StartedAt = startedAt }
}
func withRuntimePID(pid int) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) { rec.PID = pid }
}
func withRuntimeAPIVersion(v int) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) {
rec.Metadata[runtimeAPIVersion] = strconv.Itoa(v)
}
}
func withRuntimeNoSync(noSync bool) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) {
rec.Metadata[runtimeNoSync] = strconv.FormatBool(noSync)
}
}
func withRuntimeReadOnly(readOnly bool) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) {
rec.Metadata[runtimeReadOnly] = strconv.FormatBool(readOnly)
}
}
func withRuntimeRequireAuth(requireAuth bool) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) {
rec.Metadata[runtimeRequireAuth] = strconv.FormatBool(requireAuth)
}
}
func withRuntimeMetadata(key, value string) runtimeRecordOption {
return func(rec *daemon.RuntimeRecord) {
rec.Metadata[key] = value
}
}
// daemonRuntimeRecord builds a runtime record for the given address with the
// metadata fields the daemon writes. It defaults to a live, current-API,
// writable record; options override individual fields.
func daemonRuntimeRecord(
host string, port int, opts ...runtimeRecordOption,
) daemon.RuntimeRecord {
rec := daemon.RuntimeRecord{
PID: os.Getpid(),
Network: daemon.NetworkTCP,
Address: net.JoinHostPort(host, strconv.Itoa(port)),
Service: daemonService,
Version: "test",
StartedAt: time.Now(),
Metadata: map[string]string{
runtimeHost: host,
runtimePort: strconv.Itoa(port),
runtimeReadOnly: "false",
runtimeAPIVersion: strconv.Itoa(daemonAPIVersion),
runtimeDataVersion: strconv.Itoa(db.CurrentDataVersion()),
},
}
for _, opt := range opts {
opt(&rec)
}
return rec
}
func runtimeRecordForEndpoint(
endpoint testDaemonEndpoint, opts ...runtimeRecordOption,
) daemon.RuntimeRecord {
return daemonRuntimeRecord(endpoint.Host, endpoint.Port, opts...)
}
func runtimePathForTest(dataDir string, pid int) string {
path, err := runtimeStore(dataDir).Path(pid)
if err != nil {
return filepath.Join(dataDir, fmt.Sprintf("daemon.%d.json", pid))
}
return path
}
func runtimeTestDir(t *testing.T) string {
t.Helper()
dir := filepath.Join(t.TempDir(), "runtime")
_, err := runtimeStore(dir).LockPath()
require.NoError(t, err)
return dir
}
// TestSameProcessStartMarkerNeverReportsExternal hammers the race between
// markDaemonStarting's flock-acquire-then-register sequence and the
// isExternalDaemonStarting probe: a start marker owned by this process must
// never be classified as an external daemon startup, even mid-acquisition.
// Misclassification makes waitForExternalServeStartup return before the
// same-process owner publishes its runtime record, which surfaced as a flaky
// "serve --background is already in progress" error in
// TestEnsureBackgroundServeConcurrentLaunchConvergesOnDaemon.
func TestSameProcessStartMarkerNeverReportsExternal(t *testing.T) {
dir := runtimeTestDir(t)
require.NoError(t, os.MkdirAll(dir, 0o700))
stop := make(chan struct{})
var wg sync.WaitGroup
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
MarkDaemonStarting(dir)
UnmarkDaemonStarting(dir)
}
})
t.Cleanup(func() {
close(stop)
wg.Wait()
UnmarkDaemonStarting(dir)
})
deadline := time.Now().Add(300 * time.Millisecond)
for time.Now().Before(deadline) {
require.False(t, isExternalDaemonStarting(dir),
"same-process start marker reported as external")
}
}
func holdExternalDaemonStartLock(t *testing.T, dataDir string) func() {
t.Helper()
stdin := startExternalDaemonStartLockHelper(t, dataDir)
var once sync.Once
unlock := func() {
once.Do(func() { _ = stdin.Close() })
}
t.Cleanup(unlock)
return unlock
}
func startExternalDaemonStartLockHelper(t *testing.T, dataDir string) io.Closer {
t.Helper()
cmd := exec.Command(
os.Args[0],
"-test.run=^TestHoldExternalDaemonStartLockHelperProcess$",
)
cmd.Env = append(
os.Environ(),
"AGENTSVIEW_HOLD_EXTERNAL_START_LOCK_HELPER=1",
"AGENTSVIEW_HOLD_EXTERNAL_START_LOCK_DATA_DIR="+dataDir,
)
stdin, err := cmd.StdinPipe()
require.NoError(t, err)
stdout, err := cmd.StdoutPipe()
require.NoError(t, err)
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Start())
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
t.Cleanup(func() {
_ = stdin.Close()
select {
case <-done:
case <-time.After(5 * time.Second):
_ = cmd.Process.Kill()
<-done
}
})
line, err := bufio.NewReader(stdout).ReadString('\n')
if err != nil {
_ = cmd.Process.Kill()
require.NoError(t, err)
}
if strings.TrimSpace(line) != "ready" {
_ = cmd.Process.Kill()
require.Equal(t, "ready", strings.TrimSpace(line))
}
require.Eventually(t, func() bool {
return isExternalDaemonStarting(dataDir)
}, 2*time.Second, 10*time.Millisecond,
"external daemon start lock should be visible to parent")
return stdin
}
func holdExternalBackgroundLaunchLock(t *testing.T, dataDir string) func() {
t.Helper()
stdin := startExternalBackgroundLaunchLockHelper(t, dataDir)
var once sync.Once
unlock := func() {
once.Do(func() { _ = stdin.Close() })
}
t.Cleanup(unlock)
return unlock
}
func startExternalBackgroundLaunchLockHelper(
t *testing.T,
dataDir string,
) io.Closer {
t.Helper()
cmd := exec.Command(
os.Args[0],
"-test.run=^TestHoldExternalBackgroundLaunchLockHelperProcess$",
)
cmd.Env = append(
os.Environ(),
"AGENTSVIEW_HOLD_EXTERNAL_BACKGROUND_LAUNCH_LOCK_HELPER=1",
"AGENTSVIEW_HOLD_EXTERNAL_BACKGROUND_LAUNCH_LOCK_DATA_DIR="+dataDir,
)
stdin, err := cmd.StdinPipe()
require.NoError(t, err)
stdout, err := cmd.StdoutPipe()
require.NoError(t, err)
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Start())
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
t.Cleanup(func() {
_ = stdin.Close()
select {
case <-done:
case <-time.After(5 * time.Second):
_ = cmd.Process.Kill()
<-done
}
})
line, err := bufio.NewReader(stdout).ReadString('\n')
if err != nil {
_ = cmd.Process.Kill()
require.NoError(t, err)
}
if strings.TrimSpace(line) != "ready" {
_ = cmd.Process.Kill()
require.Equal(t, "ready", strings.TrimSpace(line))
}
require.Eventually(t, func() bool {
return isBackgroundLaunchActive(dataDir)
}, 2*time.Second, 10*time.Millisecond,
"external background launch lock should be visible to parent")
return stdin
}
func TestHoldExternalDaemonStartLockHelperProcess(t *testing.T) {
if os.Getenv("AGENTSVIEW_HOLD_EXTERNAL_START_LOCK_HELPER") != "1" {
return
}
dataDir := os.Getenv("AGENTSVIEW_HOLD_EXTERNAL_START_LOCK_DATA_DIR")
require.NotEmpty(t, dataDir)
lockPath, err := runtimeStore(dataDir).LockPath()
require.NoError(t, err)
lock := flock.New(lockPath)
locked, err := lock.TryLock()
require.NoError(t, err)
require.True(t, locked)
fmt.Println("ready")
_, _ = io.Copy(io.Discard, os.Stdin)
require.NoError(t, lock.Unlock())
}
func TestHoldExternalBackgroundLaunchLockHelperProcess(t *testing.T) {
if os.Getenv(
"AGENTSVIEW_HOLD_EXTERNAL_BACKGROUND_LAUNCH_LOCK_HELPER",
) != "1" {
return
}
dataDir := os.Getenv(
"AGENTSVIEW_HOLD_EXTERNAL_BACKGROUND_LAUNCH_LOCK_DATA_DIR",
)
require.NotEmpty(t, dataDir)
require.NoError(t, os.MkdirAll(dataDir, 0o700))
lock := flock.New(backgroundLaunchLockPath(dataDir))
locked, err := lock.TryLock()
require.NoError(t, err)
require.True(t, locked)
fmt.Println("ready")
_, _ = io.Copy(io.Discard, os.Stdin)
require.NoError(t, lock.Unlock())
}
func serverEndpoint(t *testing.T, ts *httptest.Server) testDaemonEndpoint {
t.Helper()
u, err := url.Parse(ts.URL)
require.NoError(t, err)
host, portText, err := net.SplitHostPort(u.Host)
require.NoError(t, err)
port, err := strconv.Atoi(portText)
require.NoError(t, err)
return testDaemonEndpoint{
Host: host,
Port: port,
Addr: net.JoinHostPort(host, portText),
}
}
func newPingDaemon(t *testing.T) testDaemonEndpoint {
t.Helper()
ts := httptest.NewServer(daemon.NewPingHandler(daemon.PingHandlerOptions{
Service: daemonService,
Version: "test",
}))
t.Cleanup(ts.Close)
return serverEndpoint(t, ts)
}
func newPingDaemonWithProbeSignal(t *testing.T) (testDaemonEndpoint, <-chan struct{}) {
t.Helper()
pinged := make(chan struct{})
var pingedOnce sync.Once
ping := daemon.NewPingHandler(daemon.PingHandlerOptions{
Service: daemonService,
Version: "test",
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pingedOnce.Do(func() { close(pinged) })
ping.ServeHTTP(w, r)
}))
t.Cleanup(ts.Close)
return serverEndpoint(t, ts), pinged
}
func testPingServer(t *testing.T) (host string, port int) {
t.Helper()
endpoint := newPingDaemon(t)
return endpoint.Host, endpoint.Port
}
func writeStartupFallbackFixture(
t *testing.T, dir, host string, port, pid int, createTime string,
) {
t.Helper()
MarkDaemonStarting(dir)
t.Cleanup(func() { UnmarkDaemonStarting(dir) })
state := startupState{
PID: pid,
StartedAt: time.Now().Add(-time.Minute),
Phase: "starting HTTP server",
Host: host,
Port: port,
RuntimeError: "permission denied writing runtime record",
CreateTime: createTime,
APIVersion: daemonAPIVersion,
DataVersion: db.CurrentDataVersion(),
UpdatedAt: time.Now(),
}
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(startupStatePath(dir), data, 0o600))
}
func failRuntimeRecordListing(t *testing.T) {
t.Helper()
oldList := listDaemonRuntimeRecords
listDaemonRuntimeRecords = func(daemon.RuntimeStore) ([]daemon.RuntimeRecord, error) {
return nil, errors.New("store unavailable")
}
t.Cleanup(func() { listDaemonRuntimeRecords = oldList })
}
func TestFindDaemonRuntime_UsesStartupFallbackWhenRuntimeStoreInspectionFails(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeStartupFallbackFixture(
t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10),
)
failRuntimeRecordListing(t)
rt := FindDaemonRuntime(dir)
require.NotNil(t, rt)
assert.True(t, rt.RuntimeFallback)
assert.Equal(t, port, rt.Port)
}
func TestFindIncompatibleDaemonRuntime_UsesStartupFallbackWhenRuntimeStoreInspectionFails(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeStartupFallbackFixture(
t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10),
)
state := readStartupState(dir)
require.NotNil(t, state)
state.APIVersion = daemonAPIVersion + 1
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(startupStatePath(dir), data, 0o600))
failRuntimeRecordListing(t)
rt, compatErr := FindIncompatibleDaemonRuntime(dir)
require.NotNil(t, rt)
require.Error(t, compatErr)
assert.True(t, rt.RuntimeFallback)
assert.ErrorContains(t, compatErr, "API version")
}
func TestWritableDaemonFallbackResolver(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeStartupFallbackFixture(t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10))
rt := FindWritableDaemonRuntime(dir)
require.NotNil(t, rt)
assert.True(t, rt.RuntimeFallback)
assert.Equal(t, port, rt.Port)
state := readStartupState(dir)
require.NotNil(t, state)
state.CreateTime = "1"
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(startupStatePath(dir), data, 0o600))
assert.Nil(t, FindWritableDaemonRuntime(dir), "stale fallback must fail closed")
}
func TestFindWritableDaemonRuntime_StartupFallbackSurvivesStartLockProbeError(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
state := startupState{
PID: os.Getpid(),
StartedAt: time.Now().Add(-time.Minute),
Phase: "starting HTTP server",
Host: host,
Port: port,
RuntimeError: "permission denied writing runtime record",
CreateTime: strconv.FormatInt(createTime, 10),
APIVersion: daemonAPIVersion,
DataVersion: db.CurrentDataVersion(),
UpdatedAt: time.Now(),
}
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(startupStatePath(dir), data, 0o600))
oldTryLock := startLockTryLock
startLockTryLock = func(*flock.Flock) (bool, error) {
return false, errors.New("simulated lock probe failure")
}
t.Cleanup(func() { startLockTryLock = oldTryLock })
rt := FindWritableDaemonRuntime(dir)
require.NotNil(t, rt)
assert.True(t, rt.RuntimeFallback)
assert.True(t, IsLocalDaemonActive(dir))
}
func TestLocalWritableDaemonRecordsWithFallbackPreservesReadOnlyRecords(t *testing.T) {
dir := runtimeTestDir(t)
readOnlyHost, readOnlyPort := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeRuntimeRecordFixture(t, dir, daemon.RuntimeRecord{
PID: os.Getpid(),
Service: daemonService,
Version: "test",
Network: daemon.NetworkTCP,
Address: net.JoinHostPort(readOnlyHost, strconv.Itoa(readOnlyPort)),
StartedAt: time.Now(),
Metadata: map[string]string{
runtimeHost: readOnlyHost,
runtimePort: strconv.Itoa(readOnlyPort),
runtimeReadOnly: "true",
runtimeAPIVersion: strconv.Itoa(daemonAPIVersion),
runtimeDataVersion: strconv.Itoa(db.CurrentDataVersion()),
runtimeCreateTime: strconv.FormatInt(createTime, 10),
},
})
host, port := testPingServer(t)
writeStartupFallbackFixture(t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10))
records, fallback := localWritableDaemonRecordsWithFallback(dir, "")
require.True(t, fallback)
require.Len(t, records, 2)
readOnlySeen := false
writableSeen := false
for _, rec := range records {
rt := daemonRuntimeFromRecord(rec)
if rt.ReadOnly {
readOnlySeen = true
assert.Equal(t, readOnlyPort, rt.Port)
continue
}
writableSeen = true
assert.Equal(t, port, rt.Port)
}
assert.True(t, readOnlySeen)
assert.True(t, writableSeen)
}
func TestFindDaemonRuntime_IgnoresIncompatibleStartupStateFallback(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeStartupFallbackFixture(
t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10),
)
state := readStartupState(dir)
require.NotNil(t, state)
state.APIVersion = daemonAPIVersion + 1
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(startupStatePath(dir), data, 0o600))
assert.Nil(t, FindDaemonRuntime(dir))
rt, compatErr := FindIncompatibleDaemonRuntime(dir)
require.NotNil(t, rt)
require.Error(t, compatErr)
assert.Contains(t, compatErr.Error(), "API version")
assert.True(t, rt.RuntimeFallback)
}
func TestWriteDaemonRuntimeFailurePreservesUpdateLaunchArgs(t *testing.T) {
dir := runtimeTestDir(t)
MarkDaemonStarting(dir)
t.Cleanup(func() { UnmarkDaemonStarting(dir) })
newStartupStateWriter(dir, time.Now).SetPhase("starting HTTP server")
host, port := testPingServer(t)
runtimePath, err := runtimeStore(dir).Path(os.Getpid())
require.NoError(t, err)
require.NoError(t, os.Mkdir(runtimePath, 0o700))
_, err = WriteDaemonRuntimeWithAuthAndNoSync(
dir, host, port, "test", false, true, true,
)
require.Error(t, err)
rt := FindWritableDaemonRuntime(dir)
require.NotNil(t, rt)
assert.Equal(t, "test", rt.Record.Version)
state := readStartupState(dir)
require.NotNil(t, state)
assert.True(t, state.RequireAuthKnown)
assert.True(t, state.RequireAuth)
assert.True(t, state.NoSyncKnown)
assert.True(t, state.NoSync)
oldStop := stopDaemonRuntimeForUpgrade
stopDaemonRuntimeForUpgrade = func(_ config.Config, _ *DaemonRuntime) error { return nil }
t.Cleanup(func() { stopDaemonRuntimeForUpgrade = oldStop })
result, err := stopWritableDaemonsForUpdate(config.Config{DataDir: dir})
require.NoError(t, err)
assert.True(t, result.Stopped)
args := restartDaemonAfterUpdateArgs(config.Config{}, result)
assert.Contains(t, args, "--require-auth")
assert.Contains(t, args, "--no-sync")
}
func TestWriteDaemonRuntimeFailurePreservesManagedCaddyIdentity(t *testing.T) {
dir := runtimeTestDir(t)
MarkDaemonStarting(dir)
t.Cleanup(func() { UnmarkDaemonStarting(dir) })
newStartupStateWriter(dir, time.Now).SetPhase("starting HTTP server")
host, port := testPingServer(t)
runtimePath, err := runtimeStore(dir).Path(os.Getpid())
require.NoError(t, err)
require.NoError(t, os.Mkdir(runtimePath, 0o700))
caddyCreateTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
_, err = WriteDaemonRuntimeWithAuthAndNoSync(
dir, host, port, "test", false, false, false, os.Getpid(),
)
require.Error(t, err)
state := readStartupState(dir)
require.NotNil(t, state)
assert.Equal(t, os.Getpid(), state.CaddyPID)
assert.Equal(t, strconv.FormatInt(caddyCreateTime, 10), state.CaddyCreateTime)
rt := FindWritableDaemonRuntime(dir)
require.NotNil(t, rt)
assert.Equal(t, strconv.Itoa(os.Getpid()), rt.Record.Metadata[runtimeCaddyPID])
assert.Equal(t, strconv.FormatInt(caddyCreateTime, 10), rt.Record.Metadata[runtimeCaddyCreateTime])
}
func TestStopWritableDaemonsForUpdateIgnoresStaleWritableRecordBeforeFallback(t *testing.T) {
dir := runtimeTestDir(t)
staleHost, stalePort := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeRuntimeRecordFixture(t, dir, daemon.RuntimeRecord{
PID: os.Getpid(),
Service: daemonService,
Version: "stale",
Network: daemon.NetworkTCP,
Address: net.JoinHostPort(staleHost, strconv.Itoa(stalePort)),
StartedAt: time.Now(),
Metadata: map[string]string{
runtimeHost: staleHost,
runtimePort: strconv.Itoa(stalePort),
runtimeAPIVersion: strconv.Itoa(daemonAPIVersion),
runtimeDataVersion: strconv.Itoa(db.CurrentDataVersion()),
runtimeCreateTime: "1",
},
})
host, port := testPingServer(t)
writeStartupFallbackFixture(
t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10),
)
oldStop := stopDaemonRuntimeForUpgrade
var stopped *DaemonRuntime
stopDaemonRuntimeForUpgrade = func(_ config.Config, rt *DaemonRuntime) error {
stopped = rt
return nil
}
t.Cleanup(func() { stopDaemonRuntimeForUpgrade = oldStop })
result, err := stopWritableDaemonsForUpdate(config.Config{DataDir: dir})
require.NoError(t, err)
require.NotNil(t, stopped)
assert.True(t, result.Stopped)
assert.Equal(t, port, stopped.Port)
assert.Equal(t, strconv.FormatInt(createTime, 10), stopped.Record.Metadata[runtimeCreateTime])
}
func newAuthenticatedPingDaemon(t *testing.T, token string) testDaemonEndpoint {
t.Helper()
ping := daemon.NewPingHandler(daemon.PingHandlerOptions{
Service: daemonService,
Version: "test",
})
ts := httptest.NewServer(http.HandlerFunc(func(
w http.ResponseWriter, r *http.Request,
) {
if r.Header.Get("Authorization") != "Bearer "+token {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ping.ServeHTTP(w, r)
}))
t.Cleanup(ts.Close)
return serverEndpoint(t, ts)
}
func testAuthenticatedPingServer(
t *testing.T, token string,
) (host string, port int) {
t.Helper()
endpoint := newAuthenticatedPingDaemon(t, token)
return endpoint.Host, endpoint.Port
}
func writeLiveRuntime(
t *testing.T,
dir string,
readOnly bool,
opts ...runtimeRecordOption,
) (testDaemonEndpoint, string) {
t.Helper()
endpoint := newPingDaemon(t)
allOpts := append([]runtimeRecordOption{withRuntimeReadOnly(readOnly)}, opts...)
path := writeRuntimeRecordFixture(
t, dir, runtimeRecordForEndpoint(endpoint, allOpts...),
)
return endpoint, path
}
func readRuntimeRecord(t *testing.T, path string) daemon.RuntimeRecord {
t.Helper()
data, err := os.ReadFile(path)
require.NoError(t, err)
var rec daemon.RuntimeRecord
require.NoError(t, json.Unmarshal(data, &rec))
return rec
}
func assertPathRemoved(t *testing.T, path string, msgAndArgs ...any) {
t.Helper()
_, err := os.Stat(path)
assert.True(t, os.IsNotExist(err), msgAndArgs...)
}
func assertRuntimeRecordRemoved(
t *testing.T,
dataDir string,
pid int,
msgAndArgs ...any,
) {
t.Helper()
if len(msgAndArgs) == 0 {
msgAndArgs = []any{"runtime record should be removed"}
}
assertPathRemoved(t, runtimePathForTest(dataDir, pid), msgAndArgs...)
}
func requireFoundRuntime(
t *testing.T, dataDir string, authTokens ...string,
) *DaemonRuntime {
t.Helper()
rt := FindDaemonRuntime(dataDir, authTokens...)
require.NotNil(t, rt, "expected running server")
return rt
}
func requireMigratedIncompatibleRuntime(
t *testing.T, dataDir string,
) *DaemonRuntime {
t.Helper()
rt, err := FindIncompatibleDaemonRuntime(dataDir)
require.NotNil(t, rt, "expected incompatible runtime")
require.Error(t, err)
return rt
}
func writeLegacyRuntimeStateForTest(
t *testing.T,
dataDir string,
state legacyStateFile,
) string {
t.Helper()
if state.Port <= 0 {
state.Port = 59999
}
path := filepath.Join(dataDir, fmt.Sprintf("server.%d.json", state.Port))
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0o644))
return path
}
func writeProbeableLegacyRuntime(
t *testing.T,
dataDir string,
state legacyStateFile,
) (testDaemonEndpoint, string) {
t.Helper()
endpoint := newPingDaemon(t)
if state.PID == 0 {
state.PID = os.Getpid()
}
if state.Host == "" {
state.Host = endpoint.Host
}
if state.Port == 0 {
state.Port = endpoint.Port
}
return endpoint, writeLegacyRuntimeStateForTest(t, dataDir, state)
}
func writeAuthenticatedProbeableLegacyRuntime(
t *testing.T,
dataDir string,
token string,
state legacyStateFile,
) (testDaemonEndpoint, string) {
t.Helper()
endpoint := newAuthenticatedPingDaemon(t, token)
if state.PID == 0 {
state.PID = os.Getpid()
}
if state.Host == "" {
state.Host = endpoint.Host
}
if state.Port == 0 {
state.Port = endpoint.Port
}
return endpoint, writeLegacyRuntimeStateForTest(t, dataDir, state)
}
func rewriteLegacyState(
t *testing.T,
path string,
mutate func(*legacyStateFile),
) {
t.Helper()
data, err := os.ReadFile(path)
require.NoError(t, err)
var state legacyStateFile
require.NoError(t, json.Unmarshal(data, &state))
mutate(&state)
data, err = json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0o644))
}
func TestWriteAndRemoveDaemonRuntime(t *testing.T) {
dir := runtimeTestDir(t)
endpoint := newPingDaemon(t)
path, err := WriteDaemonRuntimeWithAuthAndNoSync(
dir, endpoint.Host, endpoint.Port, "1.0.0", false, true, true,
)
require.NoError(t, err)
assert.Equal(t, runtimePathForTest(dir, os.Getpid()), path)
rec := readRuntimeRecord(t, path)
assert.Equal(t, daemonService, rec.Service)
assert.Equal(t, "1.0.0", rec.Version)
assert.Equal(t, os.Getpid(), rec.PID)
assert.Equal(t, daemon.NetworkTCP, rec.Network)
assert.Equal(t, endpoint.Addr, rec.Address)
assert.Equal(t, "true", rec.Metadata[runtimeRequireAuth])
assert.Equal(t, "true", rec.Metadata[runtimeNoSync])
assert.Equal(t, strconv.Itoa(daemonAPIVersion), rec.Metadata[runtimeAPIVersion])
assert.Equal(t, strconv.Itoa(db.CurrentDataVersion()), rec.Metadata[runtimeDataVersion])
rt := daemonRuntimeFromRecord(rec)
assert.True(t, rt.RequireAuth)
assert.True(t, rt.RequireAuthKnown)
assert.True(t, rt.NoSync)
RemoveDaemonRuntime(dir)
assertPathRemoved(t, path, "runtime record not removed")
}
func TestFindDaemonRuntime_NoFiles(t *testing.T) {
dir := runtimeTestDir(t)
assert.Nil(t, FindDaemonRuntime(dir))
}
func TestFindDaemonRuntime_StaleFile(t *testing.T) {
dir := runtimeTestDir(t)
_, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 9999,
withRuntimePID(deadPID(t)),
withRuntimeVersion("1.0.0"),
))
require.NoError(t, err)
assert.Nil(t, FindDaemonRuntime(dir), "expected nil for stale PID")
assert.False(t, IsDaemonActive(dir), "dead runtime record should not be active")
}
func TestFindDaemonRuntime_InvalidJSON(t *testing.T) {
dir := runtimeTestDir(t)
path := runtimePathForTest(dir, os.Getpid())
require.NoError(t, os.WriteFile(path, []byte("not json"), 0o644))
assert.Nil(t, FindDaemonRuntime(dir), "expected nil for invalid JSON")
}
func TestFindDaemonRuntime_IgnoresNonRuntimeFiles(t *testing.T) {
dir := runtimeTestDir(t)
require.NoError(t, os.WriteFile(
runtimePathForTest(dir, os.Getpid())+".tmp",
[]byte("{}"), 0o644,
))
require.NoError(t, os.WriteFile(
dir+"/config.json",
[]byte("{}"), 0o644,
))
assert.Nil(t, FindDaemonRuntime(dir))
}
func TestFindDaemonRuntime_LiveProcess(t *testing.T) {
dir := runtimeTestDir(t)
endpoint, _ := writeLiveRuntime(t, dir, false, withRuntimeVersion("1.0.0"))
result := requireFoundRuntime(t, dir)
assert.Equal(t, endpoint.Port, result.Port)
assert.Equal(t, os.Getpid(), result.Record.PID)
assert.False(t, result.ReadOnly)
}
func TestFindDaemonRuntime_IgnoresIncompatibleRuntime(t *testing.T) {
dir := runtimeTestDir(t)
writeLiveRuntime(t, dir, false,
withRuntimeVersion("old"),
withRuntimeAPIVersion(0),
)
assert.Nil(t, FindDaemonRuntime(dir))
rt, compatErr := FindIncompatibleDaemonRuntime(dir)
require.NotNil(t, rt)
require.Error(t, compatErr)
assert.Contains(t, compatErr.Error(), "API version")
assert.True(t, IsLocalDaemonActive(dir),
"incompatible writable daemon still owns the local archive")
}
func TestFindDaemonRuntime_ReadOnly(t *testing.T) {
dir := runtimeTestDir(t)
writeLiveRuntime(t, dir, true, withRuntimeVersion("1.0.0"))
result := requireFoundRuntime(t, dir)
assert.True(t, result.ReadOnly)
assert.False(t, IsLocalDaemonActive(dir))
assert.True(t, IsDaemonActive(dir))
}
func TestFindDaemonRuntime_BindAllMetadata(t *testing.T) {
dir := runtimeTestDir(t)
endpoint := newPingDaemon(t)
_, err := WriteDaemonRuntime(dir, "0.0.0.0", endpoint.Port, "1.0.0", false)
require.NoError(t, err)
result := requireFoundRuntime(t, dir)
assert.Equal(t, "0.0.0.0", result.Host)
assert.Equal(t, endpoint.Port, result.Port)
}
func TestFindDaemonRuntime_UsesAuthToken(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testAuthenticatedPingServer(t, "secret")
_, err := WriteDaemonRuntime(dir, host, port, "1.0.0", false)
require.NoError(t, err)
assert.Nil(t, FindDaemonRuntime(dir),
"expected no discovery without bearer token")
result := FindDaemonRuntime(dir, "secret")
require.NotNil(t, result, "expected discovery with bearer token")
assert.Equal(t, port, result.Port)
assert.False(t, result.ReadOnly)
}
func TestIsDaemonActive_LivePIDNoPingClaimsOwnership(t *testing.T) {
dir := runtimeTestDir(t)
writeRuntimeRecordFixture(t, dir, daemonRuntimeRecord(
"127.0.0.1", 59999,
withRuntimeVersion("1.0.0"),
))
assert.Nil(t, FindDaemonRuntime(dir), "expected no discoverable daemon")
assert.True(t, IsDaemonActive(dir),
"live runtime record should still suppress writes")
assert.True(t, IsLocalDaemonActive(dir),
"live writable runtime record should claim the SQLite archive")
}
func TestIsDaemonActive_DeadPIDDaemonRuntime(t *testing.T) {
dir := runtimeTestDir(t)
pid := deadPID(t)
path, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 59994,
withRuntimePID(pid),
))
require.NoError(t, err)
assert.False(t, IsDaemonActive(dir), "expected false for dead PID runtime record")
assertPathRemoved(t, path, "dead runtime record not cleaned up")
}
func TestIsDaemonActive_StartLock(t *testing.T) {
dir := runtimeTestDir(t)
require.False(t, IsDaemonActive(dir), "expected false with no files")
MarkDaemonStarting(dir)
require.True(t, IsDaemonActive(dir), "expected true with start lock")
UnmarkDaemonStarting(dir)
require.False(t, IsDaemonActive(dir), "expected false after start lock released")
}
func TestFindDaemonRuntime_MigratesLegacyWritableStateFile(t *testing.T) {
dir := runtimeTestDir(t)
startedAt := time.Now().UTC().Add(-time.Minute)
endpoint, legacyPath := writeProbeableLegacyRuntime(t, dir, legacyStateFile{
Version: "legacy",
StartedAt: startedAt.Format(time.RFC3339Nano),
})
result := FindDaemonRuntime(dir)
require.Nil(t, result, "legacy state without compatibility metadata is incompatible")
incompatible := requireMigratedIncompatibleRuntime(t, dir)
assert.Equal(t, os.Getpid(), incompatible.Record.PID)
assert.Equal(t, endpoint.Host, incompatible.Host)
assert.Equal(t, endpoint.Port, incompatible.Port)
assert.Equal(t, "legacy", incompatible.Record.Version)
assert.False(t, incompatible.ReadOnly)
assert.True(t, IsDaemonActive(dir))
assert.True(t, IsLocalDaemonActive(dir))
assertPathRemoved(t, legacyPath,
"migrated legacy state file should be removed")
runtimePath := runtimePathForTest(dir, os.Getpid())
assert.FileExists(t, runtimePath, "kit runtime record should be written")
}
func TestFindDaemonRuntime_MigratesLegacyReadOnlyStateFile(t *testing.T) {
dir := runtimeTestDir(t)
_, legacyPath := writeProbeableLegacyRuntime(t, dir, legacyStateFile{
ReadOnly: true,
})
result := FindDaemonRuntime(dir)
require.Nil(t, result, "read-only legacy state without compatibility metadata is incompatible")
incompatible := requireMigratedIncompatibleRuntime(t, dir)
assert.True(t, incompatible.ReadOnly)
assert.False(t, IsLocalDaemonActive(dir))
assert.True(t, IsDaemonActive(dir))
assertPathRemoved(t, legacyPath,
"migrated legacy state file should be removed")
}
func TestFindDaemonRuntime_LegacyStateFileUsesPortFromName(t *testing.T) {
dir := runtimeTestDir(t)
endpoint, legacyPath := writeProbeableLegacyRuntime(t, dir, legacyStateFile{})
rewriteLegacyState(t, legacyPath, func(state *legacyStateFile) {
state.Port = 0
})
result, compatErr := FindIncompatibleDaemonRuntime(dir)
require.NotNil(t, result, "expected legacy port from file name to migrate")
require.Error(t, compatErr)
assert.Equal(t, endpoint.Port, result.Port)
}
func TestIsLocalDaemonActive_LegacyDeadPIDStateFileRemoved(t *testing.T) {
dir := runtimeTestDir(t)
pid := deadPID(t)
path := writeLegacyRuntimeStateForTest(t, dir, legacyStateFile{PID: pid})
assert.False(t, IsLocalDaemonActive(dir))
assertPathRemoved(t, path, "dead legacy state file not removed")
assertRuntimeRecordRemoved(t, dir, pid,
"dead legacy state should not become a kit runtime record")
}
func TestIsLocalDaemonActive_UnprobeableLegacyStateFileDoesNotSuppressWrites(
t *testing.T,
) {
dir := runtimeTestDir(t)
path := writeLegacyRuntimeStateForTest(t, dir, legacyStateFile{
PID: os.Getpid(),
Host: "127.0.0.1",
Port: 59999,
})
assert.Nil(t, FindDaemonRuntime(dir), "unprobeable legacy state must not migrate")
assert.False(t, IsDaemonActive(dir))
assert.False(t, IsLocalDaemonActive(dir))
assert.FileExists(t, path, "unprobeable live legacy state should be left intact")
assertRuntimeRecordRemoved(t, dir, os.Getpid(),
"unprobeable legacy state should not become a kit runtime record")
}
func TestIsDaemonStarting_LegacyStartupLock(t *testing.T) {
dir := runtimeTestDir(t)
require.NoError(t, os.WriteFile(
filepath.Join(dir, fmt.Sprintf("%s%d", legacyStartupLockPrefix, os.Getpid())),
[]byte(strconv.Itoa(os.Getpid())),
0o644,
))
assert.True(t, IsDaemonStarting(dir))
assert.True(t, IsLocalDaemonActive(dir))
}
func TestLiveWritableRuntimeWithMismatchedCreateTimeIsRemoved(t *testing.T) {
dir := runtimeTestDir(t)
liveCreateTime, ok := processCreateTimeMillis(os.Getpid())
if !ok {
t.Skip("process create time is unavailable on this platform")
}
_, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 9,
withRuntimeMetadata(
runtimeCreateTime, strconv.FormatInt(liveCreateTime+1, 10),
),
))
require.NoError(t, err)
assert.False(t, hasLiveWritableDaemonRuntime(dir))
assert.False(t, IsLocalDaemonActive(dir))
assertRuntimeRecordRemoved(t, dir, os.Getpid(),
"mismatched create-time runtime record should be removed")
}
func TestCompareProcessCreateTime(t *testing.T) {
tests := []struct {
name string
recorded string
live int64
liveOK bool
want processCreateTimeState
}{
{name: "exact match", recorded: "1234", live: 1234, liveOK: true, want: processCreateTimeMatch},
{name: "proven mismatch", recorded: "1234", live: 1235, liveOK: true, want: processCreateTimeMismatch},
{name: "missing recorded value", recorded: "", live: 1234, liveOK: true, want: processCreateTimeUnknown},
{name: "malformed recorded value", recorded: "not-a-time", live: 1234, liveOK: true, want: processCreateTimeUnknown},
{name: "zero recorded value", recorded: "0", live: 1234, liveOK: true, want: processCreateTimeUnknown},
{name: "negative recorded value", recorded: "-1", live: 1234, liveOK: true, want: processCreateTimeUnknown},
{name: "live lookup unavailable", recorded: "1234", live: 0, liveOK: false, want: processCreateTimeUnknown},
{name: "nonpositive live value", recorded: "1234", live: 0, liveOK: true, want: processCreateTimeUnknown},
{name: "negative live value", recorded: "1234", live: -1, liveOK: true, want: processCreateTimeUnknown},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, compareProcessCreateTime(
tt.recorded, tt.live, tt.liveOK,
))
})
}
}
func TestWritableDaemonRecordsFiltersAndCleansRuntimeRecords(t *testing.T) {
liveCreateTime, ok := processCreateTimeMillis(os.Getpid())
if !ok {
t.Skip("process create time is unavailable on this platform")
}
tests := []struct {
name string
options []runtimeRecordOption
wantRecords int
wantFile bool
}{
{
name: "matching writable record",
options: []runtimeRecordOption{withRuntimeMetadata(runtimeCreateTime, strconv.FormatInt(liveCreateTime, 10))},
wantRecords: 1,
wantFile: true,
},
{
name: "missing create time is preserved as unknown",
wantRecords: 1,
wantFile: true,
},
{
name: "malformed create time is preserved as unknown",
options: []runtimeRecordOption{withRuntimeMetadata(runtimeCreateTime, "not-a-time")},
wantRecords: 1,
wantFile: true,
},
{
name: "zero create time is preserved as unknown",
options: []runtimeRecordOption{withRuntimeMetadata(runtimeCreateTime, "0")},
wantRecords: 1,
wantFile: true,
},
{
name: "negative create time is preserved as unknown",
options: []runtimeRecordOption{withRuntimeMetadata(runtimeCreateTime, "-1")},
wantRecords: 1,
wantFile: true,
},
{
name: "proven mismatch is removed",
options: []runtimeRecordOption{withRuntimeMetadata(runtimeCreateTime, strconv.FormatInt(liveCreateTime+1, 10))},
wantRecords: 0,
wantFile: false,
},
{
name: "read only record is ignored but preserved",
options: []runtimeRecordOption{withRuntimeReadOnly(true)},
wantRecords: 0,
wantFile: true,
},
{
name: "another service is ignored but preserved",
options: []runtimeRecordOption{func(rec *daemon.RuntimeRecord) { rec.Service = "other" }},
wantRecords: 0,
wantFile: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := runtimeTestDir(t)
path, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 9, tt.options...,
))
require.NoError(t, err)
records, err := writableDaemonRecords(dir, "")
require.NoError(t, err)
require.Len(t, records, tt.wantRecords)
if tt.wantRecords > 0 {
assert.Equal(t, os.Getpid(), records[0].PID)
assert.Equal(t, path, records[0].SourcePath)
}
if tt.wantFile {
assert.FileExists(t, path)
} else {
assertPathRemoved(t, path)
}
})
}
}
func TestWritableDaemonRecordsCleansDeadRecord(t *testing.T) {
dir := runtimeTestDir(t)
pid := deadPID(t)
path, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 9, withRuntimePID(pid),
))
require.NoError(t, err)
records, err := writableDaemonRecords(dir, "")
require.NoError(t, err)
assert.Empty(t, records)
assertPathRemoved(t, path, "dead runtime record should be cleaned up")
}
func TestWritableDaemonRecordsReturnsEveryLiveWritableRecord(t *testing.T) {
requirePOSIXSignals(t, "requires a long-lived child process")
dir := runtimeTestDir(t)
pids := []int{os.Getpid(), startSleepProcess(t)}
wantPaths := make(map[int]string, len(pids))
for i, pid := range pids {
path, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 10+i, withRuntimePID(pid),
))
require.NoError(t, err)
wantPaths[pid] = path
}
records, err := writableDaemonRecords(dir, "")
require.NoError(t, err)
require.Len(t, records, len(pids))
for _, rec := range records {
assert.Equal(t, wantPaths[rec.PID], rec.SourcePath)
delete(wantPaths, rec.PID)
}
assert.Empty(t, wantPaths, "all live writable records should be returned")
}
func TestWritableDaemonRecordsMigratesLegacyRuntime(t *testing.T) {
dir := runtimeTestDir(t)
endpoint, legacyPath := writeProbeableLegacyRuntime(t, dir, legacyStateFile{})
records, err := writableDaemonRecords(dir, "")
require.NoError(t, err)
require.Len(t, records, 1)
assert.Equal(t, endpoint.Port, daemonRuntimeFromRecord(records[0]).Port)
assert.Equal(t, runtimePathForTest(dir, os.Getpid()), records[0].SourcePath)
assertPathRemoved(t, legacyPath, "migrated legacy record should be removed")
}
func TestWritableDaemonRecordsReportsMismatchedRecordRemovalFailure(
t *testing.T,
) {
requirePOSIXSignals(t, "directory removal permissions require POSIX semantics")
dir := runtimeTestDir(t)
liveCreateTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
path, err := writeRuntimeRecordForTest(dir, daemonRuntimeRecord(
"127.0.0.1", 9,
withRuntimeMetadata(
runtimeCreateTime, strconv.FormatInt(liveCreateTime+1, 10),
),
))
require.NoError(t, err)
stored, err := runtimeStore(dir).List()
require.NoError(t, err)
require.Len(t, stored, 1)
require.NoError(t, os.Chmod(dir, 0o500))
t.Cleanup(func() { _ = os.Chmod(dir, 0o700) })
records, err := writableDaemonRecordsFromStore(
staticRuntimeRecordStore{records: stored},
)
assert.Nil(t, records)
require.Error(t, err)
assert.ErrorContains(t, err, "remove mismatched daemon runtime record")
assert.ErrorContains(t, err, path)
assert.FileExists(t, path, "failed cleanup must leave the record recoverable")
}
type staticRuntimeRecordStore struct {
records []daemon.RuntimeRecord
}
func (staticRuntimeRecordStore) CleanupDead() (int, error) {
return 0, nil
}
func (s staticRuntimeRecordStore) List() ([]daemon.RuntimeRecord, error) {
return s.records, nil
}
type cleanupFailingRuntimeRecordStore struct {
err error
}
func (s cleanupFailingRuntimeRecordStore) CleanupDead() (int, error) {
return 0, s.err
}
func (cleanupFailingRuntimeRecordStore) List() ([]daemon.RuntimeRecord, error) {
return nil, nil
}
type listFailingRuntimeRecordStore struct {
err error
}
func (listFailingRuntimeRecordStore) CleanupDead() (int, error) {
return 0, nil
}
func (s listFailingRuntimeRecordStore) List() ([]daemon.RuntimeRecord, error) {
return nil, s.err
}
func TestWritableDaemonRecordsSurfacesRuntimeStoreErrors(t *testing.T) {
tests := []struct {
name string
store daemonRuntimeRecordStore
want string
}{
{
name: "cleanup failure",
store: cleanupFailingRuntimeRecordStore{err: errors.New("cleanup failed")},
want: "cleanup failed",
},
{
name: "list failure",
store: listFailingRuntimeRecordStore{err: errors.New("list failed")},
want: "list failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
records, err := writableDaemonRecordsFromStore(tt.store)
assert.Nil(t, records)
require.Error(t, err)
assert.ErrorContains(t, err, tt.want)
})
}
}
func TestStartLock_OwnProcess(t *testing.T) {
dir := runtimeTestDir(t)
require.False(t, isDaemonStarting(dir), "expected false before lock written")
MarkDaemonStarting(dir)
require.True(t, isDaemonStarting(dir), "expected true after lock written")
UnmarkDaemonStarting(dir)
require.False(t, isDaemonStarting(dir), "expected false after start lock released")
}
func TestWaitForDaemonStartup_AlreadyRunning(t *testing.T) {
dir := runtimeTestDir(t)
writeLiveRuntime(t, dir, false, withRuntimeVersion("1.0.0"))
assert.True(t, WaitForDaemonStartup(dir, 100*time.Millisecond),
"expected true, server is running")
}
func TestWaitForDaemonStartup_LockClearsNoServer(t *testing.T) {
dir := runtimeTestDir(t)
assert.False(t, WaitForDaemonStartup(dir, 100*time.Millisecond),
"expected false, no start lock and no server")
}
func TestProbeHostForDial(t *testing.T) {
tests := []struct {
host string
want string
}{
{"", "127.0.0.1"},
{"0.0.0.0", "127.0.0.1"},
{"::", "::1"},
{"127.0.0.1", "127.0.0.1"},
{"192.168.1.100", "192.168.1.100"},
}
for _, tt := range tests {
assert.Equal(t, tt.want, probeHostForDial(tt.host),
"probeHostForDial(%q)", tt.host)
}
}
func TestDaemonRuntime_ReadOnlyPersisted(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
path, err := WriteDaemonRuntime(dir, host, port, "test", true)
require.NoError(t, err)
rec := readRuntimeRecord(t, path)
assert.Equal(t, "true", rec.Metadata[runtimeReadOnly])
assert.Equal(t, strconv.Itoa(port), rec.Metadata[runtimePort])
assert.Equal(t, "test", rec.Version)
}