1use cm_types::{NamespacePath, Path, RelativePath};
5use fidl::endpoints::ClientEnd;
6use futures::channel::mpsc::{UnboundedSender, unbounded};
7use namespace::{Entry as NamespaceEntry, EntryError, Namespace, NamespaceError, Tree};
8use router_error::Explain;
9use sandbox::{Capability, Dict, RemotableCapability, RouterResponse};
10use thiserror::Error;
11use vfs::directory::entry::serve_directory;
12use vfs::execution_scope::ExecutionScope;
13use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
14
15pub struct NamespaceBuilder {
17 entries: Tree<Capability>,
19
20 not_found: UnboundedSender<String>,
22
23 namespace_scope: ExecutionScope,
27}
28
29#[derive(Error, Debug, Clone)]
30pub enum BuildNamespaceError {
31 #[error(transparent)]
32 NamespaceError(#[from] NamespaceError),
33
34 #[error(
35 "while installing capabilities within the namespace entry `{path}`, \
36 failed to convert the namespace entry to Directory: {err}"
37 )]
38 Conversion {
39 path: NamespacePath,
40 #[source]
41 err: sandbox::ConversionError,
42 },
43
44 #[error("unable to serve `{path}` after converting to directory: {err}")]
45 Serve {
46 path: NamespacePath,
47 #[source]
48 err: fidl::Status,
49 },
50}
51
52impl NamespaceBuilder {
53 pub fn new(namespace_scope: ExecutionScope, not_found: UnboundedSender<String>) -> Self {
54 return NamespaceBuilder { entries: Default::default(), not_found, namespace_scope };
55 }
56
57 pub fn add_object(
60 self: &mut Self,
61 cap: Capability,
62 path: &Path,
63 ) -> Result<(), BuildNamespaceError> {
64 let dirname = path.parent();
65
66 let any = match self.entries.get_mut(&dirname) {
68 Some(dir) => dir,
69 None => {
70 let dict = self.make_dict_with_not_found_logging(dirname.to_string());
71 self.entries.add(&dirname, Capability::Dictionary(dict))?
72 }
73 };
74
75 let dict = match any {
78 Capability::Dictionary(d) => d,
79 _ => Err(NamespaceError::Duplicate(path.clone().into()))?,
80 };
81
82 dict.insert(path.basename().into(), cap)
84 .map_err(|_| NamespaceError::Duplicate(path.clone().into()).into())
85 }
86
87 pub fn add_entry(
91 self: &mut Self,
92 cap: Capability,
93 path: &NamespacePath,
94 ) -> Result<(), BuildNamespaceError> {
95 match &cap {
96 Capability::Directory(_)
97 | Capability::Dictionary(_)
98 | Capability::DirEntry(_)
99 | Capability::DirConnector(_)
100 | Capability::DirConnectorRouter(_)
101 | Capability::DictionaryRouter(_) => {}
102 _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
103 }
104 self.entries.add(path, cap)?;
105 Ok(())
106 }
107
108 pub fn serve(self: Self) -> Result<Namespace, BuildNamespaceError> {
109 let mut entries = vec![];
110 for (path, cap) in self.entries.flatten() {
111 let client_end: ClientEnd<fio::DirectoryMarker> = match cap {
112 Capability::Directory(d) => d.into(),
113 Capability::DirConnector(c) => {
114 let (client, server) =
115 fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
116 let _ = self.namespace_scope.spawn(async move {
119 let res =
120 fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
121 if res.is_err() {
122 return;
123 }
124 let _ = c.send(server, RelativePath::dot(), None);
129 });
130 client
131 }
132 Capability::DirConnectorRouter(c) => {
133 let (client, server) =
134 fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
135 let _ = self.namespace_scope.spawn(async move {
138 let res =
139 fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
140 if res.is_err() {
141 return;
142 }
143 match c.route(None, false).await {
144 Ok(RouterResponse::Capability(dir_connector)) => {
145 let _ = dir_connector.send(server, RelativePath::dot(), None);
148 }
149 Ok(RouterResponse::Unavailable) => {
150 let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
151 }
152 Ok(RouterResponse::Debug(_)) => {
153 panic!("debug response wasn't requested");
154 }
155 Err(e) => {
156 let _ = server.close_with_epitaph(e.as_zx_status());
160 }
161 }
162 });
163 client
164 }
165 Capability::Dictionary(dict) => {
166 let entry =
167 dict.try_into_directory_entry(self.namespace_scope.clone()).map_err(
168 |err| BuildNamespaceError::Conversion { path: path.clone(), err },
169 )?;
170 if entry.entry_info().type_() != fio::DirentType::Directory {
171 return Err(BuildNamespaceError::Conversion {
172 path: path.clone(),
173 err: sandbox::ConversionError::NotSupported,
174 });
175 }
176 serve_directory(
177 entry,
178 &self.namespace_scope,
179 fio::Flags::PROTOCOL_DIRECTORY
180 | fio::PERM_READABLE
181 | fio::Flags::PERM_INHERIT_WRITE
182 | fio::Flags::PERM_INHERIT_EXECUTE,
183 )
184 .map_err(|err| BuildNamespaceError::Serve { path: path.clone(), err })?
185 }
186 Capability::DictionaryRouter(router) => {
187 let (client, server) =
188 fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
189 let path = path.clone();
192 let scope = self.namespace_scope.clone();
193 let _ = self.namespace_scope.spawn(async move {
194 let res =
195 fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
196 if res.is_err() {
197 return;
198 }
199 match router.route(None, false).await {
200 Ok(RouterResponse::Capability(dictionary)) => {
201 let entry = match dictionary.try_into_directory_entry(scope.clone()) {
202 Ok(entry) => entry,
203 Err(e) => {
204 log::error!("failed to convert namespace dictionary at path {path} into dir entry: {e:?}");
205 return;
206 }
207 };
208 let client_end = serve_directory(
209 entry,
210 &scope,
211 fio::Flags::PROTOCOL_DIRECTORY
212 | fio::PERM_READABLE
213 | fio::Flags::PERM_INHERIT_WRITE
214 | fio::Flags::PERM_INHERIT_EXECUTE,
215 ).expect("failed to serve dictionary as directory");
216 let proxy = client_end.into_proxy();
217 fuchsia_fs::directory::clone_onto(&proxy, server).expect("failed to clone directory we are hosting");
218 }
219 Ok(RouterResponse::Unavailable) => {
220 let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
221 }
222 Ok(RouterResponse::Debug(_)) => {
223 panic!("debug response wasn't requested");
224 }
225 Err(e) => {
226 let _ = server.close_with_epitaph(e.as_zx_status());
230 }
231 }
232 });
233 client
234 }
235 _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
236 };
237 entries.push(NamespaceEntry { path, directory: client_end.into() })
238 }
239 let ns = entries.try_into()?;
240 Ok(ns)
241 }
242
243 fn make_dict_with_not_found_logging(&self, root_path: String) -> Dict {
244 let not_found = self.not_found.clone();
245 let new_dict = Dict::new_with_not_found(move |key| {
246 let requested_path = format!("{}/{}", root_path, key);
247 let _ = not_found.unbounded_send(requested_path);
250 });
251 new_dict
252 }
253}
254
255pub fn ignore_not_found() -> UnboundedSender<String> {
257 let (sender, _receiver) = unbounded();
258 sender
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use anyhow::Result;
265 use assert_matches::assert_matches;
266 use fidl::Peered;
267 use fidl::endpoints::{self, Proxy};
268 use fuchsia_fs::directory::DirEntry;
269 use futures::channel::mpsc;
270 use futures::{StreamExt, TryStreamExt};
271 use sandbox::{Connector, Directory, Receiver};
272 use std::sync::Arc;
273 use test_case::test_case;
274 use vfs::directory::entry::{DirectoryEntry, EntryInfo, GetEntryInfo, OpenRequest};
275 use vfs::remote::RemoteLike;
276 use vfs::{ObjectRequestRef, path, pseudo_directory};
277 use zx::AsHandleRef;
278 use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
279
280 fn multishot() -> (Connector, Receiver) {
281 let (receiver, sender) = Connector::new();
282 (sender, receiver)
283 }
284
285 fn connector_cap() -> Capability {
286 let (sender, _receiver) = multishot();
287 Capability::Connector(sender)
288 }
289
290 fn directory_cap() -> Capability {
291 let (client, _server) = endpoints::create_endpoints();
292 Capability::Directory(Directory::new(client))
293 }
294
295 fn ns_path(str: &str) -> NamespacePath {
296 str.parse().unwrap()
297 }
298
299 fn path(str: &str) -> Path {
300 str.parse().unwrap()
301 }
302
303 fn parents_valid(paths: Vec<&str>) -> Result<(), BuildNamespaceError> {
304 let scope = ExecutionScope::new();
305 let mut shadow = NamespaceBuilder::new(scope, ignore_not_found());
306 for p in paths {
307 shadow.add_object(connector_cap(), &path(p))?;
308 }
309 Ok(())
310 }
311
312 #[fuchsia::test]
313 async fn test_shadow() {
314 assert_matches!(parents_valid(vec!["/svc/foo/bar/Something", "/svc/Something"]), Err(_));
315 assert_matches!(parents_valid(vec!["/svc/Something", "/svc/foo/bar/Something"]), Err(_));
316 assert_matches!(parents_valid(vec!["/svc/Something", "/foo"]), Err(_));
317
318 assert_matches!(parents_valid(vec!["/foo/bar/a", "/foo/bar/b", "/foo/bar/c"]), Ok(()));
319 assert_matches!(parents_valid(vec!["/a", "/b", "/c"]), Ok(()));
320
321 let scope = ExecutionScope::new();
322 let mut shadow = NamespaceBuilder::new(scope, ignore_not_found());
323 shadow.add_object(connector_cap(), &path("/svc/foo")).unwrap();
324 assert_matches!(shadow.add_object(connector_cap(), &path("/svc/foo/bar")), Err(_));
325
326 let scope = ExecutionScope::new();
327 let mut not_shadow = NamespaceBuilder::new(scope, ignore_not_found());
328 not_shadow.add_object(connector_cap(), &path("/svc/foo")).unwrap();
329 assert_matches!(not_shadow.add_entry(directory_cap(), &ns_path("/svc2")), Ok(_));
330 }
331
332 #[fuchsia::test]
333 async fn test_duplicate_object() {
334 let scope = ExecutionScope::new();
335 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
336 namespace.add_object(connector_cap(), &path("/svc/a")).expect("");
337 assert_matches!(
339 namespace.add_object(connector_cap(), &path("/svc/a")),
340 Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
341 if path.to_string() == "/svc/a"
342 );
343 }
344
345 #[fuchsia::test]
346 async fn test_duplicate_entry() {
347 let scope = ExecutionScope::new();
348 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
349 namespace.add_entry(directory_cap(), &ns_path("/svc/a")).expect("");
350 assert_matches!(
352 namespace.add_entry(directory_cap(), &ns_path("/svc/a")),
353 Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
354 if path.to_string() == "/svc/a"
355 );
356 }
357
358 #[fuchsia::test]
359 async fn test_duplicate_object_and_entry() {
360 let scope = ExecutionScope::new();
361 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
362 namespace.add_object(connector_cap(), &path("/svc/a")).expect("");
363 assert_matches!(
364 namespace.add_entry(directory_cap(), &ns_path("/svc/a")),
365 Err(BuildNamespaceError::NamespaceError(NamespaceError::Shadow(path)))
366 if path.to_string() == "/svc/a"
367 );
368 }
369
370 #[fuchsia::test]
373 async fn test_duplicate_entry_at_object_parent() {
374 let scope = ExecutionScope::new();
375 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
376 namespace.add_object(connector_cap(), &path("/foo/bar")).expect("");
377 assert_matches!(
378 namespace.add_entry(directory_cap(), &ns_path("/foo")),
379 Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
380 if path.to_string() == "/foo"
381 );
382 }
383
384 #[fuchsia::test]
388 async fn test_duplicate_object_parent_at_entry() {
389 let scope = ExecutionScope::new();
390 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
391 namespace.add_entry(directory_cap(), &ns_path("/foo")).expect("");
392 assert_matches!(
393 namespace.add_object(connector_cap(), &path("/foo/bar")),
394 Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
395 if path.to_string() == "/foo/bar"
396 );
397 }
398
399 #[fuchsia::test]
400 async fn test_empty() {
401 let scope = ExecutionScope::new();
402 let namespace = NamespaceBuilder::new(scope, ignore_not_found());
403 let ns = namespace.serve().unwrap();
404 assert_eq!(ns.flatten().len(), 0);
405 }
406
407 #[fuchsia::test]
408 async fn test_one_connector_end_to_end() {
409 let (sender, receiver) = multishot();
410
411 let scope = ExecutionScope::new();
412 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
413 namespace.add_object(sender.into(), &path("/svc/a")).unwrap();
414 let ns = namespace.serve().unwrap();
415
416 let mut ns = ns.flatten();
417 assert_eq!(ns.len(), 1);
418 assert_eq!(ns[0].path.to_string(), "/svc");
419
420 let dir = ns.pop().unwrap().directory.into_proxy();
422 let entries = fuchsia_fs::directory::readdir(&dir).await.unwrap();
423 assert_eq!(
424 entries,
425 vec![DirEntry { name: "a".to_string(), kind: fio::DirentType::Service }]
426 );
427
428 let (client_end, server_end) = zx::Channel::create();
430 fdio::service_connect_at(&dir.into_channel().unwrap().into_zx_channel(), "a", server_end)
431 .unwrap();
432
433 let server_end: zx::Channel = receiver.receive().await.unwrap().channel.into();
435 client_end.signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
436 server_end.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST).unwrap();
437 }
438
439 #[fuchsia::test]
440 async fn test_two_connectors_in_same_namespace_entry() {
441 let scope = ExecutionScope::new();
442 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
443 namespace.add_object(connector_cap(), &path("/svc/a")).unwrap();
444 namespace.add_object(connector_cap(), &path("/svc/b")).unwrap();
445 let ns = namespace.serve().unwrap();
446
447 let mut ns = ns.flatten();
448 assert_eq!(ns.len(), 1);
449 assert_eq!(ns[0].path.to_string(), "/svc");
450
451 let dir = ns.pop().unwrap().directory.into_proxy();
453 let mut entries = fuchsia_fs::directory::readdir(&dir).await.unwrap();
454 let mut expectation = vec![
455 DirEntry { name: "a".to_string(), kind: fio::DirentType::Service },
456 DirEntry { name: "b".to_string(), kind: fio::DirentType::Service },
457 ];
458 entries.sort();
459 expectation.sort();
460 assert_eq!(entries, expectation);
461
462 drop(dir);
463 }
464
465 #[fuchsia::test]
466 async fn test_two_connectors_in_different_namespace_entries() {
467 let scope = ExecutionScope::new();
468 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
469 namespace.add_object(connector_cap(), &path("/svc1/a")).unwrap();
470 namespace.add_object(connector_cap(), &path("/svc2/b")).unwrap();
471 let ns = namespace.serve().unwrap();
472
473 let ns = ns.flatten();
474 assert_eq!(ns.len(), 2);
475 let (mut svc1, ns): (Vec<_>, Vec<_>) =
476 ns.into_iter().partition(|e| e.path.to_string() == "/svc1");
477 let (mut svc2, _ns): (Vec<_>, Vec<_>) =
478 ns.into_iter().partition(|e| e.path.to_string() == "/svc2");
479
480 {
482 let dir = svc1.pop().unwrap().directory.into_proxy();
483 assert_eq!(
484 fuchsia_fs::directory::readdir(&dir).await.unwrap(),
485 vec![DirEntry { name: "a".to_string(), kind: fio::DirentType::Service },]
486 );
487 }
488 {
489 let dir = svc2.pop().unwrap().directory.into_proxy();
490 assert_eq!(
491 fuchsia_fs::directory::readdir(&dir).await.unwrap(),
492 vec![DirEntry { name: "b".to_string(), kind: fio::DirentType::Service },]
493 );
494 }
495
496 drop(svc1);
497 drop(svc2);
498 }
499
500 #[fuchsia::test]
501 async fn test_not_found() {
502 let (not_found_sender, mut not_found_receiver) = unbounded();
503 let scope = ExecutionScope::new();
504 let mut namespace = NamespaceBuilder::new(scope, not_found_sender);
505 namespace.add_object(connector_cap(), &path("/svc/a")).unwrap();
506 let ns = namespace.serve().unwrap();
507
508 let mut ns = ns.flatten();
509 assert_eq!(ns.len(), 1);
510 assert_eq!(ns[0].path.to_string(), "/svc");
511
512 let dir = ns.pop().unwrap().directory.into_proxy();
513 let (client_end, server_end) = zx::Channel::create();
514 let _ = fdio::service_connect_at(
515 &dir.into_channel().unwrap().into_zx_channel(),
516 "non_existent",
517 server_end,
518 );
519
520 fasync::Channel::from_channel(client_end).on_closed().await.unwrap();
522
523 assert_eq!(not_found_receiver.next().await, Some("/svc/non_existent".to_string()));
525
526 drop(ns);
527 }
528
529 #[fuchsia::test]
530 async fn test_not_directory() {
531 let (not_found_sender, _) = unbounded();
532 let scope = ExecutionScope::new();
533 let mut namespace = NamespaceBuilder::new(scope, not_found_sender);
534 let (_, sender) = sandbox::Connector::new();
535 assert_matches!(
536 namespace.add_entry(sender.into(), &ns_path("/a")),
537 Err(BuildNamespaceError::NamespaceError(NamespaceError::EntryError(
538 EntryError::UnsupportedType
539 )))
540 );
541 }
542
543 #[test_case(fio::PERM_READABLE)]
544 #[test_case(fio::PERM_READABLE | fio::PERM_EXECUTABLE)]
545 #[test_case(fio::PERM_READABLE | fio::PERM_WRITABLE)]
546 #[test_case(fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE)]
547 #[fuchsia::test]
548 async fn test_directory_rights(rights: fio::Flags) {
549 let (open_tx, mut open_rx) = mpsc::channel::<()>(1);
550
551 struct MockDir {
552 tx: mpsc::Sender<()>,
553 rights: fio::Flags,
554 }
555 impl DirectoryEntry for MockDir {
556 fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
557 request.open_remote(self)
558 }
559 }
560 impl GetEntryInfo for MockDir {
561 fn entry_info(&self) -> EntryInfo {
562 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
563 }
564 }
565 impl RemoteLike for MockDir {
566 fn open(
567 self: Arc<Self>,
568 _scope: ExecutionScope,
569 relative_path: path::Path,
570 flags: fio::Flags,
571 _object_request: ObjectRequestRef<'_>,
572 ) -> Result<(), zx::Status> {
573 assert_eq!(relative_path.into_string(), "");
574 assert_eq!(flags, fio::Flags::PROTOCOL_DIRECTORY | self.rights);
575 self.tx.clone().try_send(()).unwrap();
576 Ok(())
577 }
578 }
579
580 let mock = Arc::new(MockDir { tx: open_tx, rights });
581
582 let fs = pseudo_directory! {
583 "foo" => mock,
584 };
585 let dir = Directory::from(vfs::directory::serve(fs, rights).into_client_end().unwrap());
586
587 let scope = ExecutionScope::new();
588 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
589 namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
590 let mut ns = namespace.serve().unwrap();
591 let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
592 let dir_proxy = dir_proxy.into_proxy();
593 let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
594 dir_proxy
595 .open(
596 "foo",
597 fio::Flags::PROTOCOL_DIRECTORY | rights,
598 &fio::Options::default(),
599 server_end.into_channel(),
600 )
601 .unwrap();
602
603 open_rx.next().await.unwrap();
605 }
606
607 #[fuchsia::test]
608 async fn test_directory_non_executable() {
609 let (open_tx, mut open_rx) = mpsc::channel::<()>(1);
610
611 struct MockDir(mpsc::Sender<()>);
612 impl DirectoryEntry for MockDir {
613 fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
614 request.open_remote(self)
615 }
616 }
617 impl GetEntryInfo for MockDir {
618 fn entry_info(&self) -> EntryInfo {
619 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
620 }
621 }
622 impl RemoteLike for MockDir {
623 fn open(
624 self: Arc<Self>,
625 _scope: ExecutionScope,
626 relative_path: path::Path,
627 flags: fio::Flags,
628 _object_request: ObjectRequestRef<'_>,
629 ) -> Result<(), zx::Status> {
630 assert_eq!(relative_path.into_string(), "");
631 assert_eq!(flags, fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE);
632 self.0.clone().try_send(()).unwrap();
633 Ok(())
634 }
635 }
636
637 let mock = Arc::new(MockDir(open_tx));
638
639 let fs = pseudo_directory! {
640 "foo" => mock,
641 };
642 let dir = Directory::from(
643 vfs::directory::serve(fs, fio::PERM_READABLE).into_client_end().unwrap(),
644 );
645
646 let scope = ExecutionScope::new();
647 let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
648 namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
649 let mut ns = namespace.serve().unwrap();
650 let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
651 let dir_proxy = dir_proxy.into_proxy();
652
653 let (node, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
655 dir_proxy
656 .open(
657 "foo",
658 fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE | fio::PERM_EXECUTABLE,
659 &fio::Options::default(),
660 server_end.into_channel(),
661 )
662 .unwrap();
663 let node = node.into_proxy();
664 let mut node = node.take_event_stream();
665 assert_matches!(
666 node.try_next().await,
667 Err(fidl::Error::ClientChannelClosed { status: zx::Status::ACCESS_DENIED, .. })
668 );
669
670 let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
672 dir_proxy
673 .open(
674 "foo",
675 fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
676 &fio::Options::default(),
677 server_end.into_channel(),
678 )
679 .unwrap();
680
681 open_rx.next().await.unwrap();
683 }
684}