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