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
198 lines
7.1 KiB
Go
198 lines
7.1 KiB
Go
package daemon
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/fnv"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/zzet/gortex/internal/platform"
|
|
)
|
|
|
|
// stateDir returns the directory the daemon keeps its runtime state in
|
|
// (socket, PID file, logs, snapshot) and whether it could be resolved.
|
|
//
|
|
// An absolute $XDG_CACHE_HOME is honoured on every platform. When it is
|
|
// unset the location stays at the historical default so an existing
|
|
// daemon state directory is not orphaned:
|
|
//
|
|
// - Windows: %USERPROFILE%\.gortex\cache (via os.UserCacheDir).
|
|
// - macOS / Linux: $HOME/.gortex/cache.
|
|
//
|
|
// The boolean is false when the home / cache directory can't be
|
|
// resolved at all, in which case callers fall back to the temp dir.
|
|
func stateDir() (string, bool) {
|
|
if runtime.GOOS == "windows" {
|
|
if v := os.Getenv("XDG_CACHE_HOME"); v == "" || !filepath.IsAbs(v) {
|
|
if _, err := os.UserCacheDir(); err != nil {
|
|
return "", false
|
|
}
|
|
}
|
|
return platform.OSCacheDir(), true
|
|
}
|
|
if v := os.Getenv("XDG_CACHE_HOME"); v == "" || !filepath.IsAbs(v) {
|
|
if _, err := os.UserHomeDir(); err != nil {
|
|
return "", false
|
|
}
|
|
}
|
|
return platform.CacheDir(), true
|
|
}
|
|
|
|
// SocketPath returns the socket path the daemon listens on. The socket
|
|
// is an AF_UNIX socket on every supported OS — Windows has supported
|
|
// AF_UNIX since Windows 10 1803, so the same transport works there.
|
|
//
|
|
// Order of preference:
|
|
// 1. $GORTEX_DAEMON_SOCKET — explicit override (tests, custom deployments).
|
|
// 2. $XDG_RUNTIME_DIR/gortex.sock — Linux standard for user runtime files.
|
|
// This path is cleaned automatically on logout and has sensible perms.
|
|
// 3. The per-user state dir — $HOME/.gortex/cache on macOS/Linux,
|
|
// %USERPROFILE%\.gortex\cache on Windows.
|
|
//
|
|
// AF_UNIX socket paths have a length limit (~104 bytes on macOS, 108 on Linux
|
|
// and Windows). An auto-computed path that would exceed it — a deeply-nested
|
|
// home directory, a long $XDG_RUNTIME_DIR — is replaced by a short, stable
|
|
// temp-dir fallback (clampSocketPath) so the listener binds instead of failing.
|
|
// An explicit $GORTEX_DAEMON_SOCKET override is honoured verbatim: the user
|
|
// chose that path and gets a loud failure if it's too long, never a silent
|
|
// redirect.
|
|
func SocketPath() string {
|
|
if override := os.Getenv("GORTEX_DAEMON_SOCKET"); override != "" {
|
|
return override
|
|
}
|
|
return clampSocketPath(autoSocketPath())
|
|
}
|
|
|
|
// autoSocketPath computes the daemon's default socket path from the runtime /
|
|
// state directories, before any length clamping.
|
|
func autoSocketPath() string {
|
|
if rt := os.Getenv("XDG_RUNTIME_DIR"); rt != "" && runtime.GOOS == "linux" {
|
|
return filepath.Join(rt, "gortex.sock")
|
|
}
|
|
if dir, ok := stateDir(); ok {
|
|
return filepath.Join(dir, "daemon.sock")
|
|
}
|
|
// Fall back to the temp dir as a last resort; the daemon must start
|
|
// somewhere.
|
|
return filepath.Join(os.TempDir(), "gortex.sock")
|
|
}
|
|
|
|
// socketAddrMax is the AF_UNIX sun_path limit for the current OS: 104 bytes on
|
|
// macOS/BSD, 108 on Linux and Windows. A path whose length reaches this fails
|
|
// the bind, so we clamp strictly below it.
|
|
func socketAddrMax() int {
|
|
if runtime.GOOS == "darwin" {
|
|
return 104
|
|
}
|
|
return 108
|
|
}
|
|
|
|
// clampSocketPath returns p unchanged when it is short enough to bind, else a
|
|
// short temp-dir fallback derived from a stable hash of p — so two daemons that
|
|
// would have used different over-long paths still get distinct sockets.
|
|
func clampSocketPath(p string) string {
|
|
if len(p) < socketAddrMax() {
|
|
return p
|
|
}
|
|
h := fnv.New32a()
|
|
_, _ = h.Write([]byte(p))
|
|
return filepath.Join(os.TempDir(), fmt.Sprintf("gx-%08x.sock", h.Sum32()))
|
|
}
|
|
|
|
// PIDFilePath returns the path of the daemon PID file. The daemon writes
|
|
// this on startup and removes it on graceful shutdown. Staleness detection
|
|
// (for crashed daemons that never removed their PID) is a process-liveness
|
|
// probe — see platform.ProcessAlive.
|
|
func PIDFilePath() string {
|
|
if override := os.Getenv("GORTEX_DAEMON_PIDFILE"); override != "" {
|
|
return override
|
|
}
|
|
if dir, ok := stateDir(); ok {
|
|
return filepath.Join(dir, "daemon.pid")
|
|
}
|
|
return filepath.Join(os.TempDir(), "gortex-daemon.pid")
|
|
}
|
|
|
|
// LogFilePath returns the path the daemon writes logs to when running in
|
|
// --detach mode. In foreground mode stderr is used instead.
|
|
func LogFilePath() string {
|
|
if override := os.Getenv("GORTEX_DAEMON_LOGFILE"); override != "" {
|
|
return override
|
|
}
|
|
if dir, ok := stateDir(); ok {
|
|
return filepath.Join(dir, "daemon.log")
|
|
}
|
|
return filepath.Join(os.TempDir(), "gortex-daemon.log")
|
|
}
|
|
|
|
// SnapshotPath returns the legacy backend-agnostic snapshot path —
|
|
// `daemon.gob.gz` under the state dir. Kept for callers that haven't
|
|
// moved to backend-tagged storage yet (the legacy cloud indexer
|
|
// worker). The daemon itself routes through
|
|
// BackendSnapshotPath so a memory ↔ disk-backend switch can't read the
|
|
// other backend's snapshot — see that function's doc.
|
|
func SnapshotPath() string {
|
|
if override := os.Getenv("GORTEX_DAEMON_SNAPSHOT"); override != "" {
|
|
return override
|
|
}
|
|
if dir, ok := stateDir(); ok {
|
|
return filepath.Join(dir, "daemon.gob.gz")
|
|
}
|
|
return filepath.Join(os.TempDir(), "gortex-daemon.gob.gz")
|
|
}
|
|
|
|
// BackendSnapshotPath returns a backend-tagged snapshot path so the
|
|
// memory and disk backends use distinct files. The memory backend
|
|
// snapshot is a full gob+gzip of the in-memory graph; the disk
|
|
// backend snapshot is metadata-only (FileMtimes, contracts, vector
|
|
// index) because the graph itself lives in the on-disk store. Loading
|
|
// the memory backend's snapshot into a disk-backed daemon (or vice
|
|
// versa) silently produced wrong state — empty graph after disk→memory
|
|
// switch, decode-and-discard nodes after memory→disk — so a fresh
|
|
// daemon now picks the right file by backend tag.
|
|
//
|
|
// Empty backend tag falls back to SnapshotPath() so embedded callers
|
|
// that don't know the backend (the cloud indexer worker) keep working.
|
|
//
|
|
// GORTEX_DAEMON_SNAPSHOT overrides every backend tag — the override
|
|
// is an explicit "use exactly this path" signal.
|
|
func BackendSnapshotPath(backend string) string {
|
|
if override := os.Getenv("GORTEX_DAEMON_SNAPSHOT"); override != "" {
|
|
return override
|
|
}
|
|
tag := normalizeBackendTag(backend)
|
|
if tag == "" {
|
|
return SnapshotPath()
|
|
}
|
|
filename := "daemon-" + tag + ".gob.gz"
|
|
if dir, ok := stateDir(); ok {
|
|
return filepath.Join(dir, filename)
|
|
}
|
|
return filepath.Join(os.TempDir(), "gortex-"+filename)
|
|
}
|
|
|
|
// normalizeBackendTag canonicalizes a backend identifier into the
|
|
// short tag used in the snapshot filename — "memory" / "sqlite" /
|
|
// etc. Empty / unknown input returns the empty string so the caller
|
|
// can fall back to the legacy unsuffixed path.
|
|
func normalizeBackendTag(backend string) string {
|
|
switch backend {
|
|
case "memory", "mem", "in-memory":
|
|
return "memory"
|
|
case "sqlite", "sqlite3":
|
|
return "sqlite"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// EnsureParentDir creates the parent directory of path with permissions
|
|
// 0o700 (user only). Daemon state files live under the user's cache dir
|
|
// and should not be world-readable. The mode is advisory on Windows,
|
|
// where filesystem ACLs already scope %USERPROFILE% to the user.
|
|
func EnsureParentDir(path string) error {
|
|
dir := filepath.Dir(path)
|
|
return os.MkdirAll(dir, 0o700)
|
|
}
|