Skip to main content

fff_search/
shared.rs

1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
3use std::time::{Duration, Instant};
4
5use crate::dbs::lmdb::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::scan::ScanJob;
12
13/// Poll `.git/index.lock` until it disappears (git write completed), giving up
14/// after [`GIT_LOCK_MAX_WAIT`]. Used by [`SharedPicker::refresh_git_status`]
15/// to avoid reading a half-updated index when the watcher fires mid-`git add`.
16///
17/// The wait is bounded and cheap: the lock file is typically cleared within
18/// a few milliseconds of the git command exiting.
19fn wait_for_git_index_lock_release(git_root: &Path) {
20    const GIT_LOCK_POLL: Duration = Duration::from_millis(10);
21    const GIT_LOCK_MAX_WAIT: Duration = Duration::from_millis(500);
22
23    let lock = git_root.join(".git").join("index.lock");
24    // Fast path: no lock present.
25    if !lock.exists() {
26        return;
27    }
28    let deadline = Instant::now() + GIT_LOCK_MAX_WAIT;
29    while lock.exists() && Instant::now() < deadline {
30        std::thread::sleep(GIT_LOCK_POLL);
31    }
32    if lock.exists() {
33        tracing::warn!(
34            "Proceeding with git status refresh despite lingering \
35             .git/index.lock at {} — will retry once it clears",
36            lock.display()
37        );
38    }
39}
40
41/// Thread-safe shared handle to the [`FilePicker`] instance.
42/// This accumulates only asynchronous non-blocking operations against the
43/// file picker: creating, triggering various rescans and so on.
44///
45/// For blocking access use internal picker via `.read()` or `.write()`
46///
47/// ```ignore
48/// let shared_picker = SharedFilePicker::default();
49///
50/// if let Some(picker) = shared_picker.read()?.as_ref() {
51///     let files = picker.fuzzy_search(&query, options);
52///     println!("Found {} files", files.len());
53/// } else {
54///     println!("Picker not initialized");
55/// }
56/// ```
57#[derive(Clone, Default)]
58pub struct SharedFilePicker(pub(crate) Arc<SharedPickerInner>);
59
60pub struct SharedPickerInner {
61    picker: parking_lot::RwLock<Option<FilePicker>>,
62}
63
64impl Default for SharedPickerInner {
65    fn default() -> Self {
66        Self {
67            picker: parking_lot::RwLock::new(None),
68        }
69    }
70}
71
72/// Non-owning handle to a [`SharedPicker`].
73#[derive(Clone)]
74pub(crate) struct WeakFilePicker(Weak<SharedPickerInner>);
75
76impl WeakFilePicker {
77    /// Try to promote the weak handle back to a strong [`SharedPicker`].
78    ///
79    /// Returns `None` once every strong `SharedPicker` clone has been
80    /// dropped. Callers should treat that as "the picker is being
81    /// torn down" and exit their current iteration cleanly.
82    pub(crate) fn upgrade(&self) -> Option<SharedFilePicker> {
83        self.0.upgrade().map(SharedFilePicker)
84    }
85}
86
87impl std::fmt::Debug for SharedFilePicker {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_tuple("SharedPicker").field(&"..").finish()
90    }
91}
92
93impl SharedFilePicker {
94    pub fn read(&self) -> Result<parking_lot::RwLockReadGuard<'_, Option<FilePicker>>, Error> {
95        Ok(self.0.picker.read())
96    }
97
98    pub fn write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, Option<FilePicker>>, Error> {
99        Ok(self.0.picker.write())
100    }
101
102    /// Produce a non-owning handle to the same inner picker.
103    /// Use it if you don't need to block internal threads from dropping while owning this ref
104    pub(crate) fn weaken(&self) -> WeakFilePicker {
105        WeakFilePicker(Arc::downgrade(&self.0))
106    }
107
108    /// Return `true` if this is an instance of the picker that requires a complicated post-scan
109    /// indexing/cache warmup job. The indexing is not crazy but it takes time.
110    pub fn need_complex_rebuild(&self) -> bool {
111        let guard = self.0.picker.read();
112        guard
113            .as_ref()
114            .is_some_and(|p| p.has_mmap_cache() || p.has_content_indexing())
115    }
116
117    /// Block until the background filesystem scan finishes.
118    /// Returns `true` if scan completed, `false` on timeout.
119    pub fn wait_for_scan(&self, timeout: Duration) -> bool {
120        let signal = {
121            let guard = self.0.picker.read();
122            match &*guard {
123                Some(picker) => Arc::clone(&picker.signals.scanning),
124                None => return true,
125            }
126        };
127
128        let start = std::time::Instant::now();
129        while signal.load(std::sync::atomic::Ordering::Acquire) {
130            if start.elapsed() >= timeout {
131                return false;
132            }
133            std::thread::sleep(Duration::from_millis(10));
134        }
135        true
136    }
137
138    /// Block until the background file watcher is ready.
139    /// Returns `true` if watcher ready, `false` on timeout.
140    pub fn wait_for_watcher(&self, timeout: Duration) -> bool {
141        let watch_ready_signal = {
142            let guard = self.0.picker.read();
143            match &*guard {
144                Some(picker) => Arc::clone(&picker.signals.watcher_ready),
145                None => return true,
146            }
147        };
148
149        let start = std::time::Instant::now();
150        while !watch_ready_signal.load(std::sync::atomic::Ordering::Acquire) {
151            if start.elapsed() >= timeout {
152                return false;
153            }
154            std::thread::sleep(Duration::from_millis(10));
155        }
156        true
157    }
158
159    /// Blocks until both the filesystem walk and post-scan indexing are done.
160    /// Returns true once scanning=false AND post_scan_indexing_active=false.
161    pub fn wait_for_indexing_complete(&self, timeout: Duration) -> bool {
162        let (scanning, post_scan_active) = {
163            let guard = self.0.picker.read();
164            match &*guard {
165                Some(picker) => (
166                    Arc::clone(&picker.signals.scanning),
167                    Arc::clone(&picker.signals.post_scan_indexing_active),
168                ),
169                None => return true,
170            }
171        };
172
173        let start = std::time::Instant::now();
174        loop {
175            if start.elapsed() >= timeout {
176                return false;
177            }
178            let s = scanning.load(std::sync::atomic::Ordering::Acquire);
179            let p = post_scan_active.load(std::sync::atomic::Ordering::Acquire);
180            if !s && !p {
181                return true;
182            }
183            std::thread::sleep(Duration::from_millis(10));
184        }
185    }
186
187    /// Trigger a full filesystem rescan without blocking the caller.
188    /// Performs a safe async rescan. Guarantees only single active rescan per picker.
189    /// If many rescans requested the last one guaranteed to be finished.
190    pub fn trigger_full_rescan_async(&self, shared_frecency: &SharedFrecency) -> Result<(), Error> {
191        match ScanJob::new_rescan(self, shared_frecency)? {
192            Some(job) => {
193                job.spawn();
194            }
195            None => {
196                // we can not abort the ongoing sync, but if the events
197                if let Ok(guard) = self.read()
198                    && let Some(picker) = guard.as_ref()
199                {
200                    picker
201                        .scan_signals()
202                        .rescan_pending
203                        .store(true, std::sync::atomic::Ordering::Release);
204                    tracing::info!(
205                        "Full rescan requested while another scan is active — \
206                         deferred via rescan_pending flag"
207                    );
208                }
209            }
210        }
211        Ok(())
212    }
213
214    /// Refresh git statuses for all indexed files.
215    pub fn refresh_git_status(&self, shared_frecency: &SharedFrecency) -> Result<usize, Error> {
216        use tracing::debug;
217
218        let git_status = {
219            let guard = self.read()?;
220            let Some(ref picker) = *guard else {
221                return Err(Error::FilePickerMissing);
222            };
223
224            let git_root = picker.git_root().map(|p| p.to_path_buf());
225            drop(guard); // updating git status could take very long time, there is not risky as we
226            // do not allow any mutations and deletions of files from the sync
227
228            debug!(?git_root, "Refreshing git status for picker");
229
230            if let Some(ref root) = git_root {
231                wait_for_git_index_lock_release(root);
232            }
233
234            GitStatusCache::read_git_status(
235                git_root.as_deref(),
236                &mut crate::git::default_status_options(),
237            )
238        };
239
240        let mut guard = self.write()?;
241        let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
242
243        let statuses_count = if let Some(git_status) = git_status {
244            let count = git_status.statuses_len();
245            picker.update_git_statuses(git_status, shared_frecency)?;
246            count
247        } else {
248            0
249        };
250
251        Ok(statuses_count)
252    }
253}
254
255/// Thread-safe shared handle to the [`FrecencyTracker`] instance.
256#[derive(Clone)]
257pub struct SharedFrecency {
258    inner: Arc<RwLock<Option<FrecencyTracker>>>,
259    enabled: bool,
260}
261
262impl Default for SharedFrecency {
263    fn default() -> Self {
264        Self {
265            inner: Arc::new(RwLock::new(None)),
266            enabled: true,
267        }
268    }
269}
270
271impl std::fmt::Debug for SharedFrecency {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        f.debug_tuple("SharedFrecency").field(&"..").finish()
274    }
275}
276
277impl SharedFrecency {
278    /// Creates a disabled instance that silently ignores all writes.
279    pub fn noop() -> Self {
280        Self {
281            inner: Arc::new(RwLock::new(None)),
282            enabled: false,
283        }
284    }
285
286    pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<FrecencyTracker>>, Error> {
287        self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
288    }
289
290    pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<FrecencyTracker>>, Error> {
291        self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
292    }
293
294    pub fn init(&self, tracker: FrecencyTracker) -> Result<(), Error> {
295        if !self.enabled {
296            return Ok(());
297        }
298
299        {
300            let mut guard = self.write()?;
301            *guard = Some(tracker);
302        }
303
304        // GC holds a read guard on this lock, so destroy / re-init wait
305        // for it naturally — no join handle, no race against file removal.
306        spawn_lmdb_gc(self.inner.clone());
307        Ok(())
308    }
309
310    /// Drop the in-memory tracker and delete the on-disk database directory.
311    ///
312    /// Acquires the write lock, ensuring all readers (including any active mmap
313    /// access) are finished before the LMDB environment is closed and the files
314    /// are removed.
315    ///
316    /// Returns `Ok(Some(path))` with the deleted path, or `Ok(None)` if no
317    /// tracker was initialized.
318    pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
319        let mut guard = self.write()?;
320        let Some(tracker) = guard.take() else {
321            return Ok(None);
322        };
323        let db_path = tracker.db_path().to_path_buf();
324        // Drop closes the LMDB env and unmaps the files
325        drop(tracker);
326        drop(guard);
327        std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
328            path: db_path.clone(),
329            source,
330        })?;
331        Ok(Some(db_path))
332    }
333}
334
335/// Thread-safe shared handle to the [`QueryTracker`] instance.
336#[derive(Clone)]
337pub struct SharedQueryTracker {
338    inner: Arc<RwLock<Option<QueryTracker>>>,
339    enabled: bool,
340}
341
342impl Default for SharedQueryTracker {
343    fn default() -> Self {
344        Self {
345            inner: Arc::new(RwLock::new(None)),
346            enabled: true,
347        }
348    }
349}
350
351impl std::fmt::Debug for SharedQueryTracker {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        f.debug_tuple("SharedQueryTracker").field(&"..").finish()
354    }
355}
356
357impl SharedQueryTracker {
358    /// Creates a disabled instance that silently ignores all writes.
359    pub fn noop() -> Self {
360        Self {
361            inner: Arc::new(RwLock::new(None)),
362            enabled: false,
363        }
364    }
365
366    pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<QueryTracker>>, Error> {
367        self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
368    }
369
370    pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<QueryTracker>>, Error> {
371        self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
372    }
373
374    /// Initialize the query tracker + spawn GC in the background.
375    /// No-op if this is a disabled instance.
376    pub fn init(&self, tracker: QueryTracker) -> Result<(), Error> {
377        if !self.enabled {
378            return Ok(());
379        }
380        {
381            let mut guard = self.write()?;
382            *guard = Some(tracker);
383        }
384
385        spawn_lmdb_gc(self.inner.clone());
386        Ok(())
387    }
388
389    ///Drop the in-memory tracker and delete the on-disk database directory.
390    ///
391    /// Acquires the write lock, ensuring all readers (including any active mmap
392    /// access) are finished before the LMDB environment is closed and the files
393    /// are removed.
394    ///
395    /// Returns `Ok(Some(path))` with the deleted path, or `Ok(None)` if no
396    /// tracker was initialized.
397    pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
398        let mut guard = self.write()?;
399        let Some(tracker) = guard.take() else {
400            return Ok(None);
401        };
402        let db_path = tracker.db_path().to_path_buf();
403        drop(tracker);
404        drop(guard);
405        std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
406            path: db_path.clone(),
407            source,
408        })?;
409        Ok(Some(db_path))
410    }
411}