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:
@@ -152,10 +152,19 @@ VeloxDispatcher::on_download_add(const proto::DownloadSpec& spec) {
|
||||
row.save_dir = target->dir;
|
||||
row.filename = target->leaf;
|
||||
row.created_at = velox::daemon::now_iso();
|
||||
row.start_mode = spec.startMode ? std::string(proto::to_string(*spec.startMode)) : "auto";
|
||||
// startMode 'manual' parks the task in `new`; anything else makes it eligible for the
|
||||
// scheduler (`queued`); on_mutation_ nudges it.
|
||||
row.state = row.start_mode == "manual" ? "new" : "queued";
|
||||
// Contract values only (StartMode.schema.json: 'now'|'later'|'queue'); absent means
|
||||
// "start it", same as an explicit 'now'.
|
||||
row.start_mode =
|
||||
spec.startMode ? std::string(proto::to_string(*spec.startMode)) : "now";
|
||||
// 'later' — the File Info dialog's Download Later button — lands the task in `paused`
|
||||
// per StartMode's own description; anything else ('now' or 'queue') is eligible for
|
||||
// the scheduler immediately (`queued`); on_mutation_ nudges it.
|
||||
if (row.start_mode == "later") {
|
||||
row.state = "paused";
|
||||
row.pause_reason = "user";
|
||||
} else {
|
||||
row.state = "queued";
|
||||
}
|
||||
row.category_id = spec.categoryId;
|
||||
row.queue_id = spec.queueId;
|
||||
row.description = spec.description;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "rpc/single_instance.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "util/crypto.hpp"
|
||||
|
||||
namespace velox::daemon::rpc {
|
||||
|
||||
int acquire_single_instance_lock(const std::string& runtime_dir) {
|
||||
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
// Truncated to 16 hex chars (64 bits): a collision would need two distinct runtime
|
||||
// dirs to hash together, which is not a security boundary here — the socket itself is
|
||||
// still 0600 same-UID-checked; this is only "don't let two daemons stomp each other".
|
||||
const std::string name =
|
||||
"velox-daemon-" + velox::daemon::crypto::sha256_hex(runtime_dir).substr(0, 16);
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
// Leading NUL selects the abstract namespace; the name follows, not NUL-terminated.
|
||||
addr.sun_path[0] = '\0';
|
||||
std::memcpy(addr.sun_path + 1, name.c_str(), name.size());
|
||||
const socklen_t len =
|
||||
static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + 1 + name.size());
|
||||
|
||||
if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), len) != 0) {
|
||||
::close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
} // namespace velox::daemon::rpc
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user