1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
3use std::time::{Duration, Instant};
4
5use crate::dbs::lmdb::{LmdbStore, spawn_lmdb_gc};
6use crate::error::Error;
7use crate::file_picker::FilePicker;
8use crate::frecency::FrecencyTracker;
9use crate::git::GitStatusCache;
10use crate::query_tracker::QueryTracker;
11use crate::rescan_stats::{RescanCounters, RescanReason, RescanStats};
12use crate::rescan_throttle::RescanThrottle;
13use crate::scan::ScanJob;
14use crate::watch::{WatchEvent, WatchId, WatchOptions, WatchRegistry};
15use git2::Repository;
16
17fn wait_for_git_index_lock_release(git_root: &Path) {
24 const GIT_LOCK_POLL: Duration = Duration::from_millis(10);
25 const GIT_LOCK_MAX_WAIT: Duration = Duration::from_millis(500);
26
27 let lock = git_root.join(".git").join("index.lock");
28 if !lock.exists() {
30 return;
31 }
32 let deadline = Instant::now() + GIT_LOCK_MAX_WAIT;
33 while lock.exists() && Instant::now() < deadline {
34 std::thread::sleep(GIT_LOCK_POLL);
35 }
36 if lock.exists() {
37 tracing::warn!(
38 "Proceeding with git status refresh despite lingering \
39 .git/index.lock at {} — will retry once it clears",
40 lock.display()
41 );
42 }
43}
44
45fn poll_until(timeout: Duration, mut done: impl FnMut() -> bool) -> bool {
48 let start = Instant::now();
49 while !done() {
50 if start.elapsed() >= timeout {
51 return false;
52 }
53 std::thread::sleep(Duration::from_millis(10));
54 }
55 true
56}
57
58#[derive(Clone, Default)]
75pub struct SharedFilePicker(pub(crate) Arc<SharedPickerInner>);
76
77pub struct SharedPickerInner {
78 picker: parking_lot::RwLock<Option<FilePicker>>,
79 watchers: Arc<WatchRegistry>,
82 rescans: RescanCounters,
83 rescan_throttle: RescanThrottle,
84}
85
86impl Default for SharedPickerInner {
87 fn default() -> Self {
88 Self {
89 picker: parking_lot::RwLock::new(None),
90 watchers: Arc::new(WatchRegistry::default()),
91 rescans: RescanCounters::default(),
92 rescan_throttle: RescanThrottle::default(),
93 }
94 }
95}
96
97#[derive(Clone)]
99pub(crate) struct WeakFilePicker(Weak<SharedPickerInner>);
100
101impl WeakFilePicker {
102 pub(crate) fn upgrade(&self) -> Option<SharedFilePicker> {
108 self.0.upgrade().map(SharedFilePicker)
109 }
110}
111
112impl std::fmt::Debug for SharedFilePicker {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.debug_tuple("SharedPicker").field(&"..").finish()
115 }
116}
117
118impl SharedFilePicker {
119 pub fn read(&self) -> Result<parking_lot::RwLockReadGuard<'_, Option<FilePicker>>, Error> {
120 Ok(self.0.picker.read())
121 }
122
123 pub fn write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, Option<FilePicker>>, Error> {
124 Ok(self.0.picker.write())
125 }
126
127 pub fn cancel(&self) {
130 if let Ok(guard) = self.read()
131 && let Some(picker) = guard.as_ref()
132 {
133 picker.cancel();
134 }
135 }
136
137 pub(crate) fn weaken(&self) -> WeakFilePicker {
140 WeakFilePicker(Arc::downgrade(&self.0))
141 }
142
143 pub fn need_complex_rebuild(&self) -> bool {
146 let guard = self.0.picker.read();
147 guard
148 .as_ref()
149 .is_some_and(|p| p.has_mmap_cache() || p.has_content_indexing())
150 }
151
152 pub fn wait_for_scan(&self, timeout: Duration) -> bool {
155 let signal = {
156 let guard = self.0.picker.read();
157 match &*guard {
158 Some(picker) => Arc::clone(&picker.signals.scanning),
159 None => return true,
160 }
161 };
162
163 poll_until(timeout, || {
164 !signal.load(std::sync::atomic::Ordering::Acquire)
165 })
166 }
167
168 pub fn wait_for_watcher(&self, timeout: Duration) -> bool {
171 let watch_ready_signal = {
172 let guard = self.0.picker.read();
173 match &*guard {
174 Some(picker) => Arc::clone(&picker.signals.watcher_ready),
175 None => return true,
176 }
177 };
178
179 poll_until(timeout, || {
180 watch_ready_signal.load(std::sync::atomic::Ordering::Acquire)
181 })
182 }
183
184 pub fn wait_for_indexing_complete(&self, timeout: Duration) -> bool {
187 let (scanning, post_scan_active) = {
188 let guard = self.0.picker.read();
189 match &*guard {
190 Some(picker) => (
191 Arc::clone(&picker.signals.scanning),
192 Arc::clone(&picker.signals.post_scan_indexing_active),
193 ),
194 None => return true,
195 }
196 };
197
198 poll_until(timeout, || {
199 !scanning.load(std::sync::atomic::Ordering::Acquire)
200 && !post_scan_active.load(std::sync::atomic::Ordering::Acquire)
201 })
202 }
203
204 pub fn trigger_full_rescan_async(&self, shared_frecency: &SharedFrecency) -> Result<(), Error> {
208 self.trigger_full_rescan_with_reason(shared_frecency, RescanReason::Explicit)
209 .map(|_| ())
210 }
211
212 pub fn rescan_stats(&self) -> RescanStats {
215 self.0.rescans.snapshot()
216 }
217
218 pub fn reset_rescan_stats(&self) {
219 self.0.rescans.reset();
220 }
221
222 pub(crate) fn trigger_full_rescan_with_reason(
226 &self,
227 shared_frecency: &SharedFrecency,
228 reason: RescanReason,
229 ) -> Result<bool, Error> {
230 if reason == RescanReason::Explicit {
234 self.0.rescan_throttle.note_explicit_scan();
235 } else if !self.check_rescan_throttle(reason) {
236 return Ok(false);
237 }
238
239 self.0.rescans.record(reason);
240
241 match ScanJob::new_rescan(self, shared_frecency)? {
242 Some(job) => {
243 job.spawn();
244 }
245 None => {
246 if let Ok(guard) = self.read()
248 && let Some(picker) = guard.as_ref()
249 {
250 picker
251 .scan_signals()
252 .rescan_pending
253 .store(true, std::sync::atomic::Ordering::Release);
254 tracing::info!(
255 "Full rescan requested while another scan is active — \
256 deferred via rescan_pending flag"
257 );
258 }
259 }
260 }
261 Ok(true)
262 }
263
264 fn check_rescan_throttle(&self, reason: RescanReason) -> bool {
265 let (live_files, has_git) = self
266 .read()
267 .ok()
268 .and_then(|guard| {
269 guard
270 .as_ref()
271 .map(|picker| (picker.live_file_count(), picker.has_git_repo()))
272 })
273 .unwrap_or((0, false));
274
275 if self.0.rescan_throttle.admit(live_files, has_git) {
276 return true;
277 }
278
279 self.0.rescans.record_throttled(reason);
280 tracing::debug!(%reason, live_files, "Rescan throttled, skipping");
281 false
282 }
283
284 pub fn watch(
292 &self,
293 pattern: &str,
294 options: WatchOptions,
295 callback: impl Fn(WatchId, &[WatchEvent]) + Send + Sync + 'static,
296 ) -> Result<WatchId, Error> {
297 let (base_path, has_watcher, watcher_ready) = {
298 let guard = self.read()?;
299 let picker = guard.as_ref().ok_or(Error::FilePickerMissing)?;
300
301 (
302 picker.base_path().to_path_buf(),
303 picker.has_watcher(),
304 picker.is_watcher_ready(),
305 )
306 };
307
308 if !has_watcher {
309 return Err(Error::WatcherDisabled);
310 }
311 if !watcher_ready {
312 return Err(Error::WatcherNotReady);
313 }
314
315 self.0
316 .watchers
317 .subscribe(&base_path, pattern, options, Box::new(callback))
318 }
319
320 pub fn unwatch(&self, id: WatchId) -> bool {
322 self.0.watchers.unsubscribe(id)
323 }
324
325 pub fn is_watch_active(&self, id: WatchId) -> bool {
327 self.0.watchers.contains(id)
328 }
329
330 pub fn shutdown_watches(&self) {
332 self.0.watchers.shutdown();
333 }
334
335 pub fn shutdown_watches_and_wait(&self) {
338 self.0.watchers.shutdown_and_wait();
339 }
340
341 pub(crate) fn rebase_watches(&self, base_path: &Path) {
342 self.0.watchers.rebase(base_path);
343 }
344
345 pub(crate) fn watch_registry(&self) -> &Arc<WatchRegistry> {
346 &self.0.watchers
347 }
348
349 #[tracing::instrument(level = "info", skip_all)]
351 pub fn refresh_git_status(&self, shared_frecency: &SharedFrecency) -> Result<usize, Error> {
352 use tracing::debug;
353
354 let git_status = {
355 let git_root = {
356 let guard = self.read()?;
357 let Some(ref picker) = *guard else {
358 return Err(Error::FilePickerMissing);
359 };
360 picker.git_root().map(|p| p.to_path_buf())
361 };
362
363 debug!(?git_root, "Refreshing git status for picker");
364
365 if let Some(ref root) = git_root {
366 wait_for_git_index_lock_release(root);
367 }
368
369 GitStatusCache::read_git_status(
370 git_root.as_deref(),
371 &mut crate::git::default_status_options(),
372 )
373 };
374
375 let mut guard = self.write()?;
376 let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
377
378 let statuses_count = if let Some(git_status) = git_status {
379 let count = git_status.statuses_len();
380 picker.update_git_statuses(git_status, shared_frecency)?;
381 count
382 } else {
383 0
384 };
385
386 Ok(statuses_count)
387 }
388
389 pub fn update_git_status_for_paths(
391 &self,
392 paths: &[PathBuf],
393 shared_frecency: &SharedFrecency,
394 ) -> Result<(), Error> {
395 if paths.is_empty() {
396 return Ok(());
397 }
398
399 let git_root = {
400 let guard = self.read()?;
401 let Some(ref picker) = *guard else {
402 return Err(Error::FilePickerMissing);
403 };
404 picker.git_root().map(|p| p.to_path_buf())
405 };
406 let Some(git_root) = git_root else {
407 return Ok(());
408 };
409
410 wait_for_git_index_lock_release(&git_root);
411
412 let repo = Repository::open(&git_root)?;
413 let status = GitStatusCache::git_status_for_paths(&repo, paths)?;
414
415 let mut guard = self.write()?;
416 let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
417 picker.update_git_statuses(status, shared_frecency)
418 }
419}
420
421#[allow(private_bounds)]
428pub struct SharedDb<T: LmdbStore> {
429 inner: Arc<RwLock<Option<T>>>,
430 enabled: bool,
431}
432
433impl<T: LmdbStore> Clone for SharedDb<T> {
435 fn clone(&self) -> Self {
436 Self {
437 inner: self.inner.clone(),
438 enabled: self.enabled,
439 }
440 }
441}
442
443impl<T: LmdbStore> Default for SharedDb<T> {
444 fn default() -> Self {
445 Self {
446 inner: Arc::new(RwLock::new(None)),
447 enabled: true,
448 }
449 }
450}
451
452impl<T: LmdbStore> std::fmt::Debug for SharedDb<T> {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 f.debug_tuple("SharedDb").field(&T::LABEL).finish()
455 }
456}
457
458#[allow(private_bounds)]
459impl<T: LmdbStore> SharedDb<T> {
460 pub fn noop() -> Self {
462 Self {
463 inner: Arc::new(RwLock::new(None)),
464 enabled: false,
465 }
466 }
467
468 pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<T>>, Error> {
469 self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
470 }
471
472 pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<T>>, Error> {
473 self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
474 }
475
476 pub fn init(&self, tracker: T) -> Result<(), Error> {
478 if !self.enabled {
479 return Ok(());
480 }
481
482 {
483 let mut guard = self.write()?;
484 *guard = Some(tracker);
485 }
486
487 spawn_lmdb_gc(self.inner.clone());
489 Ok(())
490 }
491
492 pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
496 let mut guard = self.write()?;
497 let Some(tracker) = guard.take() else {
498 return Ok(None);
499 };
500
501 let closing_event = match tracker.shared_env().destroy() {
502 Ok(closing) => closing,
503 Err(e) => {
504 *guard = Some(tracker);
505 return Err(e);
506 }
507 };
508
509 let db_path = tracker.env().path().to_path_buf();
510 drop(tracker);
512 drop(guard);
513
514 if let Some(event) = closing_event {
516 event.wait_timeout(Duration::from_secs(5));
517 }
518
519 std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
520 path: db_path.clone(),
521 source,
522 })?;
523 Ok(Some(db_path))
524 }
525}
526
527pub type SharedFrecency = SharedDb<FrecencyTracker>;
529
530pub type SharedQueryTracker = SharedDb<QueryTracker>;