Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions shell/src-tauri/src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ fn reap(child: &mut Child) -> Option<i32> {

/// `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<Launch>,
running: Arc<Mutex<Option<Searching>>>,
next_id: AtomicU64,
on_event: EventSink,
Expand All @@ -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<String>) {
self.launch.lock().unwrap().args = args;
}

/// Sends a request and waits for its reply.
pub async fn ask(&self, mut request: Value) -> Result<Value, String> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
Expand Down Expand Up @@ -171,7 +181,12 @@ impl SearchService {
}

fn start(&self) -> Result<Searching, String> {
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<Mutex<HashMap<u64, oneshot::Sender<Value>>>> = Arc::default();
let (replies, on_event, slot) = (pending.clone(), self.on_event.clone(), self.running.clone());
Expand Down
81 changes: 75 additions & 6 deletions shell/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,62 @@ fn folder_arguments(settings: &Settings, code_folders: &[String]) -> Vec<String>
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<String> {
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`.
Expand Down Expand Up @@ -693,10 +749,7 @@ fn setup(app: &mut tauri::App, options: Options) -> Result<(), Box<dyn std::erro
} else {
options.code_roots.iter().map(|root| root.display().to_string()).collect()
};
let mut search_args = vec!["search-service".to_string()];
search_args.extend(options.index.iter().flat_map(|index| ["--index".to_string(), index.display().to_string()]));
search_args.extend(folder_arguments(&settings, &code_folders));
search_args.extend(roots);
let search_args = search_arguments(options.index.as_deref(), &settings, &code_folders, &roots);
let search = SearchService::new(
Launch {
program: options.helper.clone(),
Expand Down Expand Up @@ -855,12 +908,23 @@ fn apply_folders(app: &AppHandle) {
for folder in &folders {
let _ = app.asset_protocol_scope().allow_directory(folder, true);
}
let request = folders_request(&shell);
send_folders_request(&shell);
watch_folders(app);
}

/// Tell the running search helper the current folders, and save the same list as the launch
/// arguments of the next helper to start: the running one takes the request; the one that starts
/// after an idle exit takes the arguments — which used to stay the app's startup list, losing
/// folders added during the session from its scope, its summaries and the search filter (#170).
fn send_folders_request(shell: &Arc<Shell>) {
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).
Expand Down Expand Up @@ -1416,6 +1480,11 @@ fn turn_off_code(app: AppHandle, shell: State<'_, Arc<Shell>>) -> 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;
Expand Down