daemon: fix startMode 'later' -32603 and isolate the single-instance lock by runtime dir

Two bugs blocking PROTO's live-veloxd conformance check.

1. tasks.start_mode's CHECK was ('auto','now','queue','manual') — not the
   contract's StartMode enum (['now','later','queue']) at all. 'later', a real
   documented value (the File Info dialog's Download Later button), hit the CHECK
   on every insert and surfaced as an unhandled -32603; 'auto'/'manual' were
   never contract values to begin with.

   Migration 0003 rebuilds tasks (SQLite can't ALTER a CHECK) with the contract's
   values, remapping existing rows by what they actually meant: 'auto' -> 'now'
   (eligible for the scheduler immediately), 'manual' -> 'later' (parked, matching
   StartMode's own "lands the task in paused" description). store_migrations_test
   covers the remap and that 'later' inserts clean while the retired spellings
   are rejected.

   dispatcher.cpp's on_download_add matched: default (absent startMode) is now
   'now' instead of the invented 'auto'; 'later' actually lands the task in
   `paused` (pause_reason 'user') instead of a dead 'manual' -> `new` branch that
   spec.startMode (typed as the 3-value enum) could never even reach.
   TaskRow::start_mode's in-memory default followed suit ('now').

2. main.cpp's single-instance guard bound an abstract socket named
   "velox-daemon-<euid>" — one name per user, system-wide. XDG_RUNTIME_DIR
   isolation never reached it: a leaked test veloxd held the lock for 4h40m and
   locked out every other isolated instance with the same euid (PROTO, EXT, the
   orchestrator), real daemon included.

   Extracted rpc/single_instance.{hpp,cpp} (was a static in main.cpp, untestable)
   and derived the abstract-socket name from a hash of the resolved runtime dir
   path instead of euid alone. The real per-user daemon is still unique (its
   runtime dir is unique to it); isolated instances pointed at their own runtime
   dirs now coexist. main() resolves the runtime dir before acquiring the lock
   (was the other way around). New single_instance_test covers same-dir refusal,
   different-dir coexistence, and release-on-close.

Verified against real veloxd binaries, not just unit tests: startMode: "later"
via a live download.add lands in `paused`; two veloxd with different runtime
dirs run concurrently, two with the same one and the second refuses with the
runtime dir named in the error. Full ctest: 39/39.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
This commit is contained in:
2026-09-11 17:04:02 +04:00
co-authored by Claude Sonnet 5
parent a967eca669
commit de748cc2fc
10 changed files with 270 additions and 35 deletions
+27
View File
@@ -0,0 +1,27 @@
#pragma once
// Single-instance guard: bind an abstract-namespace Unix socket whose name is derived
// from the canonical runtime directory (resolve_runtime_dir's result — already the
// per-user default, /run/user/<uid>/velox, unless XDG_RUNTIME_DIR says otherwise). A
// second daemon pointed at the same runtime dir gets EADDRINUSE and exits; one pointed at
// a different (isolated / test) runtime dir gets its own lock and starts fine. The kernel
// reclaims an abstract-namespace address when the holding process dies, so a crash never
// wedges it (docs/01 §2).
//
// Naming this "velox-daemon-<euid>" alone (the old scheme) meant exactly one name per
// user system-wide, so XDG_RUNTIME_DIR isolation never reached it: a leaked test veloxd
// with the same euid held the lock for every isolated instance too, real or test, until
// it was killed. Hashing the resolved runtime dir path instead keeps the real per-user
// daemon unique (its runtime dir is unique to it) while letting isolated instances that
// each point at their own runtime dir coexist.
#include <string>
namespace velox::daemon::rpc {
// Returns the held fd (kept open for the process lifetime; closing it releases the lock)
// or -1 if another process already holds the lock for this exact `runtime_dir`, or on any
// other socket error.
int acquire_single_instance_lock(const std::string& runtime_dir);
} // namespace velox::daemon::rpc