blob: 29257c5cfc2345f06fc05cfb7bf8762b6d5e498c [file] [log] [blame]
// Copyright 2019 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::cobalt,
fidl::endpoints::Proxy,
fidl_fuchsia_component as fcomponent, fuchsia_async as fasync, fuchsia_zircon as zx,
realm_management,
thiserror::{self, Error},
tracing::info,
};
/// Errors returned by calls startup functions.
#[derive(Debug, Error, Clone, PartialEq)]
pub enum StartupError {
#[error("Existing session not destroyed at \"{}/{}\": {:?}", collection, name, err)]
NotDestroyed { name: String, collection: String, err: fcomponent::Error },
#[error("Session {} not created at \"{}/{}\": {:?}", url, collection, name, err)]
NotCreated { name: String, collection: String, url: String, err: fcomponent::Error },
#[error(
"Exposed directory of session {} at \"{}/{}\" not opened: {:?}",
url,
collection,
name,
err
)]
ExposedDirNotOpened { name: String, collection: String, url: String, err: fcomponent::Error },
#[error("Session {} not launched at \"{}/{}\": {:?}", url, collection, name, err)]
NotLaunched { name: String, collection: String, url: String, err: fcomponent::Error },
}
/// The name of the session child component.
const SESSION_NAME: &str = "session";
/// The name of the child collection the session is added to, must match the declaration in
/// session_manager.cml.
const SESSION_CHILD_COLLECTION: &str = "session";
/// Launches the specified session.
///
/// Any existing session child will be destroyed prior to launching the new session.
///
/// Returns a channel to the session component's `exposed_dir` directory, or an error.
///
/// # Parameters
/// - `session_url`: The URL of the session to launch.
/// - `realm`: The realm in which to launch the session.
///
/// # Errors
/// If there was a problem creating or binding to the session component instance.
pub async fn launch_session(
session_url: &str,
realm: &fcomponent::RealmProxy,
) -> Result<zx::Channel, StartupError> {
info!(session_url, "Launching session");
let start_time = zx::Time::get_monotonic();
let exposed_dir = set_session(&session_url, realm).await?;
let end_time = zx::Time::get_monotonic();
fasync::Task::local(async move {
if let Ok(cobalt_logger) = cobalt::get_logger() {
// The result is disregarded as there is not retry-logic if it fails, and the error is
// not meant to be fatal.
let _ = cobalt::log_session_launch_time(cobalt_logger, start_time, end_time).await;
}
})
.detach();
Ok(exposed_dir)
}
/// Sets the currently active session.
///
/// If an existing session is running, the session's component instance will be destroyed prior to
/// creating the new session, effectively replacing the session.
///
/// The session is launched by connecting to the fuchsia.component.Binder protocol
/// in its exposed directory. This capability bind will trigger the component
/// to start.
///
/// Returns a channel to the session component's `exposed_dir` directory, or an error.
///
/// # Parameters
/// - `session_url`: The URL of the session to instantiate.
/// - `realm`: The realm in which to create the session.
///
/// # Errors
/// Returns an error if any of the realm operations fail, or the realm is unavailable.
async fn set_session(
session_url: &str,
realm: &fcomponent::RealmProxy,
) -> Result<zx::Channel, StartupError> {
realm_management::destroy_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm)
.await
.or_else(|err: fcomponent::Error| match err {
// Since the intent is simply to clear out the existing session child if it exists,
// related errors are disregarded.
fcomponent::Error::InvalidArguments
| fcomponent::Error::InstanceNotFound
| fcomponent::Error::CollectionNotFound => Ok(()),
_ => Err(err),
})
.map_err(|err| StartupError::NotDestroyed {
name: SESSION_NAME.to_string(),
collection: SESSION_CHILD_COLLECTION.to_string(),
err,
})?;
realm_management::create_child_component(
SESSION_NAME,
&session_url,
SESSION_CHILD_COLLECTION,
realm,
)
.await
.map_err(|err| StartupError::NotCreated {
name: SESSION_NAME.to_string(),
collection: SESSION_CHILD_COLLECTION.to_string(),
url: session_url.to_string(),
err,
})?;
let exposed_dir = realm_management::open_child_component_exposed_dir(
SESSION_NAME,
SESSION_CHILD_COLLECTION,
realm,
)
.await
.map_err(|err| StartupError::ExposedDirNotOpened {
name: SESSION_NAME.to_string(),
collection: SESSION_CHILD_COLLECTION.to_string(),
url: session_url.to_string(),
err,
})?;
// By connecting to the fuchsia.component.Binder protocol, we instruct
// Component Manager to *start* the session component.
let _ = fuchsia_component::client::connect_to_protocol_at_dir_root::<fcomponent::BinderMarker>(
&exposed_dir,
)
.map_err(|_err| StartupError::NotLaunched {
name: SESSION_NAME.to_string(),
collection: SESSION_CHILD_COLLECTION.to_string(),
url: session_url.to_string(),
err: fcomponent::Error::InstanceCannotStart,
})?;
Ok(exposed_dir.into_channel().unwrap().into_zx_channel())
}
#[cfg(test)]
mod tests {
use {
super::{set_session, zx, StartupError, SESSION_CHILD_COLLECTION, SESSION_NAME},
fidl::endpoints::spawn_stream_handler,
fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio,
lazy_static::lazy_static,
session_testing::spawn_directory_server,
std::sync::mpsc,
test_util::Counter,
};
#[fuchsia::test]
async fn set_session_calls_realm_methods_in_appropriate_order() {
lazy_static! {
// The number of realm calls which have been made so far.
static ref NUM_REALM_REQUESTS: Counter = Counter::new(0);
}
let session_url = "session";
let directory_request_handler = move |directory_request| match directory_request {
fio::DirectoryRequest::Open { path: _, .. } => {
assert_eq!(NUM_REALM_REQUESTS.get(), 4);
}
_ => panic!("Directory handler received an unexpected request"),
};
let realm = spawn_stream_handler(move |realm_request| async move {
match realm_request {
fcomponent::RealmRequest::DestroyChild { child, responder } => {
assert_eq!(NUM_REALM_REQUESTS.get(), 0);
assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
assert_eq!(child.name, SESSION_NAME);
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::CreateChild { collection, decl, args: _, responder } => {
assert_eq!(NUM_REALM_REQUESTS.get(), 1);
assert_eq!(decl.url.unwrap(), session_url);
assert_eq!(decl.name.unwrap(), SESSION_NAME);
assert_eq!(&collection.name, SESSION_CHILD_COLLECTION);
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::OpenExposedDir { child, exposed_dir, responder } => {
assert_eq!(NUM_REALM_REQUESTS.get(), 2);
assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
assert_eq!(child.name, SESSION_NAME);
spawn_directory_server(exposed_dir, directory_request_handler);
let _ = responder.send(&mut Ok(()));
}
_ => panic!("Realm handler received an unexpected request"),
};
NUM_REALM_REQUESTS.inc();
})
.unwrap();
assert!(set_session(session_url, &realm).await.is_ok());
}
#[fuchsia::test]
async fn set_session_returns_channel_bound_to_exposed_dir() {
let session_url = "session";
let (exposed_dir_server_end_sender, exposed_dir_server_end_receiver) = mpsc::channel();
let realm = spawn_stream_handler(move |realm_request| {
let exposed_dir_server_end_sender = exposed_dir_server_end_sender.clone();
async move {
match realm_request {
fcomponent::RealmRequest::DestroyChild { responder, .. } => {
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::CreateChild { responder, .. } => {
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => {
exposed_dir_server_end_sender
.send(exposed_dir)
.expect("Failed to relay `exposed_dir`");
let _ = responder.send(&mut Ok(()));
}
_ => panic!("Realm handler received an unexpected request"),
}
}
})
.unwrap();
let exposed_dir_client_end = match set_session(session_url, &realm).await {
Ok(exposed_dir_client_end) => exposed_dir_client_end,
Err(e) => panic!("Failed to set_session() {:?}", e),
};
let exposed_dir_server_end =
exposed_dir_server_end_receiver.recv().expect("Failed to read exposed_dir from relay");
exposed_dir_server_end
.into_channel()
.write(b"hello world", /* handles */ &mut vec![])
.expect("Failed to write to server end");
let mut read_buf = zx::MessageBuf::new();
exposed_dir_client_end.read(&mut read_buf).expect("Failed to read from client end");
assert_eq!(read_buf.bytes(), b"hello world", "server and client channels do not match");
}
#[fuchsia::test]
async fn set_session_returns_error_if_binder_connection_fails() {
let session_url = "session";
let realm = spawn_stream_handler(move |realm_request| async move {
match realm_request {
fcomponent::RealmRequest::DestroyChild { responder, .. } => {
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::CreateChild { responder, .. } => {
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::OpenExposedDir { responder, .. } => {
let _ = responder.send(&mut Ok(()));
}
_ => panic!("Realm handler received an unexpected request"),
};
})
.unwrap();
// By not implementing a handler for exposed_dir channel, the
// set_session function will observe a PEER_CLOSED signal.
assert_matches::assert_matches!(
set_session(session_url, &realm).await,
Err(StartupError::NotLaunched { .. })
);
}
}