Skip to content

Releases: grpmsoft/daemon

v0.4.2 — an authenticated control-plane client

Choose a tag to compare

@kolkov kolkov released this 22 Oct 12:15

An authenticated client for the daemon's control plane, and a shutdown path that uses it.

Added

  • Daemon.ControlClient() and ControlClient — Get(ctx, path) and Post(ctx, path) against the loopback control plane with the bearer token from the PID file. The token stays inside the client (no accessor hands it out); a non-2xx status is an error carrying the status line and a bounded body snippet, so a failed call is reported instead of read as an empty answer. Requests have a 30-second ceiling of their own (a caller's shorter deadline still wins), redirects are not followed, and a body over 8 MiB is an error instead of a silently truncated one.

Changed

  • Stop sends its shutdown request through ControlClient — one implementation of the address, the bearer token and the status check.

Upgrade

go get github.com/grpmsoft/daemon@v0.4.2

Full changelog: https://github.com/grpmsoft/daemon/blob/main/CHANGELOG.md
Compare: v0.4.1...v0.4.2

v0.4.1 — Proxy survives a daemon restart

Choose a tag to compare

@kolkov kolkov released this 19 Oct 12:08

v0.4.1 — Proxy survives a daemon restart

Fixed

  • Proxy follows a replaced daemon. Before this release, Proxy read the daemon's port once. After a restart, a crash or an idle shutdown, the next request failed and Proxy returned, so every MCP client had to reconnect by hand. The lease was not re-attached either, so the new daemon saw zero agents while clients were still open. Now:
    • when its lease drops, the proxy looks for a daemon in the PID file for up to Config.Timeout and re-attaches. It never starts one there: Stop, Hold and the idle timer are not undone by an idle client;
    • a request whose dial failed, or that a daemon answered with 401 (another daemon on the old port), is sent once more after reconnecting (EnsureRunning when Config.Binary is set, otherwise the PID file);
    • a request that failed after it was sent is answered with a JSON-RPC error for its id (-32603, "daemon unavailable: …") and is never sent again, because it may have run. Notifications get no answer;
    • Proxy returns only when its context ends, stdin ends or fails, or stdout fails.

No API changes.

go get github.com/grpmsoft/daemon@v0.4.1

Full Changelog: v0.4.0...v0.4.1

v0.4.0 — Breaking Cleanup

Choose a tag to compare

@kolkov kolkov released this 24 Sep 23:16
3d734ef

v0.4.0 — Breaking Cleanup

Zero deprecated code. All breaking changes in one release.

Breaking Changes

  • Remove connect/disconnect endpoints — use /daemon/attach (lease-based)
  • ProcessManager.Start(ctx, StartSpec) replaces StartDetached
  • DisableTokenAuth replaces RequireToken (inverted: secure by default)
  • WaitUntilReady(ctx, ...) — ctx parameter added to HealthChecker
  • ShutdownTimeout — daemon drain budget; worst case ShutdownTimeout + 7s

Added

  • Hold(ctx) / Release() — opt-in autostart prevention via stop-intent marker
  • SpawnCooldown (default 5s) — prevents rapid respawn after failure
  • sync.Mutex on *Daemon — satisfies race detector for in-process calls

Fixed

  • StateNew connection drain (5s stdlib delay with pooled connections)
  • EnsureRunning check order: IsHeld → stop-intent → cooldown → start

Migration

v0.3.x v0.4.0
RequireToken: true Remove field (secure by default)
RequireToken: false DisableTokenAuth: true
StartDetached(bin, args, log, env) Start(ctx, StartSpec{...})
WaitUntilReady(port, path, timeout) WaitUntilReady(ctx, port, path, timeout)
POST /daemon/connect GET /daemon/attach

v0.3.3 — Config.Dir + DataDir absolute path

Choose a tag to compare

@kolkov kolkov released this 12 Sep 16:41
c347e3e
  • Config.Dir: working directory for spawned daemon process (default = DataDir). Prevents daemon from holding parent's CWD which may be deleted or unmounted after start
  • DataDir absolute: resolved to absolute path at construction time via filepath.Abs. Fixes relative DataDir (.gode) causing path doubling on Windows
  • Config immutable after New(): applyDefaults runs once at construction, methods only read — eliminates data race on concurrent calls

v0.3.2

Choose a tag to compare

@kolkov kolkov released this 12 Sep 14:47
d1e4ce3

Cleanup release.

v0.3.1 — Lease-based attach + control-plane bearer token

Choose a tag to compare

@kolkov kolkov released this 12 Sep 14:21
4231b40

Lease-based connection tracking and control-plane bearer token. Additive patch -- no API breakage, full backward compat with v0.3.0.

Added

  • GET /daemon/attach -- lease-based connection tracking (ADR-003). The TCP connection IS the lease: when the client process dies (SIGKILL, OOM, closed terminal), the kernel closes the socket and the connection count drops. No heartbeats, no timers. Replaces counting-based connect/disconnect for new clients
  • Bearer token on /daemon/* endpoints -- rand.Text() (crypto/rand) stored in PID file (0600). Authorization: Bearer <token> required for /daemon/attach, /daemon/connect, /daemon/disconnect, /daemon/shutdown. /health stays open. Constant-time comparison via crypto/subtle
  • Config.RequireToken -- when true, the bearer token is also required for the application handler (everything outside /health and /daemon/). Default false for curl debugging compatibility
  • Proxy attach with fallback -- proxy tries GET /daemon/attach first; if daemon returns 404 (v0.3.0), falls back to POST /daemon/connect + POST /daemon/disconnect. Token sent on all daemon requests when available
  • ADR-003 -- design document: why counting fails, lease via TCP, token via PID file, mixed-version matrix, alternatives rejected (heartbeats, SSE, mTLS)
  • 12 new tests (3 integration), coverage 75.5% → 85%

Deprecated

  • POST /daemon/connect and POST /daemon/disconnect -- use GET /daemon/attach instead. Kept functional for backward compatibility with v0.3.0 proxies

Security

  • DNS rebinding hardening completed: loopbackGuard (v0.1.1) + bearer token (v0.3.1). A page with a rebinding domain that passes the Host check still cannot authenticate
  • Token never appears in proxy.log, daemon stderr, or error messages

Mixed-version compatibility

Proxy Daemon Behavior
v0.3.1 v0.3.1 Lease attach + bearer token (optimal)
v0.3.1 v0.3.0 Fallback to connect/disconnect, no token (compatible)
v0.3.0 v0.3.1 connect/disconnect with token from PID file (compatible)

Full Changelog: v0.3.0...v0.3.1

v0.3.0 — Lifecycle API v2, unified lock protocol

Choose a tag to compare

@kolkov kolkov released this 12 Sep 13:15
6bc8d2c

Lifecycle API v2 -- unified lock protocol. All lifecycle mutations serialize on a single startup lock. One internal path, two public semantics.

Breaking Changes

  • Start(ctx, binary, args) error → Start(ctx) (*Info, error)
  • Restart(ctx, binary, args) error → Restart(ctx) (*Info, error)
  • EnsureRunning(ctx, cfg, binary, args) (int, error) → method d.EnsureRunning(ctx) (*Info, error); package-level EnsureRunning(ctx, cfg) (int, error) kept as convenience wrapper
  • KillProcess(pid int) error → KillProcess(ctx, pid, grace) error -- context-aware with configurable timeout
  • Config gains Binary and Args fields -- daemon identity in config, not per-call params. Binary defaults to os.Executable()
  • Stop(ctx) now serialized on startup lock

Added

  • Unified lock protocol (ADR-002): startLocked/stopLocked internal methods; public methods never call other public methods (prevents OFD deadlock)
  • Config.Validate() enforced: called in Start, EnsureRunning, Stop, Restart, Serve -- path traversal rejected before any file I/O
  • Proxy IsHeld pre-check: dead daemon returns ErrNotRunning immediately instead of connection refused
  • InheritFD EWOULDBLOCK: documented and tested same-OFD re-flock behavior
  • Windows IsHeld: uses OPEN_EXISTING -- no empty file creation on probe
  • Orphan child killed on start failure (D8): KillProcess + log tail in error diagnostics
  • CI: actions upgraded to Node 24 (checkout@v7, setup-go@v7, codecov@v7, golangci-lint@v9), Dependabot for actions, cache: false for zero-deps module

Changed

  • EnsureRunning is now a method on *Daemon (testable with NewWithDeps)
  • All mutating methods return *Info (pid, port, start time in one call)
  • Restart runs stopLocked + startLocked under a single lock acquisition

Validation

  • Fable 5.1: 8 harness scenarios green -- concurrent Start (3 goroutines), Restart under parallel EnsureRunning (33 calls, 0 errors), D8 orphan kill, path traversal rejection
  • CI: 8/8 checks green (build/test/lint/fmt × 3 OS including Windows with -race)

Full Changelog: v0.2.0...v0.3.0

v0.2.0 — Inherited-Lock PID File + API Redesign

Choose a tag to compare

@kolkov kolkov released this 12 Sep 09:40
5173f33

Breaking Changes

  • Stop() → Stop(ctx) — callers must pass context
  • Proxy(ctx, port, path) → Proxy(ctx, cfg, ProxyOptions) — testable I/O
  • SetHandler() deleted — dead code
  • IsAlive() based on pidlock (lock held = alive)

Architecture: Inherited-Lock PID File

PID file IS the lock. Correct by construction on all platforms.

  • Unix: flock + fd inheritance via ExtraFiles
  • Windows: share-mode + startup LockFileEx
  • Startup lock protocol: two locks, two purposes, correct ordering
  • N12 recovery: holder dies → next waiter becomes starter
  • LockCtx: ctx-aware locking with LOCK_NB poll

New Features

  • Sentinel errors (ErrNotRunning, ErrAlreadyRunning, etc.)
  • Config.Validate() — rejects invalid names, paths
  • POST /daemon/shutdown — cross-platform graceful stop
  • ProxyOptions — testable stdin/stdout, opt-in payload logging
  • CAS Disconnect, EINTR retry

Verified

  • 13 review rounds
  • 5 integration tests (helper-process pattern)
  • CI green on Linux + macOS + Windows
  • Coverage: 71.4% daemon, 86.2% internal, 75% pidlock

v0.1.1 — Correctness & Security

Choose a tag to compare

@kolkov kolkov released this 12 Sep 03:53
a4533af

Fixed

  • Start() creates DataDir if missing (os.MkdirAll) — first-run failure
  • Idle timer arms on startup with zero connections — was waiting forever
  • ConnTracker.Disconnect() clamps to zero — no more negative count
  • Stop() uses IsAlive() (PID + binary verification) — no more killing unrelated processes. Note: macOS/BSD remains PID-only (no /proc, deferred to v0.2.0)
  • Unix reaper goroutine prevents zombie processes; isZombie() via /proc/<pid>/stat
  • EnsureRunning() serialized with exclusive file lock (flock/LockFileEx)
  • Unlock() no longer deletes lock file (flock+unlink race fix)
  • Binary path comparison handles (deleted) suffix + symlinks + case-insensitive on Windows
  • Serve() compare-and-delete: orphan cleanup can't delete a live daemon's PID file
  • Proxy() skips disconnect if connect failed (no stray decrements)

Security

  • loopbackGuard middleware: rejects non-loopback Host/Origin headers (DNS rebinding, MCP spec)
  • Origin validation via url.Parse + hostname (blocks 127.0.0.1.evil.com bypass)

Tests

  • Lock exclusivity + no-delete regression (V1)
  • binaryPathsEqual 10 table-driven cases incl. (deleted) suffix
  • Compare-and-delete: orphan keeps foreign PID, own removed (V2c)
  • Loopback guard 16 cases incl. bypass attack vectors
  • ConnTracker clamp 6 table-driven cases
  • isZombie parser 13 cases

v0.1.0 — Initial Release

Choose a tag to compare

@kolkov kolkov released this 11 Sep 18:47

daemon v0.1.0

Cross-platform daemon lifecycle library for Go. Pure Go, zero CGO, zero dependencies (stdlib only).

Features

  • Client mode — Start/Stop/Restart/Status to manage background daemon processes
  • Server mode — Serve() with automatic port selection, PID file, signal handling, health endpoint
  • Proxy mode — Proxy() bridges stdin/stdout to daemon HTTP for MCP/JSON-RPC protocols
  • EnsureRunning — start-if-needed with race-safe PID locking
  • run.Group — actor concurrency pattern (oklog/run reimplemented, zero deps)
  • Connection tracking — atomic connect/disconnect counting with idle auto-shutdown
  • Rich Model — Daemon struct + interfaces (PIDStore, ProcessManager, HealthChecker)
  • Cross-platform — Windows (CREATE_NEW_PROCESS_GROUP) + Linux/macOS (setsid)

Install

go get github.com/grpmsoft/daemon

Quality

  • 106 tests, 83%/87% coverage
  • Zero lint warnings (golangci-lint v2)
  • CI: build + test + lint on 3 OS (Ubuntu, macOS, Windows)
  • Go 1.27+, stdlib only