Start work on excluding separate mount dirs

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock          |  2 +
zed/Cargo.toml      |  4 ++
zed/src/worktree.rs | 84 +++++++++++++++++++++++++++++++++++++---------
3 files changed, 73 insertions(+), 17 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2220,6 +2220,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "arrayvec",
+ "cocoa",
  "crossbeam-channel",
  "ctor",
  "dirs",
@@ -2233,6 +2234,7 @@ dependencies = [
  "libc",
  "log",
  "num_cpus",
+ "objc",
  "parking_lot",
  "postage",
  "rand 0.8.3",

zed/Cargo.toml 🔗

@@ -42,3 +42,7 @@ env_logger = "0.8"
 serde_json = {version = "1.0.64", features = ["preserve_order"]}
 tempdir = "0.3.7"
 unindent = "0.1.7"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+cocoa = "0.24"
+objc = "0.2"

zed/src/worktree.rs 🔗

@@ -354,7 +354,11 @@ impl Snapshot {
                 self.insert_ignore_file(new_parent_inode);
             }
 
-            let mut new_parent = self.entries.get(&new_parent_inode).unwrap().clone();
+            let mut new_parent = self
+                .entries
+                .get(&new_parent_inode)
+                .expect(&format!("no entry for inode {}", new_parent_inode))
+                .clone();
             if let Entry::Dir { children, .. } = &mut new_parent {
                 *children = children
                     .iter()
@@ -417,7 +421,7 @@ impl Snapshot {
             *children = new_children.into();
             *pending = false;
         } else {
-            unreachable!("non-directory parent");
+            unreachable!("non-directory parent {}", parent_inode);
         }
         edits.push(Edit::Insert(parent));
 
@@ -703,16 +707,30 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount {
 struct BackgroundScanner {
     snapshot: Arc<Mutex<Snapshot>>,
     notify: Sender<ScanState>,
+    other_mount_paths: HashSet<PathBuf>,
     thread_pool: scoped_pool::Pool,
 }
 
 impl BackgroundScanner {
     fn new(snapshot: Arc<Mutex<Snapshot>>, notify: Sender<ScanState>, worktree_id: usize) -> Self {
-        Self {
+        let mut scanner = Self {
             snapshot,
             notify,
+            other_mount_paths: Default::default(),
             thread_pool: scoped_pool::Pool::new(16, format!("worktree-{}-scanner", worktree_id)),
-        }
+        };
+        scanner.update_other_mount_paths();
+        scanner
+    }
+
+    fn update_other_mount_paths(&mut self) {
+        let path = self.snapshot.lock().path.clone();
+        self.other_mount_paths.clear();
+        self.other_mount_paths.extend(
+            mounted_volume_paths()
+                .into_iter()
+                .filter(|mount_path| !path.starts_with(mount_path)),
+        );
     }
 
     fn path(&self) -> Arc<Path> {
@@ -723,7 +741,7 @@ impl BackgroundScanner {
         self.snapshot.lock().clone()
     }
 
-    fn run(self, event_stream: fsevent::EventStream) {
+    fn run(mut self, event_stream: fsevent::EventStream) {
         if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() {
             return;
         }
@@ -792,23 +810,17 @@ impl BackgroundScanner {
             .unwrap();
             drop(tx);
 
-            let mut results = Vec::new();
-            results.resize_with(self.thread_pool.thread_count(), || Ok(()));
             self.thread_pool.scoped(|pool| {
-                for result in &mut results {
+                for _ in 0..self.thread_pool.thread_count() {
                     pool.execute(|| {
-                        let result = result;
                         while let Ok(job) = rx.recv() {
                             if let Err(err) = self.scan_dir(&job) {
                                 log::error!("error scanning {:?}: {}", job.path, err);
-                                *result = Err(err);
-                                break;
                             }
                         }
                     });
                 }
             });
-            results.into_iter().collect::<io::Result<()>>()?;
         } else {
             let mut snapshot = self.snapshot.lock();
             snapshot.insert_entry(
@@ -842,6 +854,11 @@ impl BackgroundScanner {
             let child_is_symlink = child_metadata.file_type().is_symlink();
             let child_path = job.path.join(child_name.as_ref());
 
+            // Disallow mount points outside the file system containing the root of this worktree
+            if self.other_mount_paths.contains(&child_path) {
+                continue;
+            }
+
             if child_metadata.is_dir() {
                 new_entries.push((
                     child_name,
@@ -874,7 +891,6 @@ impl BackgroundScanner {
             };
         }
 
-        dbg!(&job.path);
         self.snapshot.lock().populate_dir(job.inode, new_entries);
         for new_job in new_jobs {
             job.scan_queue.send(new_job).unwrap();
@@ -883,7 +899,9 @@ impl BackgroundScanner {
         Ok(())
     }
 
-    fn process_events(&self, mut events: Vec<fsevent::Event>) -> bool {
+    fn process_events(&mut self, mut events: Vec<fsevent::Event>) -> bool {
+        self.update_other_mount_paths();
+
         let mut snapshot = self.snapshot();
         snapshot.scan_id += 1;
 
@@ -1210,6 +1228,34 @@ impl<'a> Iterator for FileIter<'a> {
     }
 }
 
+fn mounted_volume_paths() -> Vec<PathBuf> {
+    use cocoa::{
+        base::{id, nil},
+        foundation::{NSArray, NSString, NSURL},
+    };
+    use objc::{class, msg_send, sel, sel_impl};
+
+    unsafe {
+        let manager: id = msg_send![class!(NSFileManager), defaultManager];
+        let array = NSArray::array(nil);
+        let urls: id =
+            msg_send![manager, mountedVolumeURLsIncludingResourceValuesForKeys:array options:0];
+        let len = urls.count() as usize;
+        let mut result = Vec::with_capacity(len);
+        for i in 0..len {
+            let url = urls.objectAtIndex(i as u64);
+            let string = url.absoluteString();
+            let string = std::ffi::CStr::from_ptr(string.UTF8String())
+                .to_string_lossy()
+                .to_string();
+            if let Some(path) = string.strip_prefix("file://") {
+                result.push(PathBuf::from(path));
+            }
+        }
+        result
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -1273,8 +1319,6 @@ mod tests {
                 );
             })
         });
-
-        eprintln!("HI");
     }
 
     #[test]
@@ -1383,6 +1427,12 @@ mod tests {
         });
     }
 
+    #[test]
+    fn test_mounted_volume_paths() {
+        let paths = mounted_volume_paths();
+        assert!(paths.contains(&"/".into()));
+    }
+
     #[test]
     fn test_random() {
         let iterations = env::var("ITERATIONS")
@@ -1411,7 +1461,7 @@ mod tests {
             log::info!("Generated initial tree");
 
             let (notify_tx, _notify_rx) = smol::channel::unbounded();
-            let scanner = BackgroundScanner::new(
+            let mut scanner = BackgroundScanner::new(
                 Arc::new(Mutex::new(Snapshot {
                     id: 0,
                     scan_id: 0,