| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::model::{ |
| actions::{shutdown::do_shutdown, Action, ActionKey, ShutdownType}, |
| component::instance::InstanceState, |
| component::ComponentInstance, |
| error::{ActionError, UnresolveActionError}, |
| hooks::{Event, EventPayload}, |
| }, |
| async_trait::async_trait, |
| std::sync::Arc, |
| }; |
| |
| /// Returns a resolved component to the discovered state. The result is that the component can be |
| /// restarted, updating both the code and the manifest with destroying its resources. Unresolve can |
| /// only be applied to a resolved, stopped, component. This action supports the `ffx component |
| /// reload` command. |
| pub struct UnresolveAction {} |
| |
| impl UnresolveAction { |
| pub fn new() -> Self { |
| Self {} |
| } |
| } |
| |
| #[async_trait] |
| impl Action for UnresolveAction { |
| async fn handle(self, component: Arc<ComponentInstance>) -> Result<(), ActionError> { |
| do_unresolve(&component).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Unresolve |
| } |
| } |
| |
| // Implement the UnresolveAction by resetting the state from unresolved to unresolved and emitting |
| // an Unresolved event. |
| async fn do_unresolve(component: &Arc<ComponentInstance>) -> Result<(), ActionError> { |
| // We want to shut down the component without invoking a full shutdown action, as the |
| // unresolved state of a component is viewed as unreachable from the shutdown state by the |
| // actions system. |
| // |
| // TODO(fxbug.dev/TODO): refactor shutdown such that the component is not put in an |
| // InstanceState::Shutdown when invoked from here. This will allow us to remove |
| // UnresolvedInstanceState from InstanceState::Shutdown. |
| do_shutdown(component, ShutdownType::Instance).await?; |
| |
| // Move this component back to the Unresolved state. The state may have changed during the time |
| // between the do_shutdown call and now, so recheck here. |
| { |
| let mut state = component.lock_state().await; |
| if let InstanceState::Destroyed = &*state { |
| return Err(UnresolveActionError::InstanceDestroyed { |
| moniker: component.moniker.clone(), |
| } |
| .into()); |
| } |
| if !state.is_shut_down() { |
| return Err(UnresolveActionError::InstanceRunning { |
| moniker: component.moniker.clone(), |
| } |
| .into()); |
| } |
| state.replace(|instance_state| match instance_state { |
| InstanceState::Shutdown(_, unresolved_state) => { |
| InstanceState::Unresolved(unresolved_state) |
| } |
| instance_state => panic!( |
| "component {} was shutdown, but then moved to unexpected state {:?}", |
| component.moniker, instance_state |
| ), |
| }); |
| }; |
| |
| // Drop any tasks that might be running in the component's execution scope. We don't need to |
| // wait for old tasks to complete naturally; we just force them to stop. It's possible that |
| // waiting for them to complete would block forever anyway e.g. routing a storage capability can |
| // block if the backing directory isn't responding for some reason. |
| component.execution_scope.force_shutdown(); |
| component.execution_scope.wait().await; |
| component.execution_scope.resurrect(); |
| |
| let event = Event::new(&component, EventPayload::Unresolved); |
| component.hooks.dispatch(&event).await; |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| pub mod tests { |
| use { |
| crate::model::{ |
| actions::test_utils::{is_destroyed, is_discovered, is_resolved, is_shutdown}, |
| actions::{ActionSet, ShutdownAction, ShutdownType, UnresolveAction}, |
| component::{ComponentInstance, StartReason}, |
| error::{ActionError, UnresolveActionError}, |
| events::{registry::EventSubscription, stream::EventStream}, |
| hooks::EventType, |
| testing::test_helpers::{component_decl_with_test_runner, ActionsTest}, |
| }, |
| assert_matches::assert_matches, |
| cm_rust::{Availability, UseEventStreamDecl, UseSource}, |
| cm_rust_testing::*, |
| cm_types::Name, |
| fidl_fuchsia_component_decl as fdecl, fuchsia_async as fasync, |
| moniker::{Moniker, MonikerBase}, |
| std::sync::Arc, |
| }; |
| |
| /// Check unresolve for _recursive_ case. The system has a root with the child `a` and `a` has |
| /// descendants as shown in the diagram below. |
| /// a |
| /// \ |
| /// b |
| /// \ |
| /// c |
| /// |
| /// Also tests UnresolveAction on InstanceState::Unresolved. |
| #[fuchsia::test] |
| async fn unresolve_action_recursive_test() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().child_default("a").build()), |
| ("a", ComponentDeclBuilder::new().child_default("b").build()), |
| ("b", ComponentDeclBuilder::new().child_default("c").build()), |
| ("c", component_decl_with_test_runner()), |
| ]; |
| // Resolve components without starting them. |
| let test = ActionsTest::new("root", components, None).await; |
| let component_root = test.look_up(Moniker::root()).await; |
| let component_a = test.look_up(vec!["a"].try_into().unwrap()).await; |
| let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await; |
| let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await; |
| assert!(is_resolved(&component_root).await); |
| assert!(is_resolved(&component_a).await); |
| assert!(is_resolved(&component_b).await); |
| assert!(is_resolved(&component_c).await); |
| |
| // Unresolve, recursively. |
| ActionSet::register(component_a.clone(), UnresolveAction::new()) |
| .await |
| .expect("unresolve failed"); |
| assert!(is_resolved(&component_root).await); |
| // Component a is back in the discovered state |
| assert!(is_discovered(&component_a).await); |
| // The original descendents of component a have been placed in a Shutdown state. |
| assert!(is_shutdown(&component_b).await); |
| assert!(is_shutdown(&component_c).await); |
| // Component a itself is now unresolved, and thus does not have children anymore. |
| assert_matches!(component_a.find(&vec!["b"].try_into().unwrap()).await, None); |
| |
| // Unresolve again, which is ok because UnresolveAction is idempotent. |
| assert_matches!( |
| ActionSet::register(component_a.clone(), UnresolveAction::new()).await, |
| Ok(()) |
| ); |
| // Still Discovered. |
| assert!(is_discovered(&component_a).await); |
| } |
| |
| /// Check unresolve with recursion on eagerly-loaded peer children. The system has a root with |
| /// the child `a` and `a` has descendants as shown in the diagram below. |
| /// a |
| /// \ |
| /// b |
| /// / \ |
| /// c d |
| #[fuchsia::test] |
| async fn unresolve_action_recursive_test2() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().child_default("a").build()), |
| ( |
| "a", |
| ComponentDeclBuilder::new() |
| .child(ChildBuilder::new().name("b").eager().build()) |
| .build(), |
| ), |
| ( |
| "b", |
| ComponentDeclBuilder::new() |
| .child(ChildBuilder::new().name("c").eager().build()) |
| .child(ChildBuilder::new().name("d").eager().build()) |
| .build(), |
| ), |
| ("c", component_decl_with_test_runner()), |
| ("d", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| |
| // Resolve each component. |
| test.look_up(Moniker::root()).await; |
| let component_a = test.look_up(vec!["a"].try_into().unwrap()).await; |
| let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await; |
| let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await; |
| let component_d = test.look_up(vec!["a", "b", "d"].try_into().unwrap()).await; |
| |
| // Unresolve, recursively. |
| ActionSet::register(component_a.clone(), UnresolveAction::new()) |
| .await |
| .expect("unresolve failed"); |
| |
| // Component a is back in the discovered state |
| assert!(is_discovered(&component_a).await); |
| // The original descendents of component a have been placed in a Shutdown state. |
| assert!(is_shutdown(&component_b).await); |
| assert!(is_shutdown(&component_c).await); |
| assert!(is_shutdown(&component_d).await); |
| // Component a itself is now unresolved, and thus does not have children anymore. |
| assert_matches!(component_a.find(&vec!["b"].try_into().unwrap()).await, None); |
| } |
| |
| async fn setup_unresolve_test_event_stream( |
| test: &ActionsTest, |
| event_types: Vec<EventType>, |
| ) -> EventStream { |
| let events: Vec<_> = event_types.into_iter().map(|e| e.into()).collect(); |
| let mut event_source = |
| test.builtin_environment.lock().await.event_source_factory.create_for_above_root(); |
| let event_stream = event_source |
| .subscribe( |
| events |
| .into_iter() |
| .map(|event: Name| EventSubscription { |
| event_name: UseEventStreamDecl { |
| source_name: event, |
| source: UseSource::Parent, |
| scope: None, |
| target_path: "/svc/fuchsia.component.EventStream".parse().unwrap(), |
| filter: None, |
| availability: Availability::Required, |
| }, |
| }) |
| .collect(), |
| ) |
| .await |
| .expect("subscribe to event stream"); |
| let model = test.model.clone(); |
| let input = test.builtin_environment.lock().await.root_component_input.clone(); |
| fasync::Task::spawn(async move { model.start(input).await }).detach(); |
| event_stream |
| } |
| |
| #[fuchsia::test] |
| async fn unresolve_action_registers_unresolve_event_test() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().child_default("a").build()), |
| ("a", ComponentDeclBuilder::new().child_default("b").build()), |
| ("b", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| test.start(Moniker::root()).await; |
| let component_a = test.start(vec!["a"].try_into().unwrap()).await; |
| test.start(vec!["a", "b"].try_into().unwrap()).await; |
| |
| let mut event_stream = |
| setup_unresolve_test_event_stream(&test, vec![EventType::Unresolved]).await; |
| |
| // Register the UnresolveAction. |
| let nf = { |
| let mut actions = component_a.lock_actions().await; |
| actions.register_no_wait(&component_a, UnresolveAction::new()).await |
| }; |
| |
| // Component a is then unresolved. |
| event_stream |
| .wait_until(EventType::Unresolved, vec!["a"].try_into().unwrap()) |
| .await |
| .unwrap(); |
| nf.await.unwrap(); |
| |
| // Now attempt to unresolve again with another UnresolveAction. |
| let nf2 = { |
| let mut actions = component_a.lock_actions().await; |
| actions.register_no_wait(&component_a, UnresolveAction::new()).await |
| }; |
| // The component is not resolved anymore, so the unresolve will have no effect. |
| nf2.await.unwrap(); |
| assert!(is_discovered(&component_a).await); |
| } |
| |
| /// Start a collection with the given durability. The system has a root with a container that |
| /// has a collection containing children `a` and `b` as shown in the diagram below. |
| /// root |
| /// \ |
| /// container |
| /// / \ |
| /// coll:a coll:b |
| /// |
| async fn start_collection( |
| durability: fdecl::Durability, |
| ) -> (ActionsTest, Arc<ComponentInstance>, Arc<ComponentInstance>, Arc<ComponentInstance>) { |
| let collection = |
| CollectionBuilder::new().name("coll").durability(durability).allow_long_names().build(); |
| |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().child_default("container").build()), |
| ("container", ComponentDeclBuilder::new().collection(collection).build()), |
| ("a", component_decl_with_test_runner()), |
| ("b", component_decl_with_test_runner()), |
| ]; |
| let test = |
| ActionsTest::new("root", components, Some(vec!["container"].try_into().unwrap())).await; |
| let root = test.model.root(); |
| |
| // Create dynamic instances in "coll". |
| test.create_dynamic_child("coll", "a").await; |
| test.create_dynamic_child("coll", "b").await; |
| |
| // Start the components. This should cause them to have an `Execution`. |
| let component_container = test.look_up(vec!["container"].try_into().unwrap()).await; |
| let component_a = test.look_up(vec!["container", "coll:a"].try_into().unwrap()).await; |
| let component_b = test.look_up(vec!["container", "coll:b"].try_into().unwrap()).await; |
| root.start_instance(&component_container.moniker, &StartReason::Eager) |
| .await |
| .expect("could not start container"); |
| root.start_instance(&component_a.moniker, &StartReason::Eager) |
| .await |
| .expect("could not start coll:a"); |
| root.start_instance(&component_b.moniker, &StartReason::Eager) |
| .await |
| .expect("could not start coll:b"); |
| assert!(component_container.is_started().await); |
| assert!(is_resolved(&component_a).await); |
| assert!(is_resolved(&component_b).await); |
| assert!(component_a.is_started().await); |
| assert!(component_b.is_started().await); |
| (test, component_container, component_a, component_b) |
| } |
| |
| /// Test a collection with the given durability. |
| /// Also tests UnresolveAction on InstanceState::Destroyed. |
| async fn test_collection(durability: fdecl::Durability) { |
| let (_test, component_container, component_a, component_b) = |
| start_collection(durability).await; |
| |
| // Stop the collection. |
| ActionSet::register( |
| component_container.clone(), |
| ShutdownAction::new(ShutdownType::Instance), |
| ) |
| .await |
| .expect("shutdown failed"); |
| assert!(is_destroyed(&component_a).await); |
| assert!(is_destroyed(&component_b).await); |
| ActionSet::register(component_container.clone(), UnresolveAction::new()) |
| .await |
| .expect("unresolve failed"); |
| assert!(is_discovered(&component_container).await); |
| |
| // Trying to unresolve a child fails because the children of a collection are destroyed when |
| // the collection is stopped. Then it's an error to unresolve a Destroyed component. |
| assert_matches!( |
| ActionSet::register(component_a.clone(), UnresolveAction::new()).await, |
| Err(ActionError::UnresolveError { |
| err: UnresolveActionError::InstanceDestroyed { .. } |
| }) |
| ); |
| // Still Destroyed. |
| assert!(is_destroyed(&component_a).await); |
| assert!(is_destroyed(&component_b).await); |
| } |
| |
| /// Test a collection whose children have transient durability. |
| #[fuchsia::test] |
| async fn unresolve_action_on_transient_collection() { |
| test_collection(fdecl::Durability::Transient).await; |
| } |
| |
| /// Test a collection whose children have single-run durability. |
| #[fuchsia::test] |
| async fn unresolve_action_on_single_run_collection() { |
| test_collection(fdecl::Durability::SingleRun).await; |
| } |
| } |