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
+40
View File
@@ -0,0 +1,40 @@
// The single-instance lock: isolated by runtime dir, not just by euid. This is the bug a
// leaked test veloxd exploited — one abstract-socket name per user meant every isolated
// instance (real daemon, tests, other lanes) fought over the same lock.
#include <unistd.h>
#include "check.hpp"
#include "rpc/single_instance.hpp"
using namespace velox::daemon::rpc;
void run() {
// Two different runtime dirs: both acquire the lock independently.
{
const int a = acquire_single_instance_lock("/run/user/1000/velox-test-a");
const int b = acquire_single_instance_lock("/run/user/1000/velox-test-b");
CHECK(a >= 0);
CHECK(b >= 0);
if (a >= 0) ::close(a);
if (b >= 0) ::close(b);
}
// Same runtime dir: the second attempt is refused while the first still holds it.
{
const int first = acquire_single_instance_lock("/run/user/1000/velox-test-shared");
CHECK(first >= 0);
const int second = acquire_single_instance_lock("/run/user/1000/velox-test-shared");
CHECK(second < 0);
if (first >= 0) ::close(first);
if (second >= 0) ::close(second);
// Releasing (closing) the fd frees the abstract-namespace name immediately — a
// third attempt at the same dir succeeds once the first is gone.
const int third = acquire_single_instance_lock("/run/user/1000/velox-test-shared");
CHECK(third >= 0);
if (third >= 0) ::close(third);
}
}
TEST_MAIN()