Rebased lane/daemon onto main first — fast-forward, no conflicts (lane/daemon was already fully merged; main has since taken lane/gui, lane/pkg-qa, lane/proto). 1. capture.offer and capture.getRules were stubs, buried inside D3's generic stub list with no flag for how load-bearing capture.offer is: it's a CLAUDE.md §4 non-negotiable (750ms deadline, fails open) and AGENT-DAEMON.md build step 6, and against the real daemon it 500s to -32603 every time — the extension captures nothing, not "falls through to Firefox on a slow path." Split into their own D7/D8 rows so they stop being invisible. 2. settings.get/settings.set (D9, closed). Four pointer-to-member tables in dispatcher.cpp (bool / ranged int / plain string / string array), five enum-typed keys handled individually since parse_XXX already validates those — covers all 43 SettingKeys without ~40 repetitive hand blocks. store::Settings::kDefaults grew from 14 entries to 43 (the schema itself carries no "default" keyword anywhere, so these are hand-chosen — conservative for the ones ADR 0012 doesn't speak to; capture.monitoredExtensions defaults to the union of every builtin category's extensions rather than an arbitrary list of its own). settings.set validates every field before writing any of them: numeric min/max (hand-checked — the generated parser only checks JSON type, not schema constraints) and saveTo.* paths via fs::resolve_target/canonicalize_root (-32011) — allowedRoots entries checked as roots in their own right, defaultDir/tempDir checked as paths resolving inside the (possibly just-updated, same call) root list. Reports exactly the keys whose effective value actually changed, publishes event.settings.changed with that list, and calls the scheduler's reload_config() when a connection.* key took effect — live, not on next restart. Verified against real veloxd: all 43 keys round-trip with defaults, a keys subset filters, an out-of-range value rejects the whole call, a bad saveTo.defaultDir is -32011, allowedRoots+defaultDir set together cross-validate against the new roots, event.settings.changed fires over a live subscription. New dispatcher_settings_test covers the same ground without a socket. Full ctest: 53/53 (excluding a pre-existing, unrelated conformance failure — see below). Also found, not fixed (not this lane): tests/conformance/run.sh's live-veloxd leg fails "pairing failed: no token issued" reproducibly, isolated, on main before any of this session's changes — it launches the isolated veloxd without VELOX_PAIR_AUTO=1, so EnvAutoApprover denies every session.pair and the WS leg's session.hello never gets a token. tests/conformance/ is PROTO/QA-owned; flagging rather than editing across the lane boundary. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
169 lines
6.4 KiB
C++
169 lines
6.4 KiB
C++
#include "store/settings.hpp"
|
|
|
|
#include <array>
|
|
#include <utility>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
namespace velox::daemon::store {
|
|
|
|
namespace {
|
|
|
|
// Built-in defaults, one per SettingKey (Settings.schema.json / SettingKey.schema.json).
|
|
// The schema itself carries no "default" keyword anywhere — these are what settings.get
|
|
// falls back to for a key with no stored row, chosen per ADR 0012 where it speaks (the
|
|
// connection.* buffer/segment keys) and otherwise the conservative, least-surprising
|
|
// value for that key's own description. capture.monitoredExtensions defaults to the union
|
|
// of every builtin category's extensions (0001_initial.sql's seed) rather than an
|
|
// arbitrary list of its own, so the two stay in sync without a second place to edit.
|
|
constexpr std::array<std::pair<std::string_view, std::string_view>, 43> kDefaults{{
|
|
{"general.launchOnLogin", "false"},
|
|
{"general.minimizeToTray", "false"},
|
|
{"general.showDropTarget", "true"},
|
|
{"general.confirmOnExit", "true"},
|
|
{"general.language", "\"system\""},
|
|
{"general.checkForUpdates", "true"},
|
|
{"capture.enabled", "true"},
|
|
{"capture.monitoredExtensions",
|
|
"[\"exe\",\"msi\",\"deb\",\"rpm\",\"dmg\",\"appimage\",\"iso\",\"zip\",\"tar\",\"gz\","
|
|
"\"xz\",\"7z\",\"mp4\",\"mkv\",\"webm\",\"avi\",\"mov\",\"flv\",\"m4v\",\"ts\",\"mp3\","
|
|
"\"flac\",\"aac\",\"ogg\",\"opus\",\"wav\",\"m4a\",\"pdf\",\"doc\",\"docx\",\"xls\","
|
|
"\"xlsx\",\"ppt\",\"pptx\",\"odt\",\"epub\",\"jpg\",\"jpeg\",\"png\",\"gif\",\"webp\","
|
|
"\"svg\",\"bmp\",\"tiff\"]"},
|
|
{"capture.monitoredMimeTypes", "[]"},
|
|
{"capture.minSizeBytes", "0"},
|
|
{"capture.excludedHosts", "[]"},
|
|
{"capture.bypassModifier", "\"shift\""},
|
|
{"capture.autoStartTypes", "[]"},
|
|
{"saveTo.defaultDir", "\"~/Downloads\""},
|
|
{"saveTo.tempDir", "\"\""},
|
|
{"saveTo.allowedRoots", "[\"~/Downloads\"]"},
|
|
{"saveTo.fileExistsPolicy", "\"ask\""},
|
|
{"saveTo.createSubfolderPerSite", "false"},
|
|
{"connection.preset", "\"auto\""},
|
|
{"connection.maxSegmentsPerDownload", "8"},
|
|
{"connection.bufferBytes", "1048576"},
|
|
{"connection.maxTotalBufferBytes", "134217728"},
|
|
{"connection.maxActiveSegments", "32"},
|
|
{"connection.maxConcurrentDownloads", "5"},
|
|
{"connection.timeoutSec", "30"},
|
|
{"connection.maxRetries", "10"},
|
|
{"connection.retryBackoffSec", "5"},
|
|
{"downloads.speedLimitBps", "0"},
|
|
{"downloads.speedLimitEnabled", "false"},
|
|
{"downloads.virusScanCommand", "\"\""},
|
|
{"downloads.postDownloadCommand", "\"\""},
|
|
{"downloads.duplicatePolicy", "\"ask\""},
|
|
{"downloads.verifyChecksums", "true"},
|
|
{"proxy.mode", "\"system\""},
|
|
{"proxy.host", "\"\""},
|
|
{"proxy.port", "1"},
|
|
{"proxy.username", "\"\""},
|
|
{"proxy.bypassHosts", "[]"},
|
|
{"proxy.pacUrl", "\"\""},
|
|
{"sounds.enabled", "true"},
|
|
{"sounds.onComplete", "\"\""},
|
|
{"sounds.onQueueComplete", "\"\""},
|
|
{"sounds.onError", "\"\""},
|
|
}};
|
|
|
|
} // namespace
|
|
|
|
std::optional<std::string_view> Settings::default_for(std::string_view key) {
|
|
for (const auto& [k, v] : kDefaults) {
|
|
if (k == key) return v;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
DbResult<std::optional<std::string>> Settings::get_raw(std::string_view key) {
|
|
auto st = db_.prepare("SELECT value FROM settings WHERE key = ?1");
|
|
if (!st) return std::unexpected(st.error());
|
|
if (auto r = st->bind(1, key); !r) return std::unexpected(r.error());
|
|
auto row = st->step();
|
|
if (!row) return std::unexpected(row.error());
|
|
if (*row) return std::optional<std::string>{st->column_text(0)};
|
|
if (auto d = default_for(key)) return std::optional<std::string>{std::string(*d)};
|
|
return std::optional<std::string>{};
|
|
}
|
|
|
|
DbResult<void> Settings::set_raw(std::string_view key, std::string_view json_text) {
|
|
auto st = db_.prepare(
|
|
"INSERT INTO settings(key, value) VALUES(?1, ?2) "
|
|
"ON CONFLICT(key) DO UPDATE SET value = excluded.value");
|
|
if (!st) return std::unexpected(st.error());
|
|
if (auto r = st->bind(1, key); !r) return std::unexpected(r.error());
|
|
if (auto r = st->bind(2, json_text); !r) return std::unexpected(r.error());
|
|
if (auto r = st->step(); !r) return std::unexpected(r.error());
|
|
return {};
|
|
}
|
|
|
|
DbResult<std::map<std::string, std::string>> Settings::overrides() {
|
|
auto st = db_.prepare("SELECT key, value FROM settings ORDER BY key");
|
|
if (!st) return std::unexpected(st.error());
|
|
std::map<std::string, std::string> out;
|
|
for (;;) {
|
|
auto row = st->step();
|
|
if (!row) return std::unexpected(row.error());
|
|
if (!*row) break;
|
|
out.emplace(st->column_text(0), st->column_text(1));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::int64_t Settings::get_int(std::string_view key) {
|
|
auto raw = get_raw(key);
|
|
if (raw && *raw) {
|
|
auto j = nlohmann::json::parse(**raw, nullptr, false);
|
|
if (j.is_number_integer()) return j.get<std::int64_t>();
|
|
}
|
|
if (auto d = default_for(key)) {
|
|
auto j = nlohmann::json::parse(*d, nullptr, false);
|
|
if (j.is_number_integer()) return j.get<std::int64_t>();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool Settings::get_bool(std::string_view key) {
|
|
auto raw = get_raw(key);
|
|
if (raw && *raw) {
|
|
auto j = nlohmann::json::parse(**raw, nullptr, false);
|
|
if (j.is_boolean()) return j.get<bool>();
|
|
}
|
|
if (auto d = default_for(key)) {
|
|
auto j = nlohmann::json::parse(*d, nullptr, false);
|
|
if (j.is_boolean()) return j.get<bool>();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string Settings::get_string(std::string_view key) {
|
|
auto raw = get_raw(key);
|
|
if (raw && *raw) {
|
|
auto j = nlohmann::json::parse(**raw, nullptr, false);
|
|
if (j.is_string()) return j.get<std::string>();
|
|
}
|
|
if (auto d = default_for(key)) {
|
|
auto j = nlohmann::json::parse(*d, nullptr, false);
|
|
if (j.is_string()) return j.get<std::string>();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::vector<std::string> Settings::get_string_array(std::string_view key) {
|
|
auto raw = get_raw(key);
|
|
const std::string text = (raw && *raw) ? **raw
|
|
: default_for(key) ? std::string(*default_for(key))
|
|
: std::string("[]");
|
|
auto j = nlohmann::json::parse(text, nullptr, false);
|
|
std::vector<std::string> out;
|
|
if (j.is_array()) {
|
|
for (const auto& e : j) {
|
|
if (e.is_string()) out.push_back(e.get<std::string>());
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace velox::daemon::store
|