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 = "NEXT")]
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 = "NEXT")]
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 = "NEXT")]
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 = "NEXT")]
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 = "NEXT"))]
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 = "NEXT"))]
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 = "NEXT")]
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 = "NEXT"))]
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 = "NEXT")]
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 = "NEXT"))]
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().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).handle(|req| {
352 self.directory.clone().open3(self.scope.clone(), Path::dot(), flags, req)
353 });
354 }
355
356 fn handle_deprecated_open(
357 &self,
358 mut flags: fio::OpenFlags,
359 path: String,
360 server_end: ServerEnd<fio::NodeMarker>,
361 ) {
362 let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
363
364 let path = match Path::validate_and_split(path) {
365 Ok(path) => path,
366 Err(status) => {
367 send_on_open_with_error(describe, server_end, status);
368 return;
369 }
370 };
371
372 if path.is_dir() {
373 flags |= fio::OpenFlags::DIRECTORY;
374 }
375
376 let flags = match check_child_connection_flags(self.options.to_io1(), flags) {
377 Ok(updated) => updated,
378 Err(status) => {
379 send_on_open_with_error(describe, server_end, status);
380 return;
381 }
382 };
383 if path.is_dot() {
384 if flags.intersects(fio::OpenFlags::NOT_DIRECTORY) {
385 send_on_open_with_error(describe, server_end, Status::INVALID_ARGS);
386 return;
387 }
388 if flags.intersects(fio::OpenFlags::CREATE_IF_ABSENT) {
389 send_on_open_with_error(describe, server_end, Status::ALREADY_EXISTS);
390 return;
391 }
392 }
393
394 let directory = self.directory.clone();
396 directory.open(self.scope.clone(), flags, path, server_end);
397 }
398
399 async fn handle_open(
400 &self,
401 path: String,
402 flags: fio::Flags,
403 object_request: ObjectRequestRef<'_>,
404 ) -> Result<(), Status> {
405 let path = Path::validate_and_split(path)?;
406
407 if let Some(rights) = flags.rights() {
409 if rights.intersects(!self.options.rights) {
410 return Err(Status::ACCESS_DENIED);
411 }
412 }
413
414 if !object_request.attributes().is_empty()
416 && !self.options.rights.contains(fio::Operations::GET_ATTRIBUTES)
417 {
418 return Err(Status::ACCESS_DENIED);
419 }
420
421 match flags.creation_mode() {
422 CreationMode::Never => {
423 if object_request.create_attributes().is_some() {
424 return Err(Status::INVALID_ARGS);
425 }
426 }
427 CreationMode::UnnamedTemporary | CreationMode::UnlinkableUnnamedTemporary => {
428 if !flags.intersects(fio::Flags::PROTOCOL_FILE) {
430 return Err(Status::NOT_SUPPORTED);
431 }
432 if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
434 return Err(Status::ACCESS_DENIED);
435 }
436 }
443 CreationMode::AllowExisting | CreationMode::Always => {
444 if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
446 return Err(Status::ACCESS_DENIED);
447 }
448
449 let protocol_flags = flags & fio::MASK_KNOWN_PROTOCOLS;
450 if protocol_flags.is_empty()
453 || (protocol_flags.bits() & (protocol_flags.bits() - 1)) != 0
454 {
455 return Err(Status::INVALID_ARGS);
456 }
457 if !protocol_flags
459 .intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_FILE)
460 {
461 return Err(Status::NOT_SUPPORTED);
462 }
463 }
464 }
465
466 if path.is_dot() && flags.creation_mode() == CreationMode::Always {
467 return Err(Status::ALREADY_EXISTS);
468 }
469
470 self.directory.clone().open3_async(self.scope.clone(), path, flags, object_request).await
471 }
472
473 async fn handle_read_dirents(&mut self, max_bytes: u64) -> (Status, Vec<u8>) {
474 async {
475 let (new_pos, sealed) =
476 self.directory.read_dirents(&self.seek, read_dirents::Sink::new(max_bytes)).await?;
477 self.seek = new_pos;
478 let read_dirents::Done { buf, status } = *sealed
479 .open()
480 .downcast::<read_dirents::Done>()
481 .map_err(|_: Box<dyn std::any::Any>| {
482 #[cfg(debug)]
483 panic!(
484 "`read_dirents()` returned a `dirents_sink::Sealed`
485 instance that is not an instance of the \
486 `read_dirents::Done`. This is a bug in the \
487 `read_dirents()` implementation."
488 );
489 Status::NOT_SUPPORTED
490 })?;
491 Ok((status, buf))
492 }
493 .await
494 .unwrap_or_else(|status| (status, Vec::new()))
495 }
496
497 async fn handle_link(
498 &self,
499 source_name: &str,
500 target_parent_token: fidl::Handle,
501 target_name: String,
502 ) -> Result<(), Status> {
503 if source_name.contains('/') || target_name.contains('/') {
504 return Err(Status::INVALID_ARGS);
505 }
506
507 if !self.options.rights.contains(fio::RW_STAR_DIR) {
513 return Err(Status::BAD_HANDLE);
514 }
515
516 let target_parent = self
517 .scope
518 .token_registry()
519 .get_owner(target_parent_token)?
520 .ok_or(Err(Status::NOT_FOUND))?;
521
522 target_parent.link(target_name, self.directory.clone().into_any(), source_name).await
523 }
524
525 fn handle_watch(
526 &mut self,
527 mask: fio::WatchMask,
528 watcher: DirectoryWatcher,
529 ) -> Result<(), Status> {
530 let directory = self.directory.clone();
531 directory.register_watcher(self.scope.clone(), mask, watcher)
532 }
533}
534
535impl<DirectoryType: Directory> Representation for BaseConnection<DirectoryType> {
536 type Protocol = fio::DirectoryMarker;
537
538 async fn get_representation(
539 &self,
540 requested_attributes: fio::NodeAttributesQuery,
541 ) -> Result<fio::Representation, Status> {
542 Ok(fio::Representation::Directory(fio::DirectoryInfo {
543 attributes: if requested_attributes.is_empty() {
544 None
545 } else {
546 Some(self.directory.get_attributes(requested_attributes).await?)
547 },
548 ..Default::default()
549 }))
550 }
551
552 async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
553 Ok(fio::NodeInfoDeprecated::Directory(fio::DirectoryObject))
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use crate::directory::immutable::Simple;
561 use assert_matches::assert_matches;
562 use fidl_fuchsia_io as fio;
563 use futures::prelude::*;
564
565 #[fuchsia::test]
566 async fn test_open_not_found() {
567 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
568
569 let dir = Simple::new();
570 dir.open(
571 ExecutionScope::new(),
572 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
573 Path::dot(),
574 ServerEnd::new(dir_server_end.into_channel()),
575 );
576
577 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
578
579 assert_matches!(
581 dir_proxy.deprecated_open(
582 fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
583 fio::ModeType::empty(),
584 "foo",
585 node_server_end
586 ),
587 Ok(())
588 );
589
590 assert_matches!(
592 node_proxy.query().await,
593 Err(fidl::Error::ClientChannelClosed {
594 status: Status::NOT_FOUND,
595 protocol_name: "fuchsia.io.Node",
596 ..
597 })
598 );
599 }
600
601 #[fuchsia::test]
602 async fn test_open_not_found_event_stream() {
603 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
604
605 let dir = Simple::new();
606 dir.open(
607 ExecutionScope::new(),
608 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
609 Path::dot(),
610 ServerEnd::new(dir_server_end.into_channel()),
611 );
612
613 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
614
615 assert_matches!(
617 dir_proxy.deprecated_open(
618 fio::OpenFlags::NOT_DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
619 fio::ModeType::empty(),
620 "foo",
621 node_server_end
622 ),
623 Ok(())
624 );
625
626 let mut event_stream = node_proxy.take_event_stream();
628 assert_matches!(
629 event_stream.try_next().await,
630 Err(fidl::Error::ClientChannelClosed {
631 status: Status::NOT_FOUND,
632 protocol_name: "fuchsia.io.Node",
633 ..
634 })
635 );
636 assert_matches!(event_stream.try_next().await, Ok(None));
637 }
638
639 #[fuchsia::test]
640 async fn test_open_with_describe_not_found() {
641 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
642
643 let dir = Simple::new();
644 dir.open(
645 ExecutionScope::new(),
646 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
647 Path::dot(),
648 ServerEnd::new(dir_server_end.into_channel()),
649 );
650
651 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
652
653 assert_matches!(
655 dir_proxy.deprecated_open(
656 fio::OpenFlags::DIRECTORY
657 | fio::OpenFlags::DESCRIBE
658 | fio::OpenFlags::RIGHT_READABLE,
659 fio::ModeType::empty(),
660 "foo",
661 node_server_end,
662 ),
663 Ok(())
664 );
665
666 assert_matches!(
668 node_proxy.query().await,
669 Err(fidl::Error::ClientChannelClosed {
670 status: Status::NOT_FOUND,
671 protocol_name: "fuchsia.io.Node",
672 ..
673 })
674 );
675 }
676
677 #[fuchsia::test]
678 async fn test_open_describe_not_found_event_stream() {
679 let (dir_proxy, dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
680
681 let dir = Simple::new();
682 dir.open(
683 ExecutionScope::new(),
684 fio::OpenFlags::DIRECTORY | fio::OpenFlags::RIGHT_READABLE,
685 Path::dot(),
686 ServerEnd::new(dir_server_end.into_channel()),
687 );
688
689 let (node_proxy, node_server_end) = fidl::endpoints::create_proxy();
690
691 assert_matches!(
693 dir_proxy.deprecated_open(
694 fio::OpenFlags::DIRECTORY
695 | fio::OpenFlags::DESCRIBE
696 | fio::OpenFlags::RIGHT_READABLE,
697 fio::ModeType::empty(),
698 "foo",
699 node_server_end,
700 ),
701 Ok(())
702 );
703
704 let mut event_stream = node_proxy.take_event_stream();
706 assert_matches!(
707 event_stream.try_next().await,
708 Ok(Some(fio::NodeEvent::OnOpen_ {
709 s,
710 info: None,
711 }))
712 if Status::from_raw(s) == Status::NOT_FOUND
713 );
714 assert_matches!(
715 event_stream.try_next().await,
716 Err(fidl::Error::ClientChannelClosed {
717 status: Status::NOT_FOUND,
718 protocol_name: "fuchsia.io.Node",
719 ..
720 })
721 );
722 assert_matches!(event_stream.try_next().await, Ok(None));
723 }
724}