1use crate::common::{inherit_rights_for_clone, send_on_open_with_error};
6use crate::directory::common::check_child_connection_flags;
7use crate::directory::entry_container::{Directory, DirectoryWatcher};
8use crate::directory::traversal_position::TraversalPosition;
9use crate::directory::{read_dirents, DirectoryOptions};
10use crate::execution_scope::{yield_to_executor, ExecutionScope};
11use crate::node::OpenNode;
12use crate::object_request::Representation;
13use crate::path::Path;
14
15use anyhow::Error;
16use fidl::endpoints::ServerEnd;
17use fidl_fuchsia_io as fio;
18use std::convert::TryInto as _;
19use storage_trace::{self as trace, TraceFutureExt};
20use zx_status::Status;
21
22use crate::common::CreationMode;
23use crate::{ObjectRequest, ObjectRequestRef, ProtocolsExt};
24
25pub enum ConnectionState {
27 Alive,
29 Closed,
31}
32
33pub(in crate::directory) struct BaseConnection<DirectoryType: Directory> {
38 pub(in crate::directory) scope: ExecutionScope,
41
42 pub(in crate::directory) directory: OpenNode<DirectoryType>,
43
44 pub(in crate::directory) options: DirectoryOptions,
46
47 seek: TraversalPosition,
62}
63
64impl<DirectoryType: Directory> BaseConnection<DirectoryType> {
65 pub(in crate::directory) fn new(
69 scope: ExecutionScope,
70 directory: OpenNode<DirectoryType>,
71 options: DirectoryOptions,
72 ) -> Self {
73 BaseConnection { scope, directory, options, seek: Default::default() }
74 }
75
76 pub(in crate::directory) async fn handle_request(
79 &mut self,
80 request: fio::DirectoryRequest,
81 ) -> Result<ConnectionState, Error> {
82 match request {
83 #[cfg(fuchsia_api_level_at_least = "26")]
84 fio::DirectoryRequest::DeprecatedClone { flags, object, control_handle: _ } => {
85 trace::duration!(c"storage", c"Directory::DeprecatedClone");
86 self.handle_deprecated_clone(flags, object);
87 }
88 #[cfg(not(fuchsia_api_level_at_least = "26"))]
89 fio::DirectoryRequest::Clone { flags, object, control_handle: _ } => {
90 trace::duration!(c"storage", c"Directory::Clone");
91 self.handle_deprecated_clone(flags, object);
92 }
93 #[cfg(fuchsia_api_level_at_least = "26")]
94 fio::DirectoryRequest::Clone { request, control_handle: _ } => {
95 trace::duration!(c"storage", c"Directory::Clone");
96 self.handle_clone(request.into_channel());
97 }
98 #[cfg(not(fuchsia_api_level_at_least = "26"))]
99 fio::DirectoryRequest::Clone2 { request, control_handle: _ } => {
100 trace::duration!(c"storage", c"Directory::Clone2");
101 self.handle_clone(request.into_channel());
102 }
103 fio::DirectoryRequest::Close { responder } => {
104 trace::duration!(c"storage", c"Directory::Close");
105 responder.send(Ok(()))?;
106 return Ok(ConnectionState::Closed);
107 }
108 fio::DirectoryRequest::GetConnectionInfo { responder } => {
109 trace::duration!(c"storage", c"Directory::GetConnectionInfo");
110 responder.send(fio::ConnectionInfo {
111 rights: Some(self.options.rights),
112 ..Default::default()
113 })?;
114 }
115 fio::DirectoryRequest::GetAttr { responder } => {
116 async move {
117 let (status, attrs) = crate::common::io2_to_io1_attrs(
118 self.directory.as_ref(),
119 self.options.rights,
120 )
121 .await;
122 responder.send(status.into_raw(), &attrs)
123 }
124 .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttr"))
125 .await?;
126 }
127 fio::DirectoryRequest::GetAttributes { query, responder } => {
128 async move {
129 let attrs = self.directory.get_attributes(query).await;
131 responder.send(
132 attrs
133 .as_ref()
134 .map(|attrs| (&attrs.mutable_attributes, &attrs.immutable_attributes))
135 .map_err(|status| status.into_raw()),
136 )
137 }
138 .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttributes"))
139 .await?;
140 }
141 fio::DirectoryRequest::UpdateAttributes { payload: _, responder } => {
142 trace::duration!(c"storage", c"Directory::UpdateAttributes");
143 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
145 }
146 fio::DirectoryRequest::ListExtendedAttributes { iterator, .. } => {
147 trace::duration!(c"storage", c"Directory::ListExtendedAttributes");
148 iterator.close_with_epitaph(Status::NOT_SUPPORTED)?;
149 }
150 fio::DirectoryRequest::GetExtendedAttribute { responder, .. } => {
151 trace::duration!(c"storage", c"Directory::GetExtendedAttribute");
152 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
153 }
154 fio::DirectoryRequest::SetExtendedAttribute { responder, .. } => {
155 trace::duration!(c"storage", c"Directory::SetExtendedAttribute");
156 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
157 }
158 fio::DirectoryRequest::RemoveExtendedAttribute { responder, .. } => {
159 trace::duration!(c"storage", c"Directory::RemoveExtendedAttribute");
160 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
161 }
162 #[cfg(fuchsia_api_level_at_least = "27")]
163 fio::DirectoryRequest::GetFlags { responder } => {
164 trace::duration!(c"storage", c"Directory::GetFlags");
165 responder.send(Ok(fio::Flags::from(&self.options)))?;
166 }
167 #[cfg(fuchsia_api_level_at_least = "27")]
168 fio::DirectoryRequest::SetFlags { flags: _, responder } => {
169 trace::duration!(c"storage", c"Directory::SetFlags");
170 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
171 }
172 #[cfg(fuchsia_api_level_at_least = "27")]
173 fio::DirectoryRequest::DeprecatedGetFlags { responder } => {
174 trace::duration!(c"storage", c"Directory::DeprecatedGetFlags");
175 responder.send(Status::OK.into_raw(), self.options.to_io1())?;
176 }
177 #[cfg(fuchsia_api_level_at_least = "27")]
178 fio::DirectoryRequest::DeprecatedSetFlags { flags: _, responder } => {
179 trace::duration!(c"storage", c"Directory::DeprecatedSetFlags");
180 responder.send(Status::NOT_SUPPORTED.into_raw())?;
181 }
182 #[cfg(not(fuchsia_api_level_at_least = "27"))]
183 fio::DirectoryRequest::GetFlags { responder } => {
184 trace::duration!(c"storage", c"Directory::GetFlags");
185 responder.send(Status::OK.into_raw(), self.options.to_io1())?;
186 }
187 #[cfg(not(fuchsia_api_level_at_least = "27"))]
188 fio::DirectoryRequest::SetFlags { flags: _, responder } => {
189 trace::duration!(c"storage", c"Directory::SetFlags");
190 responder.send(Status::NOT_SUPPORTED.into_raw())?;
191 }
192 #[cfg(fuchsia_api_level_at_least = "27")]
193 fio::DirectoryRequest::DeprecatedOpen {
194 flags,
195 mode: _,
196 path,
197 object,
198 control_handle: _,
199 } => {
200 {
201 trace::duration!(c"storage", c"Directory::Open");
202 self.handle_deprecated_open(flags, path, object);
203 }
204 yield_to_executor().await;
207 }
208 #[cfg(not(fuchsia_api_level_at_least = "27"))]
209 fio::DirectoryRequest::Open { flags, mode: _, path, object, control_handle: _ } => {
210 {
211 trace::duration!(c"storage", c"Directory::Open");
212 self.handle_deprecated_open(flags, path, object);
213 }
214 yield_to_executor().await;
217 }
218 fio::DirectoryRequest::AdvisoryLock { request: _, responder } => {
219 trace::duration!(c"storage", c"Directory::AdvisoryLock");
220 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
221 }
222 fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
223 async move {
224 let (status, entries) = self.handle_read_dirents(max_bytes).await;
225 responder.send(status.into_raw(), entries.as_slice())
226 }
227 .trace(trace::trace_future_args!(c"storage", c"Directory::ReadDirents"))
228 .await?;
229 }
230 fio::DirectoryRequest::Rewind { responder } => {
231 trace::duration!(c"storage", c"Directory::Rewind");
232 self.seek = Default::default();
233 responder.send(Status::OK.into_raw())?;
234 }
235 fio::DirectoryRequest::Link { src, dst_parent_token, dst, responder } => {
236 async move {
237 let status: Status = self.handle_link(&src, dst_parent_token, dst).await.into();
238 responder.send(status.into_raw())
239 }
240 .trace(trace::trace_future_args!(c"storage", c"Directory::Link"))
241 .await?;
242 }
243 fio::DirectoryRequest::Watch { mask, options, watcher, responder } => {
244 trace::duration!(c"storage", c"Directory::Watch");
245 let status = if options != 0 {
246 Status::INVALID_ARGS
247 } else {
248 let watcher = watcher.try_into()?;
249 self.handle_watch(mask, watcher).into()
250 };
251 responder.send(status.into_raw())?;
252 }
253 fio::DirectoryRequest::Query { responder } => {
254 let () = responder.send(fio::DIRECTORY_PROTOCOL_NAME.as_bytes())?;
255 }
256 fio::DirectoryRequest::QueryFilesystem { responder } => {
257 trace::duration!(c"storage", c"Directory::QueryFilesystem");
258 match self.directory.query_filesystem() {
259 Err(status) => responder.send(status.into_raw(), None)?,
260 Ok(info) => responder.send(0, Some(&info))?,
261 }
262 }
263 fio::DirectoryRequest::Unlink { name: _, options: _, responder } => {
264 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
265 }
266 fio::DirectoryRequest::GetToken { responder } => {
267 responder.send(Status::NOT_SUPPORTED.into_raw(), None)?;
268 }
269 fio::DirectoryRequest::Rename { src: _, dst_parent_token: _, dst: _, responder } => {
270 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
271 }
272 fio::DirectoryRequest::SetAttr { flags: _, attributes: _, responder } => {
273 responder.send(Status::NOT_SUPPORTED.into_raw())?;
274 }
275 fio::DirectoryRequest::Sync { responder } => {
276 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
277 }
278 fio::DirectoryRequest::CreateSymlink { responder, .. } => {
279 responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
280 }
281 #[cfg(fuchsia_api_level_at_least = "27")]
282 fio::DirectoryRequest::Open { path, mut flags, options, object, control_handle: _ } => {
283 {
284 if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
286 flags &= !fio::Flags::PERM_INHERIT_WRITE;
287 }
288 if !self.options.rights.contains(fio::Rights::EXECUTE) {
289 flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
290 }
291
292 ObjectRequest::new(flags, &options, object)
293 .handle_async(async |req| self.handle_open(path, flags, req).await)
294 .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
295 .await;
296 }
297 yield_to_executor().await;
300 }
301 #[cfg(not(fuchsia_api_level_at_least = "27"))]
302 fio::DirectoryRequest::Open3 {
303 path,
304 mut flags,
305 options,
306 object,
307 control_handle: _,
308 } => {
309 {
310 if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
312 flags &= !fio::Flags::PERM_INHERIT_WRITE;
313 }
314 if !self.options.rights.contains(fio::Rights::EXECUTE) {
315 flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
316 }
317
318 ObjectRequest::new(flags, &options, object)
319 .handle_async(async |req| self.handle_open(path, flags, req).await)
320 .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
321 .await;
322 }
323 yield_to_executor().await;
326 }
327 fio::DirectoryRequest::_UnknownMethod { .. } => (),
328 }
329 Ok(ConnectionState::Alive)
330 }
331
332 fn handle_deprecated_clone(
333 &self,
334 flags: fio::OpenFlags,
335 server_end: ServerEnd<fio::NodeMarker>,
336 ) {
337 let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
338 let flags = match inherit_rights_for_clone(self.options.to_io1(), flags) {
339 Ok(updated) => updated,
340 Err(status) => {
341 send_on_open_with_error(describe, server_end, status);
342 return;
343 }
344 };
345
346 self.directory.clone().deprecated_open(self.scope.clone(), flags, Path::dot(), server_end);
347 }
348
349 fn handle_clone(&mut self, object: fidl::Channel) {
350 let flags = fio::Flags::from(&self.options);
351 ObjectRequest::new(flags, &Default::default(), object)
352 .handle(|req| self.directory.clone().open(self.scope.clone(), Path::dot(), flags, req));
353 }
354
355 fn handle_deprecated_open(
356 &self,
357 mut flags: fio::OpenFlags,
358 path: String,
359 server_end: ServerEnd<fio::NodeMarker>,
360 ) {
361 let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
362
363 let path = match Path::validate_and_split(path) {
364 Ok(path) => path,
365 Err(status) => {
366 send_on_open_with_error(describe, server_end, status);
367 return;
368 }
369 };
370
371 if path.is_dir() {
372 flags |= fio::OpenFlags::DIRECTORY;
373 }
374
375 let flags = match check_child_connection_flags(self.options.to_io1(), flags) {
376 Ok(updated) => updated,
377 Err(status) => {
378 send_on_open_with_error(describe, server_end, status);
379 return;
380 }
381 };
382 if path.is_dot() {
383 if flags.intersects(fio::OpenFlags::NOT_DIRECTORY) {
384 send_on_open_with_error(describe, server_end, Status::INVALID_ARGS);
385 return;
386 }
387 if flags.intersects(fio::OpenFlags::CREATE_IF_ABSENT) {
388 send_on_open_with_error(describe, server_end, Status::ALREADY_EXISTS);
389 return;
390 }
391 }
392
393 let directory = self.directory.clone();
395 directory.deprecated_open(self.scope.clone(), flags, path, server_end);
396 }
397
398 async fn handle_open(
399 &self,
400 path: String,
401 flags: fio::Flags,
402 object_request: ObjectRequestRef<'_>,
403 ) -> Result<(), Status> {
404 let path = Path::validate_and_split(path)?;
405
406 if let Some(rights) = flags.rights() {
408 if rights.intersects(!self.options.rights) {
409 return Err(Status::ACCESS_DENIED);
410 }
411 }
412
413 if !object_request.attributes().is_empty()
415 && !self.options.rights.contains(fio::Operations::GET_ATTRIBUTES)
416 {
417 return Err(Status::ACCESS_DENIED);
418 }
419
420 match flags.creation_mode() {
421 CreationMode::Never => {
422 if object_request.create_attributes().is_some() {
423 return Err(Status::INVALID_ARGS);
424 }
425 }
426 CreationMode::UnnamedTemporary | CreationMode::UnlinkableUnnamedTemporary => {
427 if !flags.intersects(fio::Flags::PROTOCOL_FILE) {
429 return Err(Status::NOT_SUPPORTED);
430 }
431 if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
433 return Err(Status::ACCESS_DENIED);
434 }
435 }
442 CreationMode::AllowExisting | CreationMode::Always => {
443 if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
445 return Err(Status::ACCESS_DENIED);
446 }
447
448 let protocol_flags = flags & fio::MASK_KNOWN_PROTOCOLS;
449 if protocol_flags.is_empty()
452 || (protocol_flags.bits() & (protocol_flags.bits() - 1)) != 0
453 {
454 return Err(Status::INVALID_ARGS);
455 }
456 if !protocol_flags
458 .intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_FILE)
459 {
460 return Err(Status::NOT_SUPPORTED);
461 }
462 }
463 }
464
465 if path.is_dot() && flags.creation_mode() == CreationMode::Always {
466 return Err(Status::ALREADY_EXISTS);
467 }
468
469 self.directory.clone().open_async(self.scope.clone(), path, flags, object_request).await
470 }
471
472 async fn handle_read_dirents(&mut self, max_bytes: u64) -> (Status, Vec<u8>) {
473 async {
474 let (new_pos, sealed) =
475 self.directory.read_dirents(&self.seek, read_dirents::Sink::new(max_bytes)).await?;
476 self.seek = new_pos;
477 let read_dirents::Done { buf, status } = *sealed
478 .open()
479 .downcast::<read_dirents::Done>()
480 .map_err(|_: Box<dyn std::any::Any>| {
481 #[cfg(debug)]
482 panic!(
483 "`read_dirents()` returned a `dirents_sink::Sealed`
484 instance that is not an instance of the \
485 `read_dirents::Done`. This is a bug in the \
486 `read_dirents()` implementation."
487 );
488 Status::NOT_SUPPORTED
489 })?;
490 Ok((status, buf))
491 }
492 .await
493 .unwrap_or_else(|status| (status, Vec::new()))
494 }
495
496 async fn handle_link(
497 &self,
498 source_name: &str,
499 target_parent_token: fidl::Handle,
500 target_name: String,
501 ) -> Result<(), Status> {
502 if source_name.contains('/') || target_name.contains('/') {
503 return Err(Status::INVALID_ARGS);
504 }
505
506 if !self.options.rights.contains(fio::RW_STAR_DIR) {
512 return Err(Status::BAD_HANDLE);
513 }
514
515 let target_parent = self
516 .scope
517 .token_registry()
518 .get_owner(target_parent_token)?
519 .ok_or(Err(Status::NOT_FOUND))?;
520
521 target_parent.link(target_name, self.directory.clone().into_any(), source_name).await
522 }
523
524 fn handle_watch(
525 &mut self,
526 mask: fio::WatchMask,
527 watcher: DirectoryWatcher,
528 ) -> Result<(), Status> {
529 let directory = self.directory.clone();
530 directory.register_watcher(self.scope.clone(), mask, watcher)
531 }
532}
533
534impl<DirectoryType: Directory> Representation for BaseConnection<DirectoryType> {
535 type Protocol = fio::DirectoryMarker;
536
537 async fn get_representation(
538 &self,
539 requested_attributes: fio::NodeAttributesQuery,
540 ) -> Result<fio::Representation, Status> {
541 Ok(fio::Representation::Directory(fio::DirectoryInfo {
542 attributes: if requested_attributes.is_empty() {
543 None
544 } else {
545 Some(self.directory.get_attributes(requested_attributes).await?)
546 },
547 ..Default::default()
548 }))
549 }
550
551 async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
552 Ok(fio::NodeInfoDeprecated::Directory(fio::DirectoryObject))
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::directory::immutable::Simple;
560 use assert_matches::assert_matches;
561 use fidl_fuchsia_io as fio;
562 use futures::prelude::*;
563
564 #[fuchsia::test]
565 async fn test_open_not_found() {
566 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
567
568 let dir = Simple::new();
569 dir.deprecated_open(
570 ExecutionScope::new(),
571 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
572 Path::dot(),
573 ServerEnd::new(dir_server_end.into_channel()),
574 );
575
576 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
577
578 assert_matches!(
580 dir_proxy.deprecated_open(
581 fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
582 fio::ModeType::empty(),
583 "foo",
584 node_server_end
585 ),
586 Ok(())
587 );
588
589 assert_matches!(
591 node_proxy.query().await,
592 Err(fidl::Error::ClientChannelClosed {
593 status: Status::NOT_FOUND,
594 protocol_name: "fuchsia.io.Node",
595 ..
596 })
597 );
598 }
599
600 #[fuchsia::test]
601 async fn test_open_not_found_event_stream() {
602 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
603
604 let dir = Simple::new();
605 dir.deprecated_open(
606 ExecutionScope::new(),
607 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
608 Path::dot(),
609 ServerEnd::new(dir_server_end.into_channel()),
610 );
611
612 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
613
614 assert_matches!(
616 dir_proxy.deprecated_open(
617 fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
618 fio::ModeType::empty(),
619 "foo",
620 node_server_end
621 ),
622 Ok(())
623 );
624
625 let mut event_stream = node_proxy.take_event_stream();
627 assert_matches!(
628 event_stream.try_next().await,
629 Err(fidl::Error::ClientChannelClosed {
630 status: Status::NOT_FOUND,
631 protocol_name: "fuchsia.io.Node",
632 ..
633 })
634 );
635 assert_matches!(event_stream.try_next().await, Ok(None));
636 }
637
638 #[fuchsia::test]
639 async fn test_open_with_describe_not_found() {
640 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
641
642 let dir = Simple::new();
643 dir.deprecated_open(
644 ExecutionScope::new(),
645 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
646 Path::dot(),
647 ServerEnd::new(dir_server_end.into_channel()),
648 );
649
650 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
651
652 assert_matches!(
654 dir_proxy.deprecated_open(
655 fio::OpenFlags::DIRECTORY
656 | fio::OpenFlags::DESCRIBE
657 | fio::OpenFlags::RIGHT_READABLE,
658 fio::ModeType::empty(),
659 "foo",
660 node_server_end,
661 ),
662 Ok(())
663 );
664
665 assert_matches!(
667 node_proxy.query().await,
668 Err(fidl::Error::ClientChannelClosed {
669 status: Status::NOT_FOUND,
670 protocol_name: "fuchsia.io.Node",
671 ..
672 })
673 );
674 }
675
676 #[fuchsia::test]
677 async fn test_open_describe_not_found_event_stream() {
678 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
679
680 let dir = Simple::new();
681 dir.deprecated_open(
682 ExecutionScope::new(),
683 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
684 Path::dot(),
685 ServerEnd::new(dir_server_end.into_channel()),
686 );
687
688 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
689
690 assert_matches!(
692 dir_proxy.deprecated_open(
693 fio::OpenFlags::DIRECTORY
694 | fio::OpenFlags::DESCRIBE
695 | fio::OpenFlags::RIGHT_READABLE,
696 fio::ModeType::empty(),
697 "foo",
698 node_server_end,
699 ),
700 Ok(())
701 );
702
703 let mut event_stream = node_proxy.take_event_stream();
705 assert_matches!(
706 event_stream.try_next().await,
707 Ok(Some(fio::NodeEvent::OnOpen_ {
708 s,
709 info: None,
710 }))
711 if Status::from_raw(s) == Status::NOT_FOUND
712 );
713 assert_matches!(
714 event_stream.try_next().await,
715 Err(fidl::Error::ClientChannelClosed {
716 status: Status::NOT_FOUND,
717 protocol_name: "fuchsia.io.Node",
718 ..
719 })
720 );
721 assert_matches!(event_stream.try_next().await, Ok(None));
722 }
723}