Prevent duplicate desktop model sessions and record calibration evidence

This commit is contained in:
karl committed 2026-10-13 11:43:20 +02:00
1 parent 8498c5401c
commit fb8b45dcc9
7 files changed
+206 -10

No files matched your search

+18
View File
@@ -3814,11 +3814,13 @@ dependencies = [
"searchi-service",
"serde",
"serde_json",
"sha2 0.10.9",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-global-shortcut",
"tauri-plugin-opener",
"tauri-plugin-single-instance",
"tempfile",
"uuid",
"window-vibrancy",
@@ -4586,6 +4588,22 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.21",
"tokio",
"tracing",
"windows-sys 0.61.2",
"zbus",
]
[[package]]
name = "tauri-runtime"
version = "2.12.1"
+2
View File
@@ -22,6 +22,7 @@ tauri-plugin-opener = "2"
tauri-plugin-global-shortcut = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tempfile = "3"
searchi-core = { path = "../../../crates/searchi-core" }
searchi-service = { path = "../../../crates/searchi-service" }
@@ -30,6 +31,7 @@ searchi-media = { path = "../../../crates/searchi-media" }
searchi-ai = { path = "../../../crates/searchi-ai" }
uuid = { version = "1", features = ["v4"] }
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "bmp"] }
tauri-plugin-single-instance = "2"
[target.'cfg(windows)'.dependencies]
window-vibrancy = "=0.8.1"
+78
View File
@@ -0,0 +1,78 @@
//! Official single-instance transport, scoped to an explicitly selected profile.
use sha2::{Digest, Sha256};
use std::{ffi::OsString, io, path::PathBuf};
use tauri::{Emitter, Manager};
pub fn profile_identifier(base: &str, profile: Option<OsString>) -> io::Result<String> {
let Some(profile) = profile else {
return Ok(base.to_owned());
};
let path = crate::data_directory::resolve(PathBuf::new(), Some(profile))?;
// Resolve the existing ancestor without creating a first-use profile.
let canonical = crate::data_directory::canonical_exclusion(&path)?;
let mut digest = Sha256::new();
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
for unit in canonical.as_os_str().encode_wide() {
digest.update(unit.to_le_bytes());
}
}
#[cfg(not(windows))]
{
use std::os::unix::ffi::OsStrExt;
digest.update(canonical.as_os_str().as_bytes());
}
Ok(format!("{base}.profile{:x}", digest.finalize()))
}
pub fn focus_search(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let result = window
.show()
.and_then(|_| window.unminimize())
.and_then(|_| window.set_focus());
if let Err(error) = result {
eprintln!("Could not focus searchi: {error}");
}
let _ = window.emit("focus-search", ());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_identity_is_unchanged_and_invalid_profile_rejected() {
assert_eq!(
profile_identifier("dev.searchi.desktop", None).unwrap(),
"dev.searchi.desktop"
);
assert!(profile_identifier("dev.searchi.desktop", Some("relative".into())).is_err());
}
#[test]
fn canonical_profiles_are_stable_distinct_and_do_not_expose_paths() {
let root = tempfile::tempdir().unwrap();
let first = root.path().join("Gemäß_日本_😀");
let second = root.path().join("other");
let id = profile_identifier("dev.searchi.desktop", Some(first.clone().into_os_string()))
.unwrap();
assert!(!first.exists());
std::fs::create_dir(&first).unwrap();
assert_eq!(
id,
profile_identifier(
"dev.searchi.desktop",
Some(first.join(".").into_os_string())
)
.unwrap()
);
assert_ne!(
id,
profile_identifier("dev.searchi.desktop", Some(second.into_os_string())).unwrap()
);
assert!(!id.contains("Gemäß"));
assert_eq!(id.len(), "dev.searchi.desktop.profile".len() + 64);
assert!(!root.path().join("other").exists());
}
}
+13 -6
View File
@@ -6,6 +6,7 @@ compile_error!("Release desktop builds require --features custom-protocol to emb
mod ai_commands;
mod appearance;
mod data_directory;
mod instance;
mod model_download;
mod semantic_commands;
mod settings;
@@ -545,16 +546,22 @@ fn start_folder_watching(
}
fn main() {
let mut context = tauri::generate_context!();
context.config_mut().identifier = instance::profile_identifier(
&context.config().identifier,
std::env::var_os("SEARCHI_DATA_DIR"),
)
.expect("searchi profile identity could not be resolved");
tauri::Builder::default()
// Detect duplicates before any model, database, watcher or other plugin starts.
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
instance::focus_search(app);
}))
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().with_handler(|app, _, event| {
if event.state == ShortcutState::Pressed {
if let Some(window) = app.get_webview_window("main") {
let result = window.show().and_then(|_| window.unminimize()).and_then(|_| window.set_focus());
if let Err(error) = result { eprintln!("Could not focus Searchi: {error}"); }
let _ = window.emit("focus-search", ());
}
instance::focus_search(app);
}
}).build())
.setup(|app| {
@@ -687,7 +694,7 @@ fn main() {
hybrid_search,
hybrid_search_measured
])
.build(tauri::generate_context!())
.build(context)
.expect("Searchi desktop could not start")
.run(|app, event| {
if matches!(event, tauri::RunEvent::Exit) {