Watch selected folders and reconcile bounded changes with media freshness guards
This commit is contained in:
1 parent
99748bd99f
commit
657de3de86
22 files changed
+2022
-149
No files matched your search
@@ -8,6 +8,7 @@ mod data_directory;
|
||||
mod model_download;
|
||||
mod semantic_commands;
|
||||
mod settings;
|
||||
mod watching;
|
||||
|
||||
use ai_commands::{generate_ai, prepare_ai_source, AiSession};
|
||||
use searchi_core::metrics::{MetricsMode, SearchMetrics, SearchResponse, TraceCollector};
|
||||
@@ -38,6 +39,8 @@ struct AppState {
|
||||
settings: Arc<Mutex<SettingsStore>>,
|
||||
shortcut_status: String,
|
||||
semantic: Arc<SemanticState>,
|
||||
watching: Arc<watching::WatchService>,
|
||||
index_options: searchi_core::IndexOptions,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -85,6 +88,7 @@ struct DesktopStatus {
|
||||
settings: Settings,
|
||||
indexing: bool,
|
||||
shortcut_status: String,
|
||||
watching: Vec<watching::WatchStatus>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -98,6 +102,7 @@ fn desktop_status(state: State<'_, AppState>) -> Result<DesktopStatus, String> {
|
||||
.clone(),
|
||||
indexing: state.indexing.load(Ordering::Acquire),
|
||||
shortcut_status: state.shortcut_status.clone(),
|
||||
watching: state.watching.status(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -362,15 +367,18 @@ async fn index_folder(root: String, state: State<'_, AppState>) -> Result<IndexR
|
||||
let database = state.database.clone();
|
||||
let settings = state.settings.clone();
|
||||
let lexical = state.lexical.clone();
|
||||
let watching = state.watching.clone();
|
||||
let options = state.index_options.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let _guard = guard;
|
||||
settings
|
||||
.lock()
|
||||
.map_err(|_| "Settings are unavailable.")?
|
||||
.add_root(std::path::Path::new(&root))?;
|
||||
watching.add_root(std::fs::canonicalize(&root).map_err(|error| error.to_string())?)?;
|
||||
let mut index = SearchIndex::open(database).map_err(|error| error.to_string())?;
|
||||
let report = index
|
||||
.index_folder(root)
|
||||
.index_folder_with_options(root, &options)
|
||||
.map_err(|error| error.to_string())?;
|
||||
index.prewarm().map_err(|error| error.to_string())?;
|
||||
*lexical
|
||||
@@ -382,6 +390,66 @@ async fn index_folder(root: String, state: State<'_, AppState>) -> Result<IndexR
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
/// Each reconciliation owns its connection; retained readers stay usable while
|
||||
/// extraction runs. The watcher retains dirty work when the shared gate is busy.
|
||||
fn start_folder_watching(
|
||||
app: tauri::AppHandle,
|
||||
database: PathBuf,
|
||||
lexical: Arc<Mutex<SearchIndex>>,
|
||||
indexing: Arc<AtomicBool>,
|
||||
roots: Vec<PathBuf>,
|
||||
options: searchi_core::IndexOptions,
|
||||
) -> Result<watching::WatchService, String> {
|
||||
watching::WatchService::start(roots, options.excluded_paths.clone(), move |request| {
|
||||
if indexing
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_err()
|
||||
{
|
||||
return watching::ScanOutcome::Busy;
|
||||
}
|
||||
let guard = IndexGuard(indexing.clone());
|
||||
let _ = app.emit("folder-indexing-changed", ());
|
||||
let result = (|| -> Result<bool, String> {
|
||||
let mut index = SearchIndex::open(&database).map_err(|error| error.to_string())?;
|
||||
let report = match request.paths {
|
||||
Some(paths) => index.index_paths_cancellable(
|
||||
&request.root,
|
||||
&paths,
|
||||
&options,
|
||||
&request.cancelled,
|
||||
),
|
||||
None if request.media_hint => index.index_paths_cancellable(
|
||||
&request.root,
|
||||
std::slice::from_ref(&request.root),
|
||||
&options,
|
||||
&request.cancelled,
|
||||
),
|
||||
None => index.index_folder_cancellable(&request.root, &options, &request.cancelled),
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
let changed = report.indexed > 0 || report.removed > 0;
|
||||
if changed && !request.cancelled.load(Ordering::Acquire) {
|
||||
index.prewarm().map_err(|error| error.to_string())?;
|
||||
*lexical
|
||||
.lock()
|
||||
.map_err(|_| "Keyword engine is unavailable.")? = index;
|
||||
}
|
||||
Ok(changed)
|
||||
})();
|
||||
drop(guard);
|
||||
let _ = app.emit("folder-indexing-changed", ());
|
||||
match result {
|
||||
Ok(changed) => {
|
||||
if changed {
|
||||
let _ = app.emit("search-index-changed", ());
|
||||
}
|
||||
watching::ScanOutcome::Completed
|
||||
}
|
||||
Err(error) => watching::ScanOutcome::Failed(error),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -455,7 +523,25 @@ fn main() {
|
||||
"The media decoder is missing from this installation. Repair or reinstall searchi.".into()
|
||||
})
|
||||
})();
|
||||
let semantic = Arc::new(SemanticState::new(model_directory, worker, media_tools));
|
||||
let mut ignored = vec![directory.clone(), model_directory.clone()];
|
||||
if let Some(parent) = worker.parent().filter(|path| path.is_absolute()) {
|
||||
ignored.push(parent.to_path_buf());
|
||||
}
|
||||
let index_options = searchi_core::IndexOptions {
|
||||
excluded_paths: ignored.iter()
|
||||
.map(|path| data_directory::canonical_exclusion(path))
|
||||
.collect::<std::io::Result<Vec<_>>>()?,
|
||||
..Default::default()
|
||||
};
|
||||
let indexing = Arc::new(AtomicBool::new(false));
|
||||
let watching = Arc::new(start_folder_watching(
|
||||
app.handle().clone(), database.clone(), lexical.clone(), indexing.clone(),
|
||||
settings.value.roots.clone(), index_options.clone(),
|
||||
).map_err(std::io::Error::other)?);
|
||||
let semantic = Arc::new(
|
||||
SemanticState::new(model_directory, worker, media_tools)
|
||||
.with_media_scratch(directory.join("MediaScratch")),
|
||||
);
|
||||
semantic.begin_automatic_setup(database.clone(), settings.value.cpu_threads);
|
||||
#[cfg(target_os = "macos")]
|
||||
let shortcut = "Shift+Super+Space";
|
||||
@@ -475,10 +561,12 @@ fn main() {
|
||||
ai: Arc::new(AiSession::default()),
|
||||
database,
|
||||
lexical,
|
||||
indexing: Arc::new(AtomicBool::new(false)),
|
||||
indexing,
|
||||
settings: Arc::new(Mutex::new(settings)),
|
||||
shortcut_status,
|
||||
semantic,
|
||||
watching,
|
||||
index_options,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
@@ -500,8 +588,15 @@ fn main() {
|
||||
hybrid_search,
|
||||
hybrid_search_measured
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("Searchi desktop could not start");
|
||||
.build(tauri::generate_context!())
|
||||
.expect("Searchi desktop could not start")
|
||||
.run(|app, event| {
|
||||
if matches!(event, tauri::RunEvent::Exit) {
|
||||
// Release the worker-held AppHandle and owned watcher before
|
||||
// application state is torn down.
|
||||
app.state::<AppState>().watching.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user