blob: d2c8ef55b92a215804076d92ae2af8c00c187d3d [file] [log] [blame]
// Copyright 2020 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::bedrock::program::{Program, StartInfo},
crate::framework::controller,
crate::model::{
actions::{Action, ActionKey},
component::instance::{InstanceState, StartedInstanceState},
component::{ComponentInstance, IncomingCapabilities, StartReason},
error::{ActionError, CreateNamespaceError, StartActionError, StructuredConfigError},
hooks::{Event, EventPayload, RuntimeInfo},
namespace::create_namespace,
routing::{open_capability, RouteRequest},
},
crate::runner::RemoteRunner,
::namespace::Entry as NamespaceEntry,
::routing::component_instance::ComponentInstanceInterface,
async_trait::async_trait,
cm_logger::scoped::ScopedLogger,
cm_rust::ComponentDecl,
cm_util::{AbortError, AbortFutureExt, AbortHandle, AbortableScope},
config_encoder::ConfigFields,
fidl::{endpoints::DiscoverableProtocolMarker, Vmo},
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_data as fdata,
fidl_fuchsia_logger as flogger, fidl_fuchsia_mem as fmem, fidl_fuchsia_process as fprocess,
fuchsia_zircon as zx,
futures::channel::oneshot,
moniker::Moniker,
sandbox::{Capability, Dict},
serve_processargs::NamespaceBuilder,
std::sync::Arc,
tracing::warn,
vfs::execution_scope::ExecutionScope,
};
/// Starts a component instance.
pub struct StartAction {
start_reason: StartReason,
execution_controller_task: Option<controller::ExecutionControllerTask>,
incoming: IncomingCapabilities,
abort_handle: AbortHandle,
namespace_scope: ExecutionScope,
abortable_scope: AbortableScope,
}
impl StartAction {
pub fn new(
start_reason: StartReason,
execution_controller_task: Option<controller::ExecutionControllerTask>,
incoming: IncomingCapabilities,
) -> Self {
Self::new_inner(start_reason, execution_controller_task, incoming, ExecutionScope::new())
}
fn new_inner(
start_reason: StartReason,
execution_controller_task: Option<controller::ExecutionControllerTask>,
incoming: IncomingCapabilities,
namespace_scope: ExecutionScope,
) -> Self {
let (abortable_scope, abort_handle) = AbortableScope::new();
Self {
start_reason,
execution_controller_task,
incoming,
namespace_scope,
abort_handle,
abortable_scope,
}
}
#[cfg(test)]
pub fn new_with_scope(
start_reason: StartReason,
execution_controller_task: Option<controller::ExecutionControllerTask>,
incoming: IncomingCapabilities,
namespace_scope: ExecutionScope,
) -> Self {
Self::new_inner(start_reason, execution_controller_task, incoming, namespace_scope)
}
}
#[async_trait]
impl Action for StartAction {
async fn handle(self, component: Arc<ComponentInstance>) -> Result<(), ActionError> {
do_start(
&component,
&self.start_reason,
self.execution_controller_task,
self.incoming,
self.namespace_scope,
self.abortable_scope,
)
.await
.map_err(Into::into)
}
fn key(&self) -> ActionKey {
ActionKey::Start
}
fn abort_handle(&self) -> Option<AbortHandle> {
Some(self.abort_handle.clone())
}
}
struct StartContext {
runner: Option<RemoteRunner>,
url: String,
namespace_builder: NamespaceBuilder,
namespace_scope: ExecutionScope,
numbered_handles: Vec<fprocess::HandleInfo>,
encoded_config: Option<fmem::Data>,
program_input_dict_additions: Option<Dict>,
start_reason: StartReason,
execution_controller_task: Option<controller::ExecutionControllerTask>,
logger: Option<ScopedLogger>,
}
async fn do_start(
component: &Arc<ComponentInstance>,
start_reason: &StartReason,
execution_controller_task: Option<controller::ExecutionControllerTask>,
incoming: IncomingCapabilities,
namespace_scope: ExecutionScope,
abortable_scope: AbortableScope,
) -> Result<(), StartActionError> {
// Translates the error when a long running future is aborted.
let abort_error =
|_: AbortError| StartActionError::Aborted { moniker: component.moniker.clone() };
// Resolve the component and find the runner to use.
let (runner, resolved_component, program_input_dict) = {
// Obtain the runner declaration under a short lock, as `open_runner` may lock the
// resolved state re-entrantly.
let resolved_state = component
.lock_resolved_state()
.with(&abortable_scope)
.await
.map_err(abort_error)?
.map_err(|err| StartActionError::ResolveActionError {
moniker: component.moniker.clone(),
err: Box::new(err),
})?;
(
resolved_state.decl().get_runner(),
resolved_state.resolved_component.clone(),
resolved_state.program_input_dict.clone(),
)
};
let runner = match runner {
Some(runner) => open_runner(component, runner)
.with(&abortable_scope)
.await
.map_err(abort_error)?
.map_err(|err| {
warn!(moniker = %component.moniker, %err, "Failed to resolve runner.");
err
})?,
None => None,
};
let IncomingCapabilities {
numbered_handles,
additional_namespace_entries,
dict: program_input_dict_additions,
} = incoming;
// Create the component's namespace.
let mut namespace_builder = create_namespace(
resolved_component.package.as_ref(),
component,
&resolved_component.decl,
&program_input_dict,
namespace_scope.clone(),
)
.await
.map_err(|err| StartActionError::CreateNamespaceError {
moniker: component.moniker.clone(),
err,
})?;
for NamespaceEntry { directory, path } in additional_namespace_entries {
let directory: sandbox::Directory = directory.into();
namespace_builder.add_entry(Capability::Directory(directory), &path).map_err(|err| {
StartActionError::CreateNamespaceError {
moniker: component.moniker.clone(),
err: CreateNamespaceError::BuildNamespaceError(err),
}
})?;
}
// Check policy.
// TODO(https://fxbug.dev/42071809): Consider moving this check to ComponentInstance::add_child
match component.on_terminate {
fdecl::OnTerminate::Reboot => {
component.policy_checker().reboot_on_terminate_allowed(&component.moniker).map_err(
|err| StartActionError::RebootOnTerminateForbidden {
moniker: component.moniker.clone(),
err,
},
)?;
}
fdecl::OnTerminate::None => {}
}
// Create a component-scoped logger if the component uses LogSink.
let decl = &resolved_component.decl;
let logger = if let Some(logsink_decl) = get_logsink_decl(&decl) {
match create_scoped_logger(component, logsink_decl.clone()).await {
Ok(logger) => Some(logger),
Err(err) => {
warn!(moniker = %component.moniker, %err, "Could not create logger for component. Logs will be attributed to component_manager");
None
}
}
} else {
None
};
let encoded_config = match decl.config {
None => None,
Some(ref config_decl) => match config_decl.value_source {
cm_rust::ConfigValueSource::PackagePath(_) => {
let Some(mut config) = resolved_component.config.clone() else {
return Err(StartActionError::StructuredConfigError {
moniker: component.moniker.clone(),
err: StructuredConfigError::ConfigValuesMissing,
});
};
if has_config_capabilities(decl) {
update_config_with_capabilities(&mut config, decl, &component)
.with(&abortable_scope)
.await
.map_err(abort_error)??;
update_component_config(&component, config.clone()).await?;
}
Some(encode_config(config, &component.moniker).await?)
}
cm_rust::ConfigValueSource::Capabilities(_) => {
let config = create_config_with_capabilities(decl, &component)
.with(&abortable_scope)
.await
.map_err(abort_error)??;
match config {
Some(c) => {
update_component_config(&component, c.clone()).await?;
Some(encode_config(c, &component.moniker).await?)
}
None => None,
}
}
},
};
let start_context = StartContext {
runner,
url: resolved_component.resolved_url.clone(),
namespace_builder,
namespace_scope,
numbered_handles,
encoded_config,
program_input_dict_additions,
start_reason: start_reason.clone(),
execution_controller_task,
logger,
};
start_component(&component, resolved_component.decl, start_context).await
}
/// Set the Runtime in the Execution and start the exit watcher. From component manager's
/// perspective, this indicates that the component has started. If this returns an error, the
/// component was shut down and the Runtime is not set, otherwise the function returns the
/// start context with the runtime set. This function acquires the state and execution locks on
/// `Component`.
async fn start_component(
component: &Arc<ComponentInstance>,
decl: ComponentDecl,
start_context: StartContext,
) -> Result<(), StartActionError> {
let runtime_info;
let timestamp;
let runtime_dir;
let break_on_start_left;
let break_on_start_right;
{
let mut state = component.lock_state().await;
if let Some(r) = should_return_early(&state, &component.moniker) {
return r;
}
let (diagnostics_sender, diagnostics_receiver) = oneshot::channel();
(break_on_start_left, break_on_start_right) = zx::EventPair::create();
let StartContext {
runner,
url,
namespace_builder,
numbered_handles,
namespace_scope,
encoded_config,
program_input_dict_additions,
start_reason,
execution_controller_task,
logger,
} = start_context;
let program = if let Some(runner) = runner {
let moniker = &component.moniker;
let component_instance = state
.instance_token(moniker, &component.context)
.ok_or(StartActionError::InstanceDestroyed { moniker: moniker.clone() })?;
let escrowed_state = state
.reap_escrowed_state_during_start()
.await
.ok_or(StartActionError::InstanceDestroyed { moniker: moniker.clone() })?;
let start_info = StartInfo {
resolved_url: url,
program: decl
.program
.as_ref()
.map(|p| p.info.clone())
.unwrap_or_else(|| fdata::Dictionary::default()),
namespace: namespace_builder,
numbered_handles,
encoded_config,
break_on_start: Some(break_on_start_left),
component_instance,
};
Some(
Program::start(
&runner,
start_info,
escrowed_state,
diagnostics_sender,
namespace_scope,
)
.map_err(|err| StartActionError::StartProgramError {
moniker: moniker.clone(),
err,
})?,
)
} else {
None
};
let started = StartedInstanceState::new(
program,
component.as_weak(),
start_reason,
execution_controller_task,
logger,
);
timestamp = started.timestamp;
runtime_info = RuntimeInfo::new(timestamp, diagnostics_receiver);
runtime_dir = started.runtime_dir().cloned();
state.replace(|instance_state| match instance_state {
InstanceState::Resolved(resolved) => InstanceState::Started(resolved, started),
other_state => panic!("starting an unresolved component: {:?}", other_state),
});
// TODO(b/322564390): Move program_input_dict_additions into `StartedInstanceState`.
state
.get_resolved_state_mut()
.expect("expected component to be resolved")
.program_input_dict_additions = program_input_dict_additions;
}
// Dispatch Started and DebugStarted events outside of the state lock, but under the
// actions lock, so that:
//
// - Hooks implementations can use the state (such as re-entrantly ensuring the component is
// started).
// - The Started events will be ordered before any other component lifecycle transitions.
//
component
.hooks
.dispatch(&Event::new_with_timestamp(
component,
EventPayload::Started { runtime: runtime_info, component_decl: decl },
timestamp,
))
.await;
component
.hooks
.dispatch(&Event::new_with_timestamp(
component,
EventPayload::DebugStarted {
runtime_dir,
break_on_start: Arc::new(break_on_start_right),
},
timestamp,
))
.await;
Ok(())
}
/// Determines if `start` should return early. Returns the following values:
/// - None: `start` should not return early because the component is not currently running
/// - Some(Ok(())): `start` should return early because the component has already been started
/// - Some(Err(err)): `start` should return early because it will be unable to start the component
/// due to `err` (for example, perhaps the component is destroyed or shut down).
pub fn should_return_early(
component: &InstanceState,
moniker: &Moniker,
) -> Option<Result<(), StartActionError>> {
match component {
InstanceState::Started(_, _) => Some(Ok(())),
InstanceState::New | InstanceState::Unresolved(_) | InstanceState::Resolved(_) => None,
InstanceState::Shutdown(_, _) => {
Some(Err(StartActionError::InstanceShutDown { moniker: moniker.clone() }))
}
InstanceState::Destroyed => {
Some(Err(StartActionError::InstanceDestroyed { moniker: moniker.clone() }))
}
}
}
/// Returns a RemoteRunner routed to the component's runner, if it specifies one.
///
/// Returns None if the component's decl does not specify a runner.
async fn open_runner(
component: &Arc<ComponentInstance>,
runner: cm_rust::UseRunnerDecl,
) -> Result<Option<RemoteRunner>, StartActionError> {
// Open up a channel to the runner.
let proxy = open_capability(&RouteRequest::UseRunner(runner.clone()), component)
.await
.map_err(|err| StartActionError::ResolveRunnerError {
moniker: component.moniker.clone(),
err: Box::new(err),
runner: runner.source_name,
})?;
Ok(Some(RemoteRunner::new(proxy)))
}
/// Returns true if the decl uses configuration capabilities.
fn has_config_capabilities(decl: &cm_rust::ComponentDecl) -> bool {
decl.uses.iter().find(|u| matches!(u, cm_rust::UseDecl::Config(_))).is_some()
}
/// Update the component's configuration fields.
async fn update_component_config(
component: &Arc<ComponentInstance>,
config: ConfigFields,
) -> Result<(), StartActionError> {
let mut resolved_state = component.lock_resolved_state().await.unwrap();
resolved_state.resolved_component.config = Some(config);
Ok(())
}
async fn create_config_with_capabilities(
decl: &cm_rust::ComponentDecl,
component: &Arc<ComponentInstance>,
) -> Result<Option<ConfigFields>, StartActionError> {
let Some(ref config_decl) = decl.config else {
return Ok(None);
};
let mut fields = ConfigFields { fields: Vec::new(), checksum: config_decl.checksum.clone() };
for field in &config_decl.fields {
let Some(use_config) = routing::config::get_use_config_from_key(&field.key, decl) else {
return Err(StartActionError::StructuredConfigError {
moniker: component.moniker.clone(),
err: StructuredConfigError::KeyNotFound { key: field.key.clone() },
});
};
let value = match routing::config::route_config_value(use_config, component).await {
Ok(Some(v)) => v,
Ok(None) => {
return Err(StartActionError::StructuredConfigError {
moniker: component.moniker.clone(),
err: StructuredConfigError::KeyNotFound { key: field.key.clone() },
})
}
Err(err) => {
return Err(StartActionError::StructuredConfigError {
moniker: component.moniker.clone(),
err: err.into(),
})
}
};
fields.fields.push(config_encoder::ConfigField {
key: field.key.clone(),
value: value,
mutability: Default::default(),
});
}
Ok(Some(fields))
}
/// Update config fields with the values received through configuration
/// capabilities. This will perform routing on each of the configuration `use`
/// decls to get the values. Updating the fields is fine because configuration
/// capabilities take precedence over both the CVF value and "mutability: parent".
async fn update_config_with_capabilities(
config: &mut ConfigFields,
decl: &cm_rust::ComponentDecl,
component: &Arc<ComponentInstance>,
) -> Result<(), StartActionError> {
for field in config.fields.iter_mut() {
let Some(use_config) = routing::config::get_use_config_from_key(&field.key, decl) else {
continue;
};
let value =
routing::config::route_config_value(use_config, component).await.map_err(|e| {
StartActionError::StructuredConfigError {
moniker: component.moniker.clone(),
err: e.into(),
}
})?;
let Some(value) = value else {
continue;
};
if !field.value.matches_type(&value) {
return Err(StartActionError::StructuredConfigError {
moniker: component.moniker.clone(),
err: StructuredConfigError::ValueMismatch { key: field.key.clone() },
});
}
field.value = value;
}
Ok(())
}
/// Encode the configuration into a VMO.
async fn encode_config(
config: ConfigFields,
moniker: &Moniker,
) -> Result<fmem::Data, StartActionError> {
let (vmo, size) = (|| {
let encoded = config.encode_as_fidl_struct();
let size = encoded.len() as u64;
let vmo = Vmo::create(size)?;
vmo.write(&encoded, 0)?;
Ok((vmo, size))
})()
.map_err(|s| StartActionError::StructuredConfigError {
err: StructuredConfigError::VmoCreateFailed(s),
moniker: moniker.clone(),
})?;
Ok(fmem::Data::Buffer(fmem::Buffer { vmo, size }))
}
/// Returns the UseProtocolDecl for the LogSink protocol, if any.
fn get_logsink_decl<'a>(decl: &'a cm_rust::ComponentDecl) -> Option<&'a cm_rust::UseProtocolDecl> {
decl.uses.iter().find_map(|use_| match use_ {
cm_rust::UseDecl::Protocol(decl) => {
(decl.source_name == flogger::LogSinkMarker::PROTOCOL_NAME).then_some(decl)
}
_ => None,
})
}
/// Returns a ScopedLogger attributed to the component, given its use declaration for the
/// `fuchsia.logger.LogSink` protocol.
async fn create_scoped_logger(
component: &Arc<ComponentInstance>,
logsink_decl: cm_rust::UseProtocolDecl,
) -> Result<ScopedLogger, anyhow::Error> {
let logsink = open_capability(&RouteRequest::UseProtocol(logsink_decl), component).await?;
Ok(ScopedLogger::create(logsink)?)
}
#[cfg(test)]
mod tests {
use super::*;
use {
crate::model::{
actions::{ActionSet, ShutdownAction, ShutdownType, StopAction},
component::instance::{ResolvedInstanceState, UnresolvedInstanceState},
component::{Component, WeakComponentInstance},
error::ModelError,
hooks::{EventType, Hook, HooksRegistration},
structured_dict::ComponentInput,
testing::{
routing_test_helpers::RoutingTestBuilder,
test_helpers::{self, ActionsTest},
test_hook::Lifecycle,
},
},
assert_matches::assert_matches,
async_trait::async_trait,
cm_rust_testing::{ChildBuilder, ComponentDeclBuilder},
fuchsia_async as fasync, fuchsia_zircon as zx,
futures::{channel::mpsc, stream::FuturesUnordered, FutureExt, StreamExt},
rand::seq::SliceRandom,
routing::resolving::ComponentAddress,
std::sync::{Mutex, Weak},
};
// Child name for test child components instantiated during tests.
const TEST_CHILD_NAME: &str = "child";
struct ShutdownOnStartHook {
component: Arc<ComponentInstance>,
done: Mutex<mpsc::Sender<()>>,
}
#[async_trait]
impl Hook for ShutdownOnStartHook {
async fn on(self: Arc<Self>, _event: &Event) -> Result<(), ModelError> {
fasync::Task::spawn(async move {
ActionSet::register(
self.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.await
.expect("shutdown failed");
self.done.lock().unwrap().try_send(()).unwrap();
})
.detach();
Ok(())
}
}
#[fuchsia::test]
/// Validate that if a start action is issued and the component shuts down
/// the action completes we see a Stop event emitted.
async fn start_issues_shutdown() {
let (test_topology, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
let (done, mut hook_done_receiver) = mpsc::channel(1);
let start_hook =
Arc::new(ShutdownOnStartHook { component: child.clone(), done: Mutex::new(done) });
child
.hooks
.install(vec![HooksRegistration::new(
"my_start_hook",
vec![EventType::Started],
Arc::downgrade(&start_hook) as Weak<dyn Hook>,
)])
.await;
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.unwrap();
// Wait until the action in the hook is done.
hook_done_receiver.next().await.unwrap();
let events: Vec<_> = test_topology
.test_hook
.lifecycle()
.into_iter()
.filter(|event| match event {
Lifecycle::Start(_) | Lifecycle::Stop(_) => true,
_ => false,
})
.collect();
assert_eq!(
events,
vec![
Lifecycle::Start(vec![format!("{}", TEST_CHILD_NAME).as_str()].try_into().unwrap()),
Lifecycle::Stop(vec![format!("{}", TEST_CHILD_NAME).as_str()].try_into().unwrap())
]
);
}
struct StopOnStartHook {
component: Arc<ComponentInstance>,
done: Mutex<mpsc::Sender<()>>,
}
#[async_trait]
impl Hook for StopOnStartHook {
async fn on(self: Arc<Self>, _event: &Event) -> Result<(), ModelError> {
fasync::Task::spawn(async move {
ActionSet::register(self.component.clone(), StopAction::new(false))
.await
.expect("stop failed");
self.done.lock().unwrap().try_send(()).unwrap();
})
.detach();
Ok(())
}
}
#[fuchsia::test]
/// Validate that if a start action is issued and the component stops
/// the action completes we see a Stop event emitted.
async fn start_issues_stop() {
let (test_topology, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
let (done, mut hook_done_receiver) = mpsc::channel(1);
let start_hook =
Arc::new(StopOnStartHook { component: child.clone(), done: Mutex::new(done) });
child
.hooks
.install(vec![HooksRegistration::new(
"my_start_hook",
vec![EventType::Started],
Arc::downgrade(&start_hook) as Weak<dyn Hook>,
)])
.await;
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.unwrap();
// Wait until the action in the hook is done.
hook_done_receiver.next().await.unwrap();
let events: Vec<_> = test_topology
.test_hook
.lifecycle()
.into_iter()
.filter(|event| match event {
Lifecycle::Start(_) | Lifecycle::Stop(_) => true,
_ => false,
})
.collect();
assert_eq!(
events,
vec![
Lifecycle::Start(vec![format!("{}", TEST_CHILD_NAME).as_str()].try_into().unwrap()),
Lifecycle::Stop(vec![format!("{}", TEST_CHILD_NAME).as_str()].try_into().unwrap())
]
);
}
#[fuchsia::test]
/// If start and stop happens concurrently, the component should either end up as
/// started or stopped, without deadlocking.
async fn concurrent_start_stop() {
let (test_topology, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
// Run start and stop in random order.
let start_fut = ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
);
let stop_fut = ActionSet::register(child.clone(), StopAction::new(false));
let mut futs = vec![start_fut.boxed(), stop_fut.boxed()];
futs.shuffle(&mut rand::thread_rng());
let stream: FuturesUnordered<_> = futs.into_iter().collect();
let _: Vec<_> = stream.collect().await;
let events: Vec<_> = test_topology
.test_hook
.lifecycle()
.into_iter()
.filter(|event| match event {
Lifecycle::Start(_) | Lifecycle::Stop(_) => true,
_ => false,
})
.collect();
let start_event =
Lifecycle::Start(vec![format!("{}", TEST_CHILD_NAME).as_str()].try_into().unwrap());
let stop_event =
Lifecycle::Stop(vec![format!("{}", TEST_CHILD_NAME).as_str()].try_into().unwrap());
assert!(events.contains(&start_event) || events.contains(&stop_event));
}
/// If start is blocked during resolving then stop can interrupt it.
#[fuchsia::test]
async fn start_aborted_by_stop() {
let root_name = "root";
let components = vec![
(root_name, ComponentDeclBuilder::new().child_default(TEST_CHILD_NAME).build()),
(TEST_CHILD_NAME, test_helpers::component_decl_with_test_runner()),
];
let builder = RoutingTestBuilder::new(components[0].0, components);
// Add a blocker to the resolver that will cause resolving the child to block.
let (resolved_tx, resolved_rx) = oneshot::channel::<()>();
let (continue_tx, continue_rx) = oneshot::channel::<()>();
let test_topology =
builder.add_blocker(TEST_CHILD_NAME, resolved_tx, continue_rx).build().await;
let _root =
test_topology.model.root().find_and_maybe_resolve(&Moniker::default()).await.unwrap();
let child =
test_topology.model.root().find(&TEST_CHILD_NAME.try_into().unwrap()).await.unwrap();
let start_fut = child
.lock_actions()
.await
.register_no_wait(
&child,
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await;
// Wait until start is blocked.
resolved_rx.await.unwrap();
// Stop should cancel start.
let stop_fut =
child.lock_actions().await.register_no_wait(&child, StopAction::new(false)).await;
assert_matches!(
start_fut.await.unwrap_err(),
ActionError::StartError { err: StartActionError::Aborted { .. } }
);
continue_tx.send(()).unwrap();
stop_fut.await.unwrap();
}
#[fuchsia::test]
async fn restart_set_execution_runtime() {
let (_test_harness, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
{
let timestamp = zx::Time::get_monotonic();
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.expect("failed to start child");
let state = child.lock_state().await;
let runtime = state.get_started_state().expect("child runtime is unexpectedly empty");
assert!(runtime.timestamp > timestamp);
}
{
ActionSet::register(child.clone(), StopAction::new(false))
.await
.expect("failed to stop child");
let state = child.lock_state().await;
assert!(state.get_started_state().is_none());
}
{
let timestamp = zx::Time::get_monotonic();
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.expect("failed to start child");
let state = child.lock_state().await;
let runtime = state.get_started_state().expect("child runtime is unexpectedly empty");
assert!(runtime.timestamp > timestamp);
}
}
#[fuchsia::test]
async fn restart_does_not_refresh_resolved_state() {
let (mut test_harness, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
{
let timestamp = zx::Time::get_monotonic();
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.expect("failed to start child");
let state = child.lock_state().await;
let runtime = state.get_started_state().expect("child runtime is unexpectedly empty");
assert!(runtime.timestamp > timestamp);
}
{
let () = ActionSet::register(child.clone(), StopAction::new(false))
.await
.expect("failed to stop child");
let state = child.lock_state().await;
assert!(state.get_started_state().is_none());
}
let resolver = test_harness.resolver.as_mut();
let original_decl =
resolver.get_component_decl(TEST_CHILD_NAME).expect("child decl not stored");
let mut modified_decl = original_decl.clone();
modified_decl.children.push(ChildBuilder::new().name("foo").build());
resolver.add_component(TEST_CHILD_NAME, modified_decl.clone());
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.expect("failed to start child");
let resolved_decl = get_resolved_decl(&child).await;
assert_ne!(resolved_decl, modified_decl);
assert_eq!(resolved_decl, original_decl);
}
async fn build_tree_with_single_child(
child_name: &'static str,
) -> (ActionsTest, Arc<ComponentInstance>) {
let root_name = "root";
let components = vec![
(root_name, ComponentDeclBuilder::new().child_default(child_name).build()),
(child_name, test_helpers::component_decl_with_test_runner()),
];
let test_topology = ActionsTest::new(components[0].0, components, None).await;
let child = test_topology.look_up(vec![child_name].try_into().unwrap()).await;
(test_topology, child)
}
async fn get_resolved_decl(component: &Arc<ComponentInstance>) -> ComponentDecl {
let state = component.lock_state().await;
state.get_resolved_state().expect("expected component to be resolved").decl().clone()
}
#[fuchsia::test]
async fn check_should_return_early() {
let m = Moniker::try_from(vec!["foo"]).unwrap();
// Checks based on InstanceState:
assert!(should_return_early(&InstanceState::New, &m).is_none());
assert!(should_return_early(
&InstanceState::Unresolved(UnresolvedInstanceState::new(ComponentInput::default())),
&m
)
.is_none());
assert_matches!(
should_return_early(&InstanceState::Destroyed, &m),
Some(Err(StartActionError::InstanceDestroyed { moniker: _ }))
);
let (_, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
let decl = ComponentDeclBuilder::new().child_default(TEST_CHILD_NAME).build();
let resolved_component = Component {
resolved_url: "".to_string(),
context_to_resolve_children: None,
decl,
package: None,
config: None,
abi_revision: None,
};
let ris = ResolvedInstanceState::new(
&child,
resolved_component.clone(),
ComponentAddress::from_absolute_url(&child.component_url).unwrap(),
Default::default(),
ComponentInput::default(),
)
.await
.unwrap();
assert!(should_return_early(&InstanceState::Resolved(ris), &m).is_none());
// Check for already_started:
{
let ris = ResolvedInstanceState::new(
&child,
resolved_component,
ComponentAddress::from_absolute_url(&child.component_url).unwrap(),
Default::default(),
ComponentInput::default(),
)
.await
.unwrap();
let started_state = StartedInstanceState::new(
None,
WeakComponentInstance::invalid(),
StartReason::Debug,
None,
None,
);
assert_matches!(
should_return_early(&InstanceState::Started(ris, started_state), &m),
Some(Ok(()))
);
}
// Check for shut_down:
let _ = child.stop_instance_internal(true).await;
assert!(child.lock_state().await.is_shut_down());
let state = child.lock_state().await;
assert_matches!(
should_return_early(&*state, &m),
Some(Err(StartActionError::InstanceShutDown { moniker: _ }))
);
}
#[fuchsia::test]
async fn check_already_started() {
let (_test_harness, child) = build_tree_with_single_child(TEST_CHILD_NAME).await;
ActionSet::register(
child.clone(),
StartAction::new(StartReason::Debug, None, IncomingCapabilities::default()),
)
.await
.expect("failed to start child");
let m = Moniker::try_from(vec!["TEST_CHILD_NAME"]).unwrap();
assert_matches!(should_return_early(&*child.lock_state().await, &m), Some(Ok(())));
}
}