From ef387a3caddaf3cb197d532f13665aa302bd260d Mon Sep 17 00:00:00 2001 From: Ocean572 <45051223+Ocean572@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:07:32 -0600 Subject: [PATCH] Shell: rebuild the search helper's launch arguments when folders change, so a restarted helper keeps them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows/Linux shell built the search helper's launch arguments once at setup and replayed them every time the helper started. A folder added during the session reaches a *running* helper through the live `folders` request, so it works — but when that helper exits by itself (idle) and the shell starts a new one from the saved arguments, the added folder is gone: the helper searches only the startup folders, so Settings rows stop showing `X of Y indexed` for it and its files drop out of search results (issue #170, Windows 11, 0.7.0-beta.3; local folders added after startup show the same mechanism). The arguments are now a factory, `search_arguments()`, and the running set is saved in the `SearchService` (`launch: Mutex`, `set_launch_args`). `apply_folders` — called on every folder change — and `turn_off_code` — which must not let a restarted helper reopen the code index the person just closed — both rebuild the saved list before sending the live request, so the next helper starts with exactly what Settings shows. A pre-push review folded in one detail: `start()` chained `launch.lock().unwrap().clone()` straight onto `.spawn()`, and a MutexGuard in a statement lives to the statement's end — the launch mutex stayed held across fork/exec (hundreds of ms under Windows AV scanning), stalling `set_launch_args` exactly while a helper is being born. The snapshot is a `let` now, and spawn runs without the lock; the swap stays atomic (a spawn sees all-old or all-new arguments, never a half-written list). Redundant clones in the unit tests went too. Known adjacent staleness, out of scope here: the saved launch env carries `DIGUP_MODELS` only, never `DIGUP_DEVICE` — `set_processor_only` relies on it for the index helper only, which rebuilds its env fresh; the search helper behaved identically before this change. Verified on this box: cargo test 15 green, including two new unit tests that check the rebuilt argument list carries the current folders, their exclusions, the code folders, and the index (and drops the index flag when there is none). End to end against the Swift helper: after `op:folders` grows 1 root to 3, a restart with startup args shows the reporter's symptom (summaries: root1 only), a restart with rebuilt args restores all three (root1:2, root2:1, root3:1 indexed). Swift suite 125 green (no Swift changes), shell JS tests 17 pass. GUI acceptance on Windows is the reporter/maintainer's call — this box runs only the shell's Rust unit tests, not the Tauri app. --- shell/src-tauri/src/helper.rs | 21 +++++++-- shell/src-tauri/src/main.rs | 81 ++++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 9 deletions(-) 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;