diff options
author | Aleksey Kladov <[email protected]> | 2018-12-18 13:38:05 +0000 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2018-12-20 09:15:38 +0000 |
commit | a422d480a188a28c6b5e7862fbf07817eb2c7447 (patch) | |
tree | d2a1945e49d1728f210c29ae8e88bffef19d22b7 /crates | |
parent | e69b05781f7fb0f0dfdcd4acb433dbcde9cbb7b7 (diff) |
implement vfs events handling
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_lsp_server/tests/heavy_tests/support.rs | 4 | ||||
-rw-r--r-- | crates/ra_vfs/Cargo.toml | 3 | ||||
-rw-r--r-- | crates/ra_vfs/src/arena.rs | 7 | ||||
-rw-r--r-- | crates/ra_vfs/src/io.rs | 43 | ||||
-rw-r--r-- | crates/ra_vfs/src/lib.rs | 153 | ||||
-rw-r--r-- | crates/ra_vfs/tests/vfs.rs | 101 |
6 files changed, 268 insertions, 43 deletions
diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs index 07a878a26..c14d287ca 100644 --- a/crates/ra_lsp_server/tests/heavy_tests/support.rs +++ b/crates/ra_lsp_server/tests/heavy_tests/support.rs | |||
@@ -174,11 +174,11 @@ impl Server { | |||
174 | impl Drop for Server { | 174 | impl Drop for Server { |
175 | fn drop(&mut self) { | 175 | fn drop(&mut self) { |
176 | self.send_request::<Shutdown>(666, ()); | 176 | self.send_request::<Shutdown>(666, ()); |
177 | let receiver = self.worker.take().unwrap().stop(); | 177 | let receiver = self.worker.take().unwrap().shutdown(); |
178 | while let Some(msg) = recv_timeout(&receiver) { | 178 | while let Some(msg) = recv_timeout(&receiver) { |
179 | drop(msg); | 179 | drop(msg); |
180 | } | 180 | } |
181 | self.watcher.take().unwrap().stop().unwrap(); | 181 | self.watcher.take().unwrap().shutdown().unwrap(); |
182 | } | 182 | } |
183 | } | 183 | } |
184 | 184 | ||
diff --git a/crates/ra_vfs/Cargo.toml b/crates/ra_vfs/Cargo.toml index 9ce619a77..ccea8a866 100644 --- a/crates/ra_vfs/Cargo.toml +++ b/crates/ra_vfs/Cargo.toml | |||
@@ -12,3 +12,6 @@ crossbeam-channel = "0.2.4" | |||
12 | log = "0.4.6" | 12 | log = "0.4.6" |
13 | 13 | ||
14 | thread_worker = { path = "../thread_worker" } | 14 | thread_worker = { path = "../thread_worker" } |
15 | |||
16 | [dev-dependencies] | ||
17 | tempfile = "3" | ||
diff --git a/crates/ra_vfs/src/arena.rs b/crates/ra_vfs/src/arena.rs index d6fad753b..6b42ae26d 100644 --- a/crates/ra_vfs/src/arena.rs +++ b/crates/ra_vfs/src/arena.rs | |||
@@ -1,5 +1,4 @@ | |||
1 | use std::{ | 1 | use std::{ |
2 | hash::{Hash, Hasher}, | ||
3 | marker::PhantomData, | 2 | marker::PhantomData, |
4 | ops::{Index, IndexMut}, | 3 | ops::{Index, IndexMut}, |
5 | }; | 4 | }; |
@@ -21,6 +20,12 @@ impl<ID: ArenaId, T> Arena<ID, T> { | |||
21 | self.data.push(value); | 20 | self.data.push(value); |
22 | ID::from_u32(id) | 21 | ID::from_u32(id) |
23 | } | 22 | } |
23 | pub fn iter<'a>(&'a self) -> impl Iterator<Item = (ID, &'a T)> { | ||
24 | self.data | ||
25 | .iter() | ||
26 | .enumerate() | ||
27 | .map(|(idx, value)| (ID::from_u32(idx as u32), value)) | ||
28 | } | ||
24 | } | 29 | } |
25 | 30 | ||
26 | impl<ID: ArenaId, T> Default for Arena<ID, T> { | 31 | impl<ID: ArenaId, T> Default for Arena<ID, T> { |
diff --git a/crates/ra_vfs/src/io.rs b/crates/ra_vfs/src/io.rs index c46760583..178c9beff 100644 --- a/crates/ra_vfs/src/io.rs +++ b/crates/ra_vfs/src/io.rs | |||
@@ -1,35 +1,26 @@ | |||
1 | use std::{ | 1 | use std::{ |
2 | fs, | 2 | fs, |
3 | path::{Path, PathBuf}, | 3 | path::{Path, PathBuf}, |
4 | thread::JoinHandle, | ||
5 | }; | 4 | }; |
6 | 5 | ||
7 | use walkdir::{DirEntry, WalkDir}; | 6 | use walkdir::{DirEntry, WalkDir}; |
8 | use crossbeam_channel::{Sender, Receiver}; | ||
9 | use thread_worker::{WorkerHandle}; | 7 | use thread_worker::{WorkerHandle}; |
8 | use relative_path::RelativePathBuf; | ||
10 | 9 | ||
11 | use crate::VfsRoot; | 10 | use crate::VfsRoot; |
12 | 11 | ||
13 | pub(crate) enum Task { | 12 | pub(crate) struct Task { |
14 | ScanRoot { | 13 | pub(crate) root: VfsRoot, |
15 | root: VfsRoot, | ||
16 | path: PathBuf, | ||
17 | filter: Box<FnMut(&DirEntry) -> bool + Send>, | ||
18 | }, | ||
19 | } | ||
20 | |||
21 | #[derive(Debug)] | ||
22 | pub(crate) struct FileEvent { | ||
23 | pub(crate) path: PathBuf, | 14 | pub(crate) path: PathBuf, |
24 | pub(crate) kind: FileEventKind, | 15 | pub(crate) filter: Box<Fn(&DirEntry) -> bool + Send>, |
25 | } | 16 | } |
26 | 17 | ||
27 | #[derive(Debug)] | 18 | pub struct TaskResult { |
28 | pub(crate) enum FileEventKind { | 19 | pub(crate) root: VfsRoot, |
29 | Add(String), | 20 | pub(crate) files: Vec<(RelativePathBuf, String)>, |
30 | } | 21 | } |
31 | 22 | ||
32 | pub(crate) type Worker = thread_worker::Worker<Task, (PathBuf, Vec<FileEvent>)>; | 23 | pub(crate) type Worker = thread_worker::Worker<Task, TaskResult>; |
33 | 24 | ||
34 | pub(crate) fn start() -> (Worker, WorkerHandle) { | 25 | pub(crate) fn start() -> (Worker, WorkerHandle) { |
35 | thread_worker::spawn("vfs", 128, |input_receiver, output_sender| { | 26 | thread_worker::spawn("vfs", 128, |input_receiver, output_sender| { |
@@ -39,17 +30,17 @@ pub(crate) fn start() -> (Worker, WorkerHandle) { | |||
39 | }) | 30 | }) |
40 | } | 31 | } |
41 | 32 | ||
42 | fn handle_task(task: Task) -> (PathBuf, Vec<FileEvent>) { | 33 | fn handle_task(task: Task) -> TaskResult { |
43 | let Task::ScanRoot { path, .. } = task; | 34 | let Task { root, path, filter } = task; |
44 | log::debug!("loading {} ...", path.as_path().display()); | 35 | log::debug!("loading {} ...", path.as_path().display()); |
45 | let events = load_root(path.as_path()); | 36 | let files = load_root(path.as_path(), &*filter); |
46 | log::debug!("... loaded {}", path.as_path().display()); | 37 | log::debug!("... loaded {}", path.as_path().display()); |
47 | (path, events) | 38 | TaskResult { root, files } |
48 | } | 39 | } |
49 | 40 | ||
50 | fn load_root(path: &Path) -> Vec<FileEvent> { | 41 | fn load_root(root: &Path, filter: &dyn Fn(&DirEntry) -> bool) -> Vec<(RelativePathBuf, String)> { |
51 | let mut res = Vec::new(); | 42 | let mut res = Vec::new(); |
52 | for entry in WalkDir::new(path) { | 43 | for entry in WalkDir::new(root).into_iter().filter_entry(filter) { |
53 | let entry = match entry { | 44 | let entry = match entry { |
54 | Ok(entry) => entry, | 45 | Ok(entry) => entry, |
55 | Err(e) => { | 46 | Err(e) => { |
@@ -71,10 +62,8 @@ fn load_root(path: &Path) -> Vec<FileEvent> { | |||
71 | continue; | 62 | continue; |
72 | } | 63 | } |
73 | }; | 64 | }; |
74 | res.push(FileEvent { | 65 | let path = RelativePathBuf::from_path(path.strip_prefix(root).unwrap()).unwrap(); |
75 | path: path.to_owned(), | 66 | res.push((path.to_owned(), text)) |
76 | kind: FileEventKind::Add(text), | ||
77 | }) | ||
78 | } | 67 | } |
79 | res | 68 | res |
80 | } | 69 | } |
diff --git a/crates/ra_vfs/src/lib.rs b/crates/ra_vfs/src/lib.rs index 8ce6b6ee0..792f722a7 100644 --- a/crates/ra_vfs/src/lib.rs +++ b/crates/ra_vfs/src/lib.rs | |||
@@ -15,14 +15,19 @@ mod arena; | |||
15 | mod io; | 15 | mod io; |
16 | 16 | ||
17 | use std::{ | 17 | use std::{ |
18 | mem, | ||
18 | thread, | 19 | thread, |
19 | cmp::Reverse, | 20 | cmp::Reverse, |
20 | path::{Path, PathBuf}, | 21 | path::{Path, PathBuf}, |
21 | ffi::OsStr, | 22 | ffi::OsStr, |
22 | sync::Arc, | 23 | sync::Arc, |
24 | fs, | ||
23 | }; | 25 | }; |
24 | 26 | ||
27 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
25 | use relative_path::RelativePathBuf; | 28 | use relative_path::RelativePathBuf; |
29 | use crossbeam_channel::Receiver; | ||
30 | use walkdir::DirEntry; | ||
26 | use thread_worker::{WorkerHandle}; | 31 | use thread_worker::{WorkerHandle}; |
27 | 32 | ||
28 | use crate::{ | 33 | use crate::{ |
@@ -40,15 +45,25 @@ impl RootFilter { | |||
40 | fn new(root: PathBuf) -> RootFilter { | 45 | fn new(root: PathBuf) -> RootFilter { |
41 | RootFilter { | 46 | RootFilter { |
42 | root, | 47 | root, |
43 | file_filter: rs_extension_filter, | 48 | file_filter: has_rs_extension, |
44 | } | 49 | } |
45 | } | 50 | } |
46 | fn can_contain(&self, path: &Path) -> bool { | 51 | /// Check if this root can contain `path`. NB: even if this returns |
47 | (self.file_filter)(path) && path.starts_with(&self.root) | 52 | /// true, the `path` might actually be conained in some nested root. |
53 | fn can_contain(&self, path: &Path) -> Option<RelativePathBuf> { | ||
54 | if !(self.file_filter)(path) { | ||
55 | return None; | ||
56 | } | ||
57 | if !(path.starts_with(&self.root)) { | ||
58 | return None; | ||
59 | } | ||
60 | let path = path.strip_prefix(&self.root).unwrap(); | ||
61 | let path = RelativePathBuf::from_path(path).unwrap(); | ||
62 | Some(path) | ||
48 | } | 63 | } |
49 | } | 64 | } |
50 | 65 | ||
51 | fn rs_extension_filter(p: &Path) -> bool { | 66 | fn has_rs_extension(p: &Path) -> bool { |
52 | p.extension() == Some(OsStr::new("rs")) | 67 | p.extension() == Some(OsStr::new("rs")) |
53 | } | 68 | } |
54 | 69 | ||
@@ -82,10 +97,11 @@ struct VfsFileData { | |||
82 | text: Arc<String>, | 97 | text: Arc<String>, |
83 | } | 98 | } |
84 | 99 | ||
85 | struct Vfs { | 100 | pub struct Vfs { |
86 | roots: Arena<VfsRoot, RootFilter>, | 101 | roots: Arena<VfsRoot, RootFilter>, |
87 | files: Arena<VfsFile, VfsFileData>, | 102 | files: Arena<VfsFile, VfsFileData>, |
88 | // pending_changes: Vec<PendingChange>, | 103 | root2files: FxHashMap<VfsRoot, FxHashSet<VfsFile>>, |
104 | pending_changes: Vec<VfsChange>, | ||
89 | worker: io::Worker, | 105 | worker: io::Worker, |
90 | worker_handle: WorkerHandle, | 106 | worker_handle: WorkerHandle, |
91 | } | 107 | } |
@@ -97,33 +113,144 @@ impl Vfs { | |||
97 | let mut res = Vfs { | 113 | let mut res = Vfs { |
98 | roots: Arena::default(), | 114 | roots: Arena::default(), |
99 | files: Arena::default(), | 115 | files: Arena::default(), |
116 | root2files: FxHashMap::default(), | ||
100 | worker, | 117 | worker, |
101 | worker_handle, | 118 | worker_handle, |
119 | pending_changes: Vec::new(), | ||
102 | }; | 120 | }; |
103 | 121 | ||
104 | // A hack to make nesting work. | 122 | // A hack to make nesting work. |
105 | roots.sort_by_key(|it| Reverse(it.as_os_str().len())); | 123 | roots.sort_by_key(|it| Reverse(it.as_os_str().len())); |
106 | 124 | for (i, path) in roots.iter().enumerate() { | |
107 | for path in roots { | 125 | let root = res.roots.alloc(RootFilter::new(path.clone())); |
108 | res.roots.alloc(RootFilter::new(path)); | 126 | let nested = roots[..i] |
127 | .iter() | ||
128 | .filter(|it| it.starts_with(path)) | ||
129 | .map(|it| it.clone()) | ||
130 | .collect::<Vec<_>>(); | ||
131 | let filter = move |entry: &DirEntry| { | ||
132 | if entry.file_type().is_file() { | ||
133 | has_rs_extension(entry.path()) | ||
134 | } else { | ||
135 | nested.iter().all(|it| it != entry.path()) | ||
136 | } | ||
137 | }; | ||
138 | let task = io::Task { | ||
139 | root, | ||
140 | path: path.clone(), | ||
141 | filter: Box::new(filter), | ||
142 | }; | ||
143 | res.worker.inp.send(task); | ||
109 | } | 144 | } |
110 | res | 145 | res |
111 | } | 146 | } |
112 | 147 | ||
113 | pub fn add_file_overlay(&mut self, path: &Path, content: String) {} | 148 | pub fn task_receiver(&self) -> &Receiver<io::TaskResult> { |
149 | &self.worker.out | ||
150 | } | ||
151 | |||
152 | pub fn handle_task(&mut self, task: io::TaskResult) { | ||
153 | let mut files = Vec::new(); | ||
154 | for (path, text) in task.files { | ||
155 | let text = Arc::new(text); | ||
156 | let file = self.add_file(task.root, path.clone(), Arc::clone(&text)); | ||
157 | files.push((file, path, text)); | ||
158 | } | ||
159 | let change = VfsChange::AddRoot { | ||
160 | root: task.root, | ||
161 | files, | ||
162 | }; | ||
163 | self.pending_changes.push(change); | ||
164 | } | ||
114 | 165 | ||
115 | pub fn change_file_overlay(&mut self, path: &Path, new_content: String) {} | 166 | pub fn add_file_overlay(&mut self, path: &Path, text: String) { |
167 | if let Some((root, path, file)) = self.find_root(path) { | ||
168 | let text = Arc::new(text); | ||
169 | let change = if let Some(file) = file { | ||
170 | self.change_file(file, Arc::clone(&text)); | ||
171 | VfsChange::ChangeFile { file, text } | ||
172 | } else { | ||
173 | let file = self.add_file(root, path.clone(), Arc::clone(&text)); | ||
174 | VfsChange::AddFile { | ||
175 | file, | ||
176 | text, | ||
177 | root, | ||
178 | path, | ||
179 | } | ||
180 | }; | ||
181 | self.pending_changes.push(change); | ||
182 | } | ||
183 | } | ||
116 | 184 | ||
117 | pub fn remove_file_overlay(&mut self, path: &Path) {} | 185 | pub fn change_file_overlay(&mut self, path: &Path, new_text: String) { |
186 | if let Some((_root, _path, file)) = self.find_root(path) { | ||
187 | let file = file.expect("can't change a file which wasn't added"); | ||
188 | let text = Arc::new(new_text); | ||
189 | self.change_file(file, Arc::clone(&text)); | ||
190 | let change = VfsChange::ChangeFile { file, text }; | ||
191 | self.pending_changes.push(change); | ||
192 | } | ||
193 | } | ||
194 | |||
195 | pub fn remove_file_overlay(&mut self, path: &Path) { | ||
196 | if let Some((root, path, file)) = self.find_root(path) { | ||
197 | let file = file.expect("can't remove a file which wasn't added"); | ||
198 | let full_path = path.to_path(&self.roots[root].root); | ||
199 | let change = if let Ok(text) = fs::read_to_string(&full_path) { | ||
200 | let text = Arc::new(text); | ||
201 | self.change_file(file, Arc::clone(&text)); | ||
202 | VfsChange::ChangeFile { file, text } | ||
203 | } else { | ||
204 | self.remove_file(file); | ||
205 | VfsChange::RemoveFile { file } | ||
206 | }; | ||
207 | self.pending_changes.push(change); | ||
208 | } | ||
209 | } | ||
118 | 210 | ||
119 | pub fn commit_changes(&mut self) -> Vec<VfsChange> { | 211 | pub fn commit_changes(&mut self) -> Vec<VfsChange> { |
120 | unimplemented!() | 212 | mem::replace(&mut self.pending_changes, Vec::new()) |
121 | } | 213 | } |
122 | 214 | ||
123 | pub fn shutdown(self) -> thread::Result<()> { | 215 | pub fn shutdown(self) -> thread::Result<()> { |
124 | let _ = self.worker.shutdown(); | 216 | let _ = self.worker.shutdown(); |
125 | self.worker_handle.shutdown() | 217 | self.worker_handle.shutdown() |
126 | } | 218 | } |
219 | |||
220 | fn add_file(&mut self, root: VfsRoot, path: RelativePathBuf, text: Arc<String>) -> VfsFile { | ||
221 | let data = VfsFileData { root, path, text }; | ||
222 | let file = self.files.alloc(data); | ||
223 | self.root2files | ||
224 | .entry(root) | ||
225 | .or_insert_with(FxHashSet::default) | ||
226 | .insert(file); | ||
227 | file | ||
228 | } | ||
229 | |||
230 | fn change_file(&mut self, file: VfsFile, new_text: Arc<String>) { | ||
231 | self.files[file].text = new_text; | ||
232 | } | ||
233 | |||
234 | fn remove_file(&mut self, file: VfsFile) { | ||
235 | //FIXME: use arena with removal | ||
236 | self.files[file].text = Default::default(); | ||
237 | self.files[file].path = Default::default(); | ||
238 | let root = self.files[file].root; | ||
239 | let removed = self.root2files.get_mut(&root).unwrap().remove(&file); | ||
240 | assert!(removed); | ||
241 | } | ||
242 | |||
243 | fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option<VfsFile>)> { | ||
244 | let (root, path) = self | ||
245 | .roots | ||
246 | .iter() | ||
247 | .find_map(|(root, data)| data.can_contain(path).map(|it| (root, it)))?; | ||
248 | let file = self.root2files[&root] | ||
249 | .iter() | ||
250 | .map(|&it| it) | ||
251 | .find(|&file| self.files[file].path == path); | ||
252 | Some((root, path, file)) | ||
253 | } | ||
127 | } | 254 | } |
128 | 255 | ||
129 | #[derive(Debug, Clone)] | 256 | #[derive(Debug, Clone)] |
diff --git a/crates/ra_vfs/tests/vfs.rs b/crates/ra_vfs/tests/vfs.rs new file mode 100644 index 000000000..4f44215c8 --- /dev/null +++ b/crates/ra_vfs/tests/vfs.rs | |||
@@ -0,0 +1,101 @@ | |||
1 | use std::{ | ||
2 | fs, | ||
3 | collections::HashSet, | ||
4 | }; | ||
5 | |||
6 | use tempfile::tempdir; | ||
7 | |||
8 | use ra_vfs::{Vfs, VfsChange}; | ||
9 | |||
10 | #[test] | ||
11 | fn test_vfs_works() -> std::io::Result<()> { | ||
12 | let files = [ | ||
13 | ("a/foo.rs", "hello"), | ||
14 | ("a/bar.rs", "world"), | ||
15 | ("a/b/baz.rs", "nested hello"), | ||
16 | ]; | ||
17 | |||
18 | let dir = tempdir()?; | ||
19 | for (path, text) in files.iter() { | ||
20 | let file_path = dir.path().join(path); | ||
21 | fs::create_dir_all(file_path.parent().unwrap())?; | ||
22 | fs::write(file_path, text)? | ||
23 | } | ||
24 | |||
25 | let a_root = dir.path().join("a"); | ||
26 | let b_root = dir.path().join("a/b"); | ||
27 | |||
28 | let mut vfs = Vfs::new(vec![a_root, b_root]); | ||
29 | for _ in 0..2 { | ||
30 | let task = vfs.task_receiver().recv().unwrap(); | ||
31 | vfs.handle_task(task); | ||
32 | } | ||
33 | { | ||
34 | let files = vfs | ||
35 | .commit_changes() | ||
36 | .into_iter() | ||
37 | .flat_map(|change| { | ||
38 | let files = match change { | ||
39 | VfsChange::AddRoot { files, .. } => files, | ||
40 | _ => panic!("unexpected change"), | ||
41 | }; | ||
42 | files.into_iter().map(|(_id, path, text)| { | ||
43 | let text: String = (&*text).clone(); | ||
44 | (format!("{}", path.display()), text) | ||
45 | }) | ||
46 | }) | ||
47 | .collect::<HashSet<_>>(); | ||
48 | |||
49 | let expected_files = [ | ||
50 | ("foo.rs", "hello"), | ||
51 | ("bar.rs", "world"), | ||
52 | ("baz.rs", "nested hello"), | ||
53 | ] | ||
54 | .iter() | ||
55 | .map(|(path, text)| (path.to_string(), text.to_string())) | ||
56 | .collect::<HashSet<_>>(); | ||
57 | |||
58 | assert_eq!(files, expected_files); | ||
59 | } | ||
60 | |||
61 | vfs.add_file_overlay(&dir.path().join("a/b/baz.rs"), "quux".to_string()); | ||
62 | let change = vfs.commit_changes().pop().unwrap(); | ||
63 | match change { | ||
64 | VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "quux"), | ||
65 | _ => panic!("unexpected change"), | ||
66 | } | ||
67 | |||
68 | vfs.change_file_overlay(&dir.path().join("a/b/baz.rs"), "m".to_string()); | ||
69 | let change = vfs.commit_changes().pop().unwrap(); | ||
70 | match change { | ||
71 | VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "m"), | ||
72 | _ => panic!("unexpected change"), | ||
73 | } | ||
74 | |||
75 | vfs.remove_file_overlay(&dir.path().join("a/b/baz.rs")); | ||
76 | let change = vfs.commit_changes().pop().unwrap(); | ||
77 | match change { | ||
78 | VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "nested hello"), | ||
79 | _ => panic!("unexpected change"), | ||
80 | } | ||
81 | |||
82 | vfs.add_file_overlay(&dir.path().join("a/b/spam.rs"), "spam".to_string()); | ||
83 | let change = vfs.commit_changes().pop().unwrap(); | ||
84 | match change { | ||
85 | VfsChange::AddFile { text, path, .. } => { | ||
86 | assert_eq!(&*text, "spam"); | ||
87 | assert_eq!(path, "spam.rs"); | ||
88 | } | ||
89 | _ => panic!("unexpected change"), | ||
90 | } | ||
91 | |||
92 | vfs.remove_file_overlay(&dir.path().join("a/b/spam.rs")); | ||
93 | let change = vfs.commit_changes().pop().unwrap(); | ||
94 | match change { | ||
95 | VfsChange::RemoveFile { .. } => (), | ||
96 | _ => panic!("unexpected change"), | ||
97 | } | ||
98 | |||
99 | vfs.shutdown().unwrap(); | ||
100 | Ok(()) | ||
101 | } | ||