Files
wehub-resource-sync f99010fae1
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

784 lines
25 KiB
Go

// ABOUTME: Implements the canonical daemon lifecycle command.
// ABOUTME: Serializes writable-server start/stop transitions under one lock.
package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/gofrs/flock"
"github.com/spf13/cobra"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/kit/daemon"
)
type daemonLaunchLock interface {
Unlock() error
}
type daemonLaunchObservation struct {
LockHeld bool
Starting bool
Snapshot *startupState
Records []daemon.RuntimeRecord
UnconfirmedRecords []daemon.RuntimeRecord
Err error
}
type daemonLaunchWaitDeps struct {
acquireLaunchLock func(string) (*flock.Flock, bool, error)
loadReadOnlyConfig func() (config.Config, error)
writableRecords func(string, string) ([]daemon.RuntimeRecord, error)
confirmed func(daemon.RuntimeRecord, string) bool
probe func(daemon.RuntimeRecord, string) bool
isStarting func(string) bool
readStartupState func(string) *startupState
now func() time.Time
sleep func(time.Duration)
timeout time.Duration
tick time.Duration
onAttempt func()
}
func defaultDaemonLaunchWaitDeps() daemonLaunchWaitDeps {
return daemonLaunchWaitDeps{
acquireLaunchLock: acquireBackgroundLaunchLockWithError,
loadReadOnlyConfig: config.LoadReadOnly,
writableRecords: writableDaemonRecords,
confirmed: stopTargetConfirmed,
probe: daemonRecordPingConfirmed,
isStarting: IsDaemonStarting,
readStartupState: readStartupState,
now: time.Now,
sleep: time.Sleep,
timeout: backgroundServeReadyTimeout,
tick: startProbeTick(),
}
}
type daemonCommandDeps struct {
resolveDataDir func() (string, error)
mkdirAll func(string, os.FileMode) error
loadConfig func() (config.Config, error)
loadReadOnlyConfig func() (config.Config, error)
acquireLaunchLockWithError func(string) (daemonLaunchLock, bool, error)
waitContendedLaunch func(string) daemonLaunchObservation
writableRecords func(string, string) ([]daemon.RuntimeRecord, error)
statusRecords func(string, string) ([]daemon.RuntimeRecord, error)
isStarting func(string) bool
readStartupState func(string) *startupState
startBackground func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error)
stopTargetConfirmed func(daemon.RuntimeRecord, string) bool
stopProcess func(daemon.RuntimeRecord, time.Duration) error
stopCaddy func(io.Writer, daemon.RuntimeRecord) error
validateConfig func(config.Config) error
checkDataVersion func(string) error
probeRecord func(daemon.RuntimeRecord, string) (daemon.PingInfo, bool)
writableRuntime func(string, string) *DaemonRuntime
now func() time.Time
}
func defaultDaemonCommandDeps() daemonCommandDeps {
return daemonCommandDeps{
resolveDataDir: config.ResolveDataDir,
mkdirAll: os.MkdirAll,
loadConfig: config.LoadMinimal,
loadReadOnlyConfig: config.LoadReadOnly,
acquireLaunchLockWithError: func(dataDir string) (daemonLaunchLock, bool, error) {
return acquireBackgroundLaunchLockWithError(dataDir)
},
waitContendedLaunch: waitForDaemonLaunchContention,
writableRecords: writableDaemonRecords,
statusRecords: daemonStatusRecords,
isStarting: IsDaemonStarting,
readStartupState: readStartupState,
startBackground: startServeBackground,
stopTargetConfirmed: stopTargetConfirmed,
stopProcess: stopDaemonProcess,
stopCaddy: stopOrphanedCaddyChildWithWriter,
validateConfig: validateServeConfig,
checkDataVersion: db.CheckDataVersion,
probeRecord: probeDaemonRecord,
writableRuntime: func(dataDir, authToken string) *DaemonRuntime {
return FindWritableDaemonRuntime(dataDir, authToken)
},
now: time.Now,
}
}
func newDaemonCommand() *cobra.Command {
return newDaemonCommandWithDeps(defaultDaemonCommandDeps())
}
func newDaemonCommandWithDeps(deps daemonCommandDeps) *cobra.Command {
cmd := &cobra.Command{
Use: "daemon",
Short: "Manage the background server",
GroupID: groupCore,
SilenceUsage: true,
Args: cobra.NoArgs,
}
cmd.AddCommand(
&cobra.Command{
Use: "start", Short: "Start the background server",
SilenceUsage: true, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runDaemonStart(cmd.OutOrStdout(), deps)
},
},
&cobra.Command{
Use: "status", Short: "Show background server status",
SilenceUsage: true, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runDaemonStatus(cmd.OutOrStdout(), deps)
},
},
&cobra.Command{
Use: "stop", Short: "Stop the background server",
SilenceUsage: true, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runDaemonStop(cmd.OutOrStdout(), deps)
},
},
&cobra.Command{
Use: "restart", Short: "Restart the background server",
SilenceUsage: true, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx, stop := signal.NotifyContext(
cmd.Context(), os.Interrupt, syscall.SIGTERM,
)
defer stop()
return runDaemonRestartAttached(ctx, cmd.OutOrStdout(), deps)
},
},
)
return cmd
}
func prepareDaemonMutation(deps daemonCommandDeps) (string, error) {
dataDir, err := deps.resolveDataDir()
if err != nil {
return "", fmt.Errorf("resolving data dir: %w", err)
}
if err := deps.mkdirAll(dataDir, 0o700); err != nil {
return "", fmt.Errorf("creating data dir: %w", err)
}
return dataDir, nil
}
func writableRuntimeFallbackForCommand(cfg config.Config, deps daemonCommandDeps) *DaemonRuntime {
if deps.writableRuntime == nil {
return nil
}
rt := deps.writableRuntime(cfg.DataDir, cfg.AuthToken)
if rt == nil || !rt.RuntimeFallback {
return nil
}
return rt
}
func runDaemonStart(w io.Writer, deps daemonCommandDeps) error {
dataDir, err := prepareDaemonMutation(deps)
if err != nil {
return fmt.Errorf("daemon start: %w", err)
}
launchLock, ok, err := deps.acquireLaunchLockWithError(dataDir)
if err != nil {
return fmt.Errorf("daemon start: acquiring launch lock: %w", err)
}
if !ok {
return reportDaemonLaunchContention(w, dataDir, deps.waitContendedLaunch(dataDir), deps.now())
}
defer func() { _ = launchLock.Unlock() }()
cfg, err := deps.loadConfig()
if err != nil {
return fmt.Errorf("daemon start: loading config: %w", err)
}
if err := validateLockedDataDir(dataDir, cfg.DataDir); err != nil {
return fmt.Errorf("daemon start: %w", err)
}
if rt := writableRuntimeFallbackForCommand(cfg, deps); rt != nil {
writeDaemonStartResult(w, backgroundLaunchResult{Runtime: rt}, false)
return nil
}
if deps.isStarting(cfg.DataDir) {
return daemonPersistentStartupError("daemon start", cfg.DataDir, deps.readStartupState(cfg.DataDir), deps.now())
}
result, err := deps.startBackground(
cfg, []string{"serve"}, serveReplacementOptions{},
backgroundLaunchPolicy{ConfigOnly: true, Operation: "daemon start"},
)
if err != nil {
return backgroundResultError(err, result)
}
if result.Started && result.Runtime == nil {
return daemonSlowStartupError("daemon start", result)
}
if !result.Started && result.Runtime == nil {
if deps.isStarting(cfg.DataDir) {
return daemonPersistentStartupError("daemon start", cfg.DataDir, deps.readStartupState(cfg.DataDir), deps.now())
}
return errors.New("daemon start: startup did not publish a writable runtime; inspect serve.log before retrying")
}
writeDaemonStartResult(w, result, false)
return nil
}
func reportDaemonLaunchContention(
w io.Writer, dataDir string, observation daemonLaunchObservation, now time.Time,
) error {
if observation.Err != nil {
return fmt.Errorf("daemon start: inspecting concurrent launch: %w", observation.Err)
}
if observation.LockHeld {
if observation.Starting {
return daemonPersistentStartupError("daemon start", dataDir, observation.Snapshot, now)
}
return fmt.Errorf(
"daemon start: launch is still in progress under %s; retry later and verify the owning process manually if it persists",
backgroundLaunchLockPath(dataDir),
)
}
if len(observation.UnconfirmedRecords) > 0 {
return unconfirmedWritableDaemonError(
"daemon start", observation.UnconfirmedRecords,
)
}
if len(observation.Records) > 1 {
return fmt.Errorf(
"daemon start: multiple writable agentsview daemons are running (pids %s); run `agentsview daemon status`, then `agentsview daemon stop` before retrying",
formatRecordPIDList(observation.Records),
)
}
if len(observation.Records) > 0 {
rt := daemonRuntimeFromRecord(observation.Records[0])
fmt.Fprintf(w, "agentsview already running at %s (pid %d)\n", urlFromDaemonRuntime(rt), rt.Record.PID)
return nil
}
if observation.Starting {
return daemonPersistentStartupError("daemon start", dataDir, observation.Snapshot, now)
}
return errors.New("daemon start: concurrent startup failed without publishing a writable runtime; inspect serve.log before retrying")
}
func waitForDaemonLaunchContention(dataDir string) daemonLaunchObservation {
return waitForDaemonLaunchContentionWithDeps(
dataDir, defaultDaemonLaunchWaitDeps(),
)
}
func waitForDaemonLaunchContentionWithDeps(
dataDir string, deps daemonLaunchWaitDeps,
) daemonLaunchObservation {
deadline := deps.now().Add(deps.timeout)
for {
lock, acquired, err := deps.acquireLaunchLock(dataDir)
if deps.onAttempt != nil {
deps.onAttempt()
}
if err != nil {
return daemonLaunchObservation{Err: err}
}
if acquired {
cfg, configErr := deps.loadReadOnlyConfig()
if configErr != nil {
_ = lock.Unlock()
return daemonLaunchObservation{
Err: fmt.Errorf("loading read-only config: %w", configErr),
}
}
if dataDirErr := validateLockedDataDir(dataDir, cfg.DataDir); dataDirErr != nil {
_ = lock.Unlock()
return daemonLaunchObservation{Err: dataDirErr}
}
records, recordsErr := deps.writableRecords(dataDir, cfg.AuthToken)
if recordsErr != nil {
_ = lock.Unlock()
return daemonLaunchObservation{Err: recordsErr}
}
confirmed, unconfirmed := partitionConfirmedDaemonRecords(
records, cfg.AuthToken, deps.confirmed,
)
if len(unconfirmed) == 0 {
for _, rec := range confirmed {
if !deps.probe(rec, cfg.AuthToken) {
_ = lock.Unlock()
return daemonLaunchObservation{Err: fmt.Errorf(
"writable daemon pid %d is not responding to its health probe; run `agentsview daemon restart` to replace it",
rec.PID,
)}
}
if compatibilityErr := daemonRuntimeCompatibilityError(
daemonRuntimeFromRecord(rec),
); compatibilityErr != nil {
_ = lock.Unlock()
return daemonLaunchObservation{Err: fmt.Errorf(
"writable daemon pid %d is incompatible: %w; run `agentsview daemon restart` to replace it",
rec.PID, compatibilityErr,
)}
}
}
}
starting := deps.isStarting(dataDir)
var snapshot *startupState
if starting {
snapshot = deps.readStartupState(dataDir)
}
_ = lock.Unlock()
return daemonLaunchObservation{
Starting: starting,
Snapshot: snapshot,
Records: confirmed,
UnconfirmedRecords: unconfirmed,
}
}
if deps.now().After(deadline) {
starting := deps.isStarting(dataDir)
var snapshot *startupState
if starting {
snapshot = deps.readStartupState(dataDir)
}
return daemonLaunchObservation{
LockHeld: true, Starting: starting, Snapshot: snapshot,
}
}
deps.sleep(deps.tick)
}
}
func unconfirmedWritableDaemonError(
operation string, records []daemon.RuntimeRecord,
) error {
details := make([]string, 0, len(records))
for _, rec := range records {
detail := fmt.Sprintf("pid %d", rec.PID)
if rec.SourcePath != "" {
detail += " (runtime record " + rec.SourcePath + ")"
}
details = append(details, detail)
}
return fmt.Errorf(
"%s: cannot confirm existing writable daemon identity: %s; refusing to launch another writer; verify each process and terminate it manually before retrying",
operation, strings.Join(details, ", "),
)
}
func daemonPersistentStartupError(
operation, dataDir string, st *startupState, now time.Time,
) error {
var details []string
if st != nil {
if st.PID > 0 {
details = append(details, fmt.Sprintf("pid %d", st.PID))
}
if !st.StartedAt.IsZero() && !now.Before(st.StartedAt) {
details = append(details, fmt.Sprintf("elapsed %s", now.Sub(st.StartedAt).Round(time.Second)))
}
if st.Phase != "" {
details = append(details, "phase "+st.Phase)
}
if st.LogPath != "" {
details = append(details, "log "+st.LogPath)
}
}
detail := ""
if len(details) > 0 {
detail = " (" + strings.Join(details, ", ") + ")"
}
return fmt.Errorf(
"%s: startup is still in progress%s; runtime publication may have failed; verify the process and terminate it manually before retrying (startup state: %s)",
operation, detail, startupStatePath(dataDir),
)
}
func writeDaemonStartResult(w io.Writer, result backgroundLaunchResult, restarted bool) {
if result.Runtime != nil && !result.Started {
fmt.Fprintf(w, "agentsview already running at %s (pid %d)\n", urlFromDaemonRuntime(result.Runtime), result.Runtime.Record.PID)
return
}
verb := "running"
if restarted {
verb = "restarted"
}
if result.Runtime != nil {
fmt.Fprintf(w, "agentsview %s at %s (pid %d)\n", verb, urlFromDaemonRuntime(result.Runtime), result.Runtime.Record.PID)
} else {
fmt.Fprintf(w, "agentsview %s in background (pid %d)\n", verb, result.childPID)
}
if result.LogPath != "" {
fmt.Fprintf(w, "Logs: %s\n", result.LogPath)
}
}
func backgroundResultError(err error, result backgroundLaunchResult) error {
if result.LogPath != "" && !strings.Contains(err.Error(), result.LogPath) {
return fmt.Errorf("%w\nLogs: %s", err, result.LogPath)
}
return err
}
func daemonSlowStartupError(
operation string, result backgroundLaunchResult,
) error {
details := []string{fmt.Sprintf("pid %d", result.childPID)}
if result.LogPath != "" {
details = append(details, "log "+result.LogPath)
}
return fmt.Errorf(
"%s: startup is still in progress (%s); the child continues running; retry `agentsview daemon status`",
operation, strings.Join(details, ", "),
)
}
const daemonRestartProgressHeartbeat = 5 * time.Second
type daemonRestartProgressWriter struct {
w io.Writer
phase string
detail string
lastPrintedAt time.Duration
printedAnyProgress bool
}
func (p *daemonRestartProgressWriter) launch(pid int, logPath string) {
fmt.Fprintf(p.w, "Starting agentsview (pid %d)...\n", pid)
if logPath != "" {
fmt.Fprintf(p.w, " log: %s\n", logPath)
}
}
func (p *daemonRestartProgressWriter) progress(
st *startupState, elapsed time.Duration,
) {
if st == nil || st.Phase == "" {
return
}
changed := !p.printedAnyProgress || st.Phase != p.phase || st.Detail != p.detail
if !changed && elapsed-p.lastPrintedAt < daemonRestartProgressHeartbeat {
return
}
line := st.Phase
if st.Detail != "" {
line += ": " + st.Detail
}
fmt.Fprintf(p.w, " %s (%s)\n", line, elapsed.Round(time.Second))
p.phase = st.Phase
p.detail = st.Detail
p.lastPrintedAt = elapsed
p.printedAnyProgress = true
}
func validateLockedDataDir(locked, loaded string) error {
if filepath.Clean(locked) == filepath.Clean(loaded) {
return nil
}
return fmt.Errorf(
"data dir changed after launch lock: locked %q, loaded %q",
locked, loaded,
)
}
func runDaemonStatus(w io.Writer, deps daemonCommandDeps) error {
cfg, err := deps.loadReadOnlyConfig()
if err != nil {
return fmt.Errorf("daemon status: loading config: %w", err)
}
records, err := deps.statusRecords(cfg.DataDir, cfg.AuthToken)
if err != nil {
if rt := writableRuntimeFallbackForCommand(cfg, deps); rt != nil {
for _, line := range serveStatusLines(rt) {
fmt.Fprintln(w, line)
}
return nil
}
return fmt.Errorf("daemon status: inspecting runtime store: %w", err)
}
var writable []daemon.RuntimeRecord
for _, rec := range records {
if !daemonRuntimeFromRecord(rec).ReadOnly {
writable = append(writable, rec)
}
}
if len(writable) > 1 {
fmt.Fprintln(w, "warning: multiple writable agentsview daemons are running; the single-writer invariant is violated.")
for _, rec := range writable {
writeDaemonRecordStatus(w, cfg, rec, deps)
}
return nil
}
if len(writable) == 1 {
writeDaemonRecordStatus(w, cfg, writable[0], deps)
return nil
}
if deps.writableRuntime != nil {
if rt := deps.writableRuntime(cfg.DataDir, cfg.AuthToken); rt != nil {
for _, line := range serveStatusLines(rt) {
fmt.Fprintln(w, line)
}
return nil
}
}
if deps.isStarting(cfg.DataDir) {
fmt.Fprintln(w, "agentsview daemon is starting up.")
for _, line := range serveStartingStatusLines(deps.readStartupState(cfg.DataDir), deps.now()) {
fmt.Fprintln(w, line)
}
return nil
}
fmt.Fprintln(w, "No agentsview daemon is running.")
return nil
}
func writeDaemonRecordStatus(
w io.Writer, cfg config.Config, rec daemon.RuntimeRecord, deps daemonCommandDeps,
) {
rt := daemonRuntimeFromRecord(rec)
compatErr := daemonRuntimeCompatibilityError(rt)
info, responding := deps.probeRecord(rec, cfg.AuthToken)
if responding && info.Version != "" {
rt.Record.Version = info.Version
}
if compatErr != nil {
fmt.Fprintln(w, "agentsview found an incompatible running writable daemon.")
for _, line := range serveStatusLines(rt) {
fmt.Fprintln(w, line)
}
fmt.Fprintf(w, " compatibility: %v\n", compatErr)
if !responding {
fmt.Fprintln(w, " health: not responding to health checks")
}
if rec.SourcePath != "" {
fmt.Fprintf(w, " runtime: %s\n", rec.SourcePath)
}
return
}
if !responding {
fmt.Fprintln(w, "agentsview daemon process is running but not responding to health checks.")
for _, line := range serveStatusLines(rt) {
fmt.Fprintln(w, line)
}
if rec.SourcePath != "" {
fmt.Fprintf(w, " runtime: %s\n", rec.SourcePath)
}
return
}
for _, line := range serveStatusLines(rt) {
fmt.Fprintln(w, line)
}
}
func runDaemonStop(w io.Writer, deps daemonCommandDeps) error {
dataDir, err := prepareDaemonMutation(deps)
if err != nil {
return fmt.Errorf("daemon stop: %w", err)
}
launchLock, ok, err := deps.acquireLaunchLockWithError(dataDir)
if err != nil {
return fmt.Errorf("daemon stop: acquiring launch lock: %w", err)
}
if !ok {
return fmt.Errorf("daemon stop: launch lock %s is busy; retry later", backgroundLaunchLockPath(dataDir))
}
defer func() { _ = launchLock.Unlock() }()
cfg, err := deps.loadConfig()
if err != nil {
return fmt.Errorf("daemon stop: loading config: %w", err)
}
if err := validateLockedDataDir(dataDir, cfg.DataDir); err != nil {
return fmt.Errorf("daemon stop: %w", err)
}
records, err := deps.writableRecords(cfg.DataDir, cfg.AuthToken)
if err != nil {
fallback := writableRuntimeFallbackForCommand(cfg, deps)
if fallback == nil {
return fmt.Errorf("daemon stop: inspecting runtime store: %w", err)
}
records = []daemon.RuntimeRecord{fallback.Record}
} else {
var fallback bool
records, fallback = writableDaemonRecordsWithFallback(records, func() *DaemonRuntime {
return writableRuntimeFallbackForCommand(cfg, deps)
})
if !fallback && deps.isStarting(cfg.DataDir) {
return daemonPersistentStartupError("daemon stop", cfg.DataDir, deps.readStartupState(cfg.DataDir), deps.now())
}
}
if len(records) == 0 {
fmt.Fprintln(w, "agentsview daemon is not running.")
return nil
}
return stopWritableDaemonRecordsSafely(w, cfg, records, daemonStopOperations{
confirmed: deps.stopTargetConfirmed,
stop: deps.stopProcess,
cleanup: deps.stopCaddy,
})
}
func runDaemonRestart(w io.Writer, deps daemonCommandDeps) error {
return runDaemonRestartWithPolicy(context.Background(), w, deps, false)
}
func runDaemonRestartAttached(
ctx context.Context, w io.Writer, deps daemonCommandDeps,
) error {
return runDaemonRestartWithPolicy(ctx, w, deps, true)
}
func runDaemonRestartWithPolicy(
ctx context.Context, w io.Writer, deps daemonCommandDeps, attached bool,
) error {
dataDir, err := prepareDaemonMutation(deps)
if err != nil {
return fmt.Errorf("daemon restart: %w", err)
}
launchLock, ok, err := deps.acquireLaunchLockWithError(dataDir)
if err != nil {
return fmt.Errorf("daemon restart: acquiring launch lock: %w", err)
}
if !ok {
return fmt.Errorf("daemon restart: launch lock %s is busy; retry later", backgroundLaunchLockPath(dataDir))
}
defer func() { _ = launchLock.Unlock() }()
cfg, err := deps.loadConfig()
if err != nil {
return fmt.Errorf("daemon restart: loading config: %w", err)
}
if err := validateLockedDataDir(dataDir, cfg.DataDir); err != nil {
return fmt.Errorf("daemon restart: %w", err)
}
fallback := writableRuntimeFallbackForCommand(cfg, deps)
if fallback == nil && deps.isStarting(cfg.DataDir) {
return daemonPersistentStartupError("daemon restart", cfg.DataDir, deps.readStartupState(cfg.DataDir), deps.now())
}
if err := deps.validateConfig(cfg); err != nil {
return fmt.Errorf("daemon restart: invalid config: %w", err)
}
if err := deps.checkDataVersion(cfg.DBPath); err != nil {
return fmt.Errorf("daemon restart: checking data version: %w", err)
}
records, err := deps.writableRecords(cfg.DataDir, cfg.AuthToken)
if err != nil {
if fallback == nil {
return fmt.Errorf("daemon restart: inspecting runtime store: %w", err)
}
}
if fallback != nil {
records = []daemon.RuntimeRecord{fallback.Record}
}
wasRunning := len(records) > 0
if wasRunning {
if err := stopWritableDaemonRecordsSafely(w, cfg, records, daemonStopOperations{
confirmed: deps.stopTargetConfirmed,
stop: deps.stopProcess,
cleanup: deps.stopCaddy,
}); err != nil {
return fmt.Errorf("daemon restart: %w", err)
}
}
policy := backgroundLaunchPolicy{
ConfigOnly: true, Operation: "daemon restart",
}
if attached {
progress := &daemonRestartProgressWriter{w: w}
policy.Context = ctx
policy.Attached = true
policy.OnLaunch = progress.launch
policy.OnProgress = progress.progress
}
result, err := deps.startBackground(
cfg, []string{"serve"}, serveReplacementOptions{},
policy,
)
if err != nil {
if attached && result.Started &&
(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return daemonRestartCanceledError(result)
}
return backgroundResultError(err, result)
}
if result.Started && result.Runtime == nil {
return daemonSlowStartupError("daemon restart", result)
}
if !result.Started && result.Runtime == nil {
if deps.isStarting(cfg.DataDir) {
return daemonPersistentStartupError("daemon restart", cfg.DataDir, deps.readStartupState(cfg.DataDir), deps.now())
}
return errors.New("daemon restart: startup did not publish a writable runtime; inspect serve.log before retrying")
}
if attached {
// The attached launch callback already printed the log path alongside
// the child identity; keep the completion output to its existing result.
result.LogPath = ""
}
if wasRunning {
writeDaemonStartResult(w, result, true)
} else {
fmt.Fprintln(w, "agentsview started (was not running).")
if result.Runtime != nil {
fmt.Fprintf(w, "agentsview running at %s (pid %d)\n", urlFromDaemonRuntime(result.Runtime), result.Runtime.Record.PID)
}
if result.LogPath != "" {
fmt.Fprintf(w, "Logs: %s\n", result.LogPath)
}
}
return nil
}
func daemonRestartCanceledError(result backgroundLaunchResult) error {
details := []string{fmt.Sprintf("pid %d", result.childPID)}
if result.LogPath != "" {
details = append(details, "log "+result.LogPath)
}
return fmt.Errorf(
"daemon restart: wait canceled (%s); the child continues running; run `agentsview daemon status` to inspect it",
strings.Join(details, ", "),
)
}
func daemonStatusRecords(
dataDir string, authToken string,
) ([]daemon.RuntimeRecord, error) {
migrateLegacyDaemonRuntimes(dataDir, authToken)
store := runtimeStore(dataDir)
if _, err := store.CleanupDead(); err != nil {
return nil, fmt.Errorf("clean dead daemon runtime records: %w", err)
}
records, err := store.List()
if err != nil {
return nil, fmt.Errorf("list daemon runtime records: %w", err)
}
alive := make([]daemon.RuntimeRecord, 0, len(records))
for _, rec := range records {
if rec.Service != "" && rec.Service != daemonService {
continue
}
if !daemon.ProcessAlive(rec.PID) {
continue
}
state := processCreateTimeStateForPID(rec.PID, rec.Metadata[runtimeCreateTime])
if state == processCreateTimeMismatch {
if rec.SourcePath != "" {
if err := os.Remove(rec.SourcePath); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("remove mismatched daemon runtime record %s: %w", rec.SourcePath, err)
}
}
continue
}
alive = append(alive, rec)
}
return alive, nil
}