feat(qconnect): persist device_uuid in qconnect_settings.db

resolve_qconnect_device_uuid() generated a fresh Uuid::new_v4() per
process via a OnceLock, so every launch registered a new renderer
identity cloud-side (zombie renderers) and broke reconnect continuity.
Persist it beside device_name in qconnect_settings.db: generate once on
first run, reuse thereafter, keeping the QBZ_QCONNECT_DEVICE_UUID env
override for tests. Mirrors load_persisted_device_name/persist_device_name.

cc
This commit is contained in:
vicrodh committed 2026-06-01 02:44:14 -06:00
1 parent 653ded1e72
commit c32bbc5db3
2 files changed
+85 -7

No files matched your search

+27
View File
@@ -1053,3 +1053,30 @@ fn resolves_remote_previous_to_prior_item_even_mid_track() {
}
);
}
#[test]
fn device_uuid_env_override_takes_precedence() {
// SAFETY: single-threaded test process for this var; restore after.
std::env::set_var("QBZ_QCONNECT_DEVICE_UUID", "env-override-uuid-123");
let uuid = super::transport::resolve_qconnect_device_uuid();
std::env::remove_var("QBZ_QCONNECT_DEVICE_UUID");
assert_eq!(uuid, "env-override-uuid-123");
}
#[test]
fn device_uuid_persists_and_is_reused_across_calls() {
let tmp = std::env::temp_dir().join(format!(
"qbz_qconnect_uuid_test_{}.db",
std::process::id()
));
let _ = std::fs::remove_file(&tmp);
// First call generates and persists.
let first = super::transport::device_uuid_from_db(&tmp);
assert!(!first.trim().is_empty(), "generated uuid must be non-empty");
// Second call must return the SAME value (read back from disk, not a fresh v4).
let second = super::transport::device_uuid_from_db(&tmp);
assert_eq!(first, second, "device_uuid must be stable across calls");
let _ = std::fs::remove_file(&tmp);
}
+58 -7
View File
@@ -4,8 +4,6 @@
//! handshake, hex helpers used for inbound payload diagnostics, and the
//! once-initialized device UUID.
use std::sync::OnceLock;
use qconnect_transport_ws::WsTransportConfig;
use serde_json::Value;
use uuid::Uuid;
@@ -26,8 +24,6 @@ const VOLUME_REMOTE_CONTROL_ALLOWED: i32 = 2;
const QCONNECT_QWS_TOKEN_KIND: &str = "jwt_qws";
const QCONNECT_QWS_CREATE_TOKEN_PATH: &str = "/qws/createToken";
static QCONNECT_DEVICE_UUID: OnceLock<String> = OnceLock::new();
pub(super) fn default_qconnect_device_info() -> QconnectDeviceInfoPayload {
default_qconnect_device_info_with_name(None)
}
@@ -91,9 +87,64 @@ pub(super) fn resolve_qconnect_device_uuid() -> String {
return explicit;
}
QCONNECT_DEVICE_UUID
.get_or_init(|| Uuid::new_v4().to_string())
.clone()
match qconnect_settings_db_path() {
Some(path) => device_uuid_from_db(&path),
// Fail-open: if we cannot resolve a settings path, fall back to a
// fresh v4 (same behavior as before persistence existed). This keeps
// QConnect functional on exotic systems without a data dir.
None => Uuid::new_v4().to_string(),
}
}
/// Load the persisted device_uuid from `path`, generating + persisting one on
/// first run. Mirrors `load_persisted_device_name`/`persist_device_name`. Split
/// out so the persistence round-trip is unit-testable against a temp path.
pub(super) fn device_uuid_from_db(path: &std::path::Path) -> String {
if let Some(existing) = load_persisted_device_uuid(path) {
return existing;
}
let generated = Uuid::new_v4().to_string();
persist_device_uuid(path, &generated);
generated
}
/// Load the persisted device_uuid. Returns None if not set or on any error.
fn load_persisted_device_uuid(path: &std::path::Path) -> Option<String> {
let conn = rusqlite::Connection::open(path).ok()?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
.ok()?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)",
)
.ok()?;
conn.query_row(
"SELECT value FROM settings WHERE key = 'device_uuid'",
[],
|row| row.get::<_, String>(0),
)
.ok()
.filter(|v| !v.trim().is_empty())
}
/// Persist the device_uuid to disk (INSERT OR REPLACE).
fn persist_device_uuid(path: &std::path::Path, uuid: &str) {
let Ok(conn) = rusqlite::Connection::open(path) else {
return;
};
let _ = conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;");
let _ = conn.execute_batch(
"CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)",
);
let _ = conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('device_uuid', ?1)",
rusqlite::params![uuid],
);
}
pub(super) fn resolve_system_hostname() -> String {