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:
2 files changed
+85
-7
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user