LRC-LIB-CATALOG-SWITCH: the launch arguments' library checks share one deadline

Each argument's check had its own 2 s, so several paths on a dead mount could hold the window back
2 s each. `launch_imports_in` now takes one `launch_deadline()` for the whole batch and checks each
argument with `launch_path_until`; once it has passed, an argument is imported without the check.
Test: a past deadline imports a library folder at once, without probing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OneSlips
2026-10-29 13:42:33 +10:00
co-authored by Claude Opus 5.5
parent d627deabf4
commit da65a3f118
3 changed files with 49 additions and 8 deletions
+4 -2
View File
@@ -62,10 +62,12 @@ pub(crate) struct LaunchArgs {
}
pub(crate) fn launch_imports_in(args: &[String], cwd: Option<&std::path::Path>, home: Option<&std::path::Path>) -> LaunchArgs {
use lightcraft_engine::import::{LaunchPath, launch_path};
use lightcraft_engine::import::{LaunchPath, launch_deadline, launch_path_until};
let (mut files, mut notices, mut library) = (Vec::new(), Vec::new(), None);
// one deadline for every argument's library check: they all run before the window opens
let deadline = launch_deadline();
for a in args {
match launch_path(a, cwd, home, lightcraft_engine::devices::dcim_in) {
match launch_path_until(a, cwd, home, lightcraft_engine::devices::dcim_in, deadline) {
Some(LaunchPath::Library(dir)) if library.is_none() => library = Some(std::path::PathBuf::from(dir)),
Some(LaunchPath::Library(dir)) => {
log::warn!("one library at a time: not opening {dir} given at launch");
+3 -1
View File
@@ -43,7 +43,9 @@ mod types;
pub(crate) use commit::workers;
pub use commit::{batch_size, civil, commit_prepared, system_clock};
pub use defaults::{CameraDefault, ImportDefaults, apply_import_defaults, has_import_look, import_defaults};
pub use files::{EXTENSIONS, LaunchPath, accept_list, expand, expand_within, is_supported, launch_path};
pub use files::{
EXTENSIONS, LAUNCH_PROBE_LIMIT, LaunchPath, accept_list, expand, expand_within, is_supported, launch_deadline, launch_path, launch_path_until,
};
pub(crate) use files::{expand_noting, probe_all, probe_paths, probe_with};
#[cfg(test)]
use job::PreparedItem;
+42 -5
View File
@@ -137,10 +137,16 @@ pub fn library_dir_of(path: &Path) -> Option<PathBuf> {
(path.is_dir() && crate::library::is_library(path)).then(|| path.to_path_buf())
}
/// How long a launch argument's library check may take before LightCraft gives up on it and treats the argument
/// as photos to import (in the background). A path on a dead network mount can block a file-system query for
/// minutes, and this check runs before the window opens.
pub(crate) const LAUNCH_PROBE_LIMIT: std::time::Duration = std::time::Duration::from_secs(2);
/// How long the launch arguments' library checks may take, all of them together, before LightCraft gives up and
/// treats the arguments not yet checked as photos to import (in the background). A path on a dead network mount
/// can block a file-system query for minutes, and these checks run before the window opens.
pub const LAUNCH_PROBE_LIMIT: std::time::Duration = std::time::Duration::from_secs(2);
/// When the launch arguments' library checks, starting now, must be done: [`LAUNCH_PROBE_LIMIT`] from now.
pub fn launch_deadline() -> std::time::Instant {
let now = std::time::Instant::now();
now.checked_add(LAUNCH_PROBE_LIMIT).unwrap_or(now)
}
/// `probe`'s answer, or `None` when it takes longer than `limit`: it runs on its own thread, which is left to
/// finish (or stay blocked) on its own. Also `None` when no thread can be started.
@@ -169,6 +175,19 @@ pub(crate) fn within<T: Send + 'static>(limit: std::time::Duration, probe: impl
/// or file-system root is replaced by its camera folder (`DCIM`, from `dcim`: a memory card) or
/// refused, and so is the home folder `home`: either would walk most of the disk.
pub fn launch_path(arg: &str, cwd: Option<&Path>, home: Option<&Path>, dcim: impl Fn(&Path) -> Option<PathBuf>) -> Option<LaunchPath> {
launch_path_until(arg, cwd, home, dcim, launch_deadline())
}
/// [`launch_path`], with its library check bounded by `deadline` (from [`launch_deadline`]), which every argument
/// of one launch shares: several paths on a dead mount wait [`LAUNCH_PROBE_LIMIT`] in all, not each. Once it has
/// passed, an argument is imported without the check.
pub fn launch_path_until(
arg: &str,
cwd: Option<&Path>,
home: Option<&Path>,
dcim: impl Fn(&Path) -> Option<PathBuf>,
deadline: std::time::Instant,
) -> Option<LaunchPath> {
if arg.trim().is_empty() {
return None;
}
@@ -203,8 +222,11 @@ pub fn launch_path(arg: &str, cwd: Option<&Path>, home: Option<&Path>, dcim: imp
if home.is_some_and(|h| key(&h.to_string_lossy()) == key(&shown)) {
return Some(LaunchPath::Refused { path: shown, why: "the whole home folder".into() });
}
let left = deadline.saturating_duration_since(std::time::Instant::now());
let probed = abs.clone();
if let Some(dir) = within(LAUNCH_PROBE_LIMIT, move || library_dir_of(&probed)) {
if !left.is_zero()
&& let Some(dir) = within(left, move || library_dir_of(&probed))
{
return Some(LaunchPath::Library(dir.to_string_lossy().to_string()));
}
Some(LaunchPath::Import(shown))
@@ -345,6 +367,21 @@ mod prepared_tests {
assert_eq!(within(LAUNCH_PROBE_LIMIT, || Some(1)), Some(1), "a quick answer is kept");
}
/// The launch arguments share one deadline (CodeRabbit on #2979): once it has passed, an argument is imported
/// without its library check, so several paths on a dead mount can't each wait the limit.
#[test]
fn launch_arguments_share_one_deadline() {
let dir = temp_dir("deadline");
std::fs::write(dir.join(lightcraft_catalog::journal::LOG), b"x").unwrap();
let arg = dir.to_string_lossy().to_string();
let none = |_: &Path| None;
assert_eq!(launch_path_until(&arg, None, None, none, launch_deadline()), Some(LaunchPath::Library(arg.clone())));
let start = std::time::Instant::now();
assert_eq!(launch_path_until(&arg, None, None, none, start), Some(LaunchPath::Import(arg.clone())), "deadline passed");
assert!(start.elapsed() < std::time::Duration::from_millis(500), "waited {:?}", start.elapsed());
let _ = std::fs::remove_dir_all(dir);
}
/// Extensions the picker may offer: every one in [`EXTENSIONS`], minus the
/// files this build cannot import (see [`accept_list`]).
fn pickable_extensions() -> Vec<&'static str> {