a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gofrs/flock"
|
|
|
|
"github.com/zzet/gortex/internal/daemon"
|
|
)
|
|
|
|
// daemonDecision is the resolved auto-start outcome.
|
|
type daemonDecision int
|
|
|
|
const (
|
|
daemonReady daemonDecision = iota // socket was already live
|
|
daemonAutostarted // we (or a peer) brought it up; socket live
|
|
daemonUnavailable // autostart off, or spawn failed/timed out
|
|
)
|
|
|
|
const (
|
|
// spawnLockTimeout must outlast spawnDetachedDaemon's readiness wait
|
|
// (~60s) so a loser blocked on the lock is waited out, not contended
|
|
// into a second spawn.
|
|
spawnLockTimeout = 65 * time.Second
|
|
// spawnFailCooldown bounds serial retries of a broken spawn: within
|
|
// the window at most one spawn attempt runs across contending callers.
|
|
spawnFailCooldown = 5 * time.Second
|
|
)
|
|
|
|
// Injectable seams so the race/fallback/spawn-failure branches are
|
|
// testable without a real daemon.
|
|
var (
|
|
isDaemonRunning = daemon.IsRunning
|
|
spawnDaemon = spawnDetachedDaemon
|
|
stopIntentActive = daemon.StopIntentActive
|
|
)
|
|
|
|
// resolveDaemonDecision probes the socket and, when auto-start is enabled
|
|
// and no daemon is up, single-flights a spawn. It never returns an error;
|
|
// an unrecoverable state collapses to daemonUnavailable so the caller
|
|
// falls back to the embedded server.
|
|
func resolveDaemonDecision() daemonDecision {
|
|
return ensureDaemonReady(daemon.ParseAutostart())
|
|
}
|
|
|
|
// ensureDaemonReady is the lock-protected single-flight critical section,
|
|
// shared by `gortex mcp` and `gortex track`.
|
|
func ensureDaemonReady(autostart bool) daemonDecision {
|
|
if isDaemonRunning() {
|
|
return daemonReady
|
|
}
|
|
if !autostart {
|
|
return daemonUnavailable
|
|
}
|
|
// Respect an explicit `daemon stop`: do not resurrect a daemon the user
|
|
// deliberately stopped. The mark is cleared by `daemon start` / `restart`.
|
|
// A suppressed `gortex mcp` falls back to the embedded server, so this
|
|
// declines the background daemon without breaking the tool surface.
|
|
if stopIntentActive() {
|
|
return daemonUnavailable
|
|
}
|
|
|
|
lockPath := daemon.SpawnLockPath()
|
|
_ = os.MkdirAll(filepath.Dir(lockPath), 0o700)
|
|
lock := flock.New(lockPath)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), spawnLockTimeout)
|
|
defer cancel()
|
|
locked, err := lock.TryLockContext(ctx, 100*time.Millisecond)
|
|
if err != nil || !locked {
|
|
// Couldn't acquire the lock in time. The winner may have brought
|
|
// the socket up while we waited — re-probe before giving up.
|
|
if isDaemonRunning() {
|
|
return daemonReady
|
|
}
|
|
return daemonUnavailable
|
|
}
|
|
defer func() { _ = lock.Unlock() }()
|
|
|
|
// Re-probe inside the lock: a peer may have won the race while we
|
|
// blocked on the lock.
|
|
if isDaemonRunning() {
|
|
return daemonReady
|
|
}
|
|
// A recent failed spawn within the cooldown — skip our own attempt so
|
|
// K callers don't serially retry a broken spawn.
|
|
if spawnFailedRecently() {
|
|
return daemonUnavailable
|
|
}
|
|
if err := spawnDaemon(); err != nil {
|
|
stampSpawnFailure()
|
|
return daemonUnavailable
|
|
}
|
|
return daemonAutostarted
|
|
}
|
|
|
|
func spawnFailedRecently() bool {
|
|
info, err := os.Stat(daemon.SpawnFailMarkerPath())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return time.Since(info.ModTime()) < spawnFailCooldown
|
|
}
|
|
|
|
func stampSpawnFailure() {
|
|
path := daemon.SpawnFailMarkerPath()
|
|
_ = os.MkdirAll(filepath.Dir(path), 0o700)
|
|
_ = os.WriteFile(path, []byte(strconv.FormatInt(time.Now().UnixNano(), 10)), 0o600)
|
|
}
|