diff --git a/shell/src-tauri/src/helper.rs b/shell/src-tauri/src/helper.rs index fed4d159..967d91b2 100644 --- a/shell/src-tauri/src/helper.rs +++ b/shell/src-tauri/src/helper.rs @@ -113,7 +113,10 @@ fn reap(child: &mut Child) -> Option { /// `digup search-service`: started on the first request, and again after it died. pub struct SearchService { - launch: Launch, + /// How to start it. `setup` makes the arguments from the settings at startup; `apply_folders` + /// rebuilds them when the folders change, so a helper that starts after an idle exit searches + /// what Settings shows, not what was current when the app launched (issue #170). + launch: Mutex, running: Arc>>, next_id: AtomicU64, on_event: EventSink, @@ -130,13 +133,20 @@ struct Searching { impl SearchService { pub fn new(launch: Launch, on_event: impl Fn(Value) + Send + Sync + 'static) -> SearchService { SearchService { - launch, + launch: Mutex::new(launch), running: Arc::default(), next_id: AtomicU64::new(1), on_event: Arc::new(on_event), } } + /// The arguments for the next helper to start (folders, exclusions, code folders): kept current + /// by `apply_folders`. A running helper gets the live `folders` request; this is for the one + /// that starts when the running one exits by itself (issue #170). + pub fn set_launch_args(&self, args: Vec) { + self.launch.lock().unwrap().args = args; + } + /// Sends a request and waits for its reply. pub async fn ask(&self, mut request: Value) -> Result { let id = self.next_id.fetch_add(1, Ordering::Relaxed); @@ -171,7 +181,12 @@ impl SearchService { } fn start(&self) -> Result { - let (child, stdin, stdout) = self.launch.spawn()?; + // Snapshot the arguments, then spawn outside the lock: the mutex guard of a chained + // `.clone().spawn()` lives to the end of the statement, which would hold the launch + // mutex across fork/exec (hundreds of ms with Windows AV scanning) and stall + // `set_launch_args` while a helper is being born. + let launch = self.launch.lock().unwrap().clone(); + let (child, stdin, stdout) = launch.spawn()?; let number = STARTED.fetch_add(1, Ordering::Relaxed); let pending: Arc>>> = Arc::default(); let (replies, on_event, slot) = (pending.clone(), self.on_event.clone(), self.running.clone()); diff --git a/shell/src-tauri/src/main.rs b/shell/src-tauri/src/main.rs index 375fbee2..56ad1c65 100644 --- a/shell/src-tauri/src/main.rs +++ b/shell/src-tauri/src/main.rs @@ -253,6 +253,62 @@ fn folder_arguments(settings: &Settings, code_folders: &[String]) -> Vec args } +/// The search helper's launch arguments: the index, the current folders' exclusions and code +/// folders, and the folders themselves last. `setup` makes them from the settings at startup, and +/// `apply_folders` rebuilds them on every change, so a helper starting after an idle exit searches +/// what Settings shows — not what was current when the app launched (issue #170: the saved startup +/// list was replayed, and folders added during the session dropped out of the helper's scope, its +/// summaries, and the search result filter). +fn search_arguments(index: Option<&Path>, settings: &Settings, code_folders: &[String], roots: &[String]) -> Vec { + let mut args = vec!["search-service".to_string()]; + if let Some(index) = index { + args.extend(["--index".to_string(), index.display().to_string()]); + } + args.extend(folder_arguments(settings, code_folders)); + args.extend(roots.iter().cloned()); + args +} + +#[cfg(test)] +mod search_launch_tests { + use super::*; + + fn settings() -> Settings { + Settings { + folders: vec!["C:\\Initial".into(), "E:\\AddedExternal".into()], + skipped: vec!["C:\\Initial\\Secret".into()], + skip_types: vec!["tmp".into()], + code_folders: vec!["D:\\code".into()], + code_skipped: vec!["D:\\code\\vendor".into()], + ..Settings::default() + } + } + + /// The exact shape #170 lost: a helper restarted from saved arguments must carry the folders + /// added during the session, their exclusions, and the code folders — the same list setup + /// would build if the app started now. + #[test] + fn search_arguments_carry_the_current_folders() { + let s = settings(); + let args = search_arguments(Some(Path::new("C:\\digup-index")), &s, &s.code_folders, &s.folders); + let joined = args.join("|"); + assert_eq!(args.first().unwrap(), "search-service"); + assert!(joined.contains("--index|C:\\digup-index"), "{joined}"); + assert!(joined.contains("--exclude|C:\\Initial\\Secret"), "{joined}"); + assert!(joined.contains("--exclude-ext|tmp"), "{joined}"); + assert!(joined.contains("--code-root|D:\\code"), "{joined}"); + assert!(joined.contains("--code-exclude|D:\\code\\vendor"), "{joined}"); + assert_eq!(args[args.len() - 2..], ["C:\\Initial", "E:\\AddedExternal"]); + } + + #[test] + fn search_arguments_without_an_index_folder() { + let s = Settings { folders: vec!["/home/me/Pictures".into()], ..Settings::default() }; + let args = search_arguments(None, &s, &[], &s.folders); + assert_eq!(args, ["search-service", "/home/me/Pictures"]); + } +} + // MARK: Commands /// The panel's and the window's questions for `search-service`. @@ -693,10 +749,7 @@ fn setup(app: &mut tauri::App, options: Options) -> Result<(), Box) { + let (roots, code) = (shell.folders(), shell.code_folders()); + let settings = shell.settings.lock().unwrap().clone(); + shell.search.set_launch_args(search_arguments(shell.options.index.as_deref(), &settings, &code, &roots)); + let request = folders_request(shell); let search = shell.clone(); tauri::async_runtime::spawn(async move { let _ = search.search.ask(request).await; }); - watch_folders(app); } /// The search service's "folders": what it searches (without code folders, it closes the code index). @@ -1416,6 +1480,11 @@ fn turn_off_code(app: AppHandle, shell: State<'_, Arc>) -> Result<(), Str settings.save(&shell.settings_path).map_err(|error| format!("can't save the settings: {error}"))?; } let shell = shell.inner().clone(); + // The saved launch arguments follow the cleared code folders too: a restarted helper must + // not reopen the code index the person just turned off (#170's mechanism, same staleness). + let (roots, code) = (shell.folders(), shell.code_folders()); + let settings = shell.settings.lock().unwrap().clone(); + shell.search.set_launch_args(search_arguments(shell.options.index.as_deref(), &settings, &code, &roots)); let request = folders_request(&shell); tauri::async_runtime::spawn(async move { let _ = shell.search.ask(request).await;