blob: 84b5c749a495b79c161844dbf3667e31475c7d97 [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::startup,
anyhow::{Context as _, Error},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_element as felement,
fidl_fuchsia_session::{
LaunchConfiguration, LaunchError, LauncherRequest, LauncherRequestStream, RestartError,
RestarterRequest, RestarterRequestStream,
},
fidl_fuchsia_sessionmanager::StartupRequestStream,
fuchsia_component::server::ServiceFs,
fuchsia_zircon as zx,
futures::{lock::Mutex, StreamExt, TryFutureExt, TryStreamExt},
std::sync::Arc,
tracing::{error, info},
};
/// Maximum number of concurrent connections to the protocols served by SessionManager.
const MAX_CONCURRENT_CONNECTIONS: usize = 10_000;
/// A request to connect to a protocol exposed by SessionManager.
enum IncomingRequest {
Manager(felement::ManagerRequestStream),
GraphicalPresenter(felement::GraphicalPresenterRequestStream),
Launcher(LauncherRequestStream),
Restarter(RestarterRequestStream),
Startup(StartupRequestStream),
}
struct SessionManagerState {
/// The URL of the most recently launched session.
///
/// If set, the session is not guaranteed to be running.
session_url: Option<String>,
/// A client-end channel to the most recently launched session's `exposed_dir`.
///
/// If set, the session is not guaranteed to be running, and the channel is not
/// guaranteed to be connected.
session_exposed_dir_channel: Option<zx::Channel>,
/// The realm in which sessions will be launched.
realm: fcomponent::RealmProxy,
}
/// Manages the session lifecycle and provides services to control the session.
#[derive(Clone)]
pub struct SessionManager {
state: Arc<Mutex<SessionManagerState>>,
}
impl SessionManager {
/// Constructs a new SessionManager.
///
/// # Parameters
/// - `realm`: The realm in which sessions will be launched.
pub fn new(realm: fcomponent::RealmProxy) -> Self {
let state =
SessionManagerState { session_url: None, session_exposed_dir_channel: None, realm };
SessionManager { state: Arc::new(Mutex::new(state)) }
}
/// Launch the session with the component URL in `session_url`.
///
/// # Errors
///
/// Returns an error if the session could not be launched.
pub async fn launch_startup_session(&mut self, session_url: String) -> Result<(), Error> {
let mut state = self.state.lock().await;
state.session_exposed_dir_channel =
Some(startup::launch_session(&session_url, &state.realm).await?);
state.session_url = Some(session_url);
Ok(())
}
/// Starts serving [`IncomingRequest`] from `svc`.
///
/// This will return once the [`ServiceFs`] stops serving requests.
///
/// # Errors
/// Returns an error if there is an issue serving the `svc` directory handle.
pub async fn serve(&mut self) -> Result<(), Error> {
let mut fs = ServiceFs::new_local();
fs.dir("svc")
.add_fidl_service(IncomingRequest::Manager)
.add_fidl_service(IncomingRequest::GraphicalPresenter)
.add_fidl_service(IncomingRequest::Launcher)
.add_fidl_service(IncomingRequest::Restarter)
.add_fidl_service(IncomingRequest::Startup);
fs.take_and_serve_directory_handle()?;
fs.for_each_concurrent(MAX_CONCURRENT_CONNECTIONS, |request| {
let mut session_manager = self.clone();
async move {
session_manager
.handle_incoming_request(request)
.unwrap_or_else(|err| error!(?err))
.await
}
})
.await;
Ok(())
}
/// Handles an [`IncomingRequest`].
///
/// This will return once the protocol connection has been closed.
///
/// # Errors
/// Returns an error if there is an issue serving the request.
async fn handle_incoming_request(&mut self, request: IncomingRequest) -> Result<(), Error> {
match request {
IncomingRequest::Manager(request_stream) => {
// Connect to element.Manager served by the session.
let (manager_proxy, server_end) =
fidl::endpoints::create_proxy::<felement::ManagerMarker>()
.context("Failed to create ManagerProxy")?;
{
let state = self.state.lock().await;
let session_exposed_dir_channel =
state.session_exposed_dir_channel.as_ref().context(
"Failed to connect to ManagerProxy because no session was started",
)?;
fdio::service_connect_at(
session_exposed_dir_channel,
"fuchsia.element.Manager",
server_end.into_channel(),
)
.context("Failed to connect to Manager service")?;
}
SessionManager::handle_manager_request_stream(request_stream, manager_proxy)
.await
.context("Manager request stream got an error.")?;
}
IncomingRequest::GraphicalPresenter(request_stream) => {
// Connect to GraphicalPresenter served by the session.
let (graphical_presenter_proxy, server_end) =
fidl::endpoints::create_proxy::<felement::GraphicalPresenterMarker>()
.context("Failed to create GraphicalPresenterProxy")?;
{
let state = self.state.lock().await;
let session_exposed_dir_channel = state
.session_exposed_dir_channel
.as_ref()
.context(
"Failed to connect to GraphicalPresenterProxy because no session was started",
)?;
fdio::service_connect_at(
session_exposed_dir_channel,
"fuchsia.element.GraphicalPresenter",
server_end.into_channel(),
)
.context("Failed to connect to GraphicalPresenter service")?;
}
SessionManager::handle_graphical_presenter_request_stream(
request_stream,
graphical_presenter_proxy,
)
.await
.context("Graphical Presenter request stream got an error.")?;
}
IncomingRequest::Launcher(request_stream) => {
self.handle_launcher_request_stream(request_stream)
.await
.context("Session Launcher request stream got an error.")?;
}
IncomingRequest::Restarter(request_stream) => {
self.handle_restarter_request_stream(request_stream)
.await
.context("Session Restarter request stream got an error.")?;
}
IncomingRequest::Startup(request_stream) => {
self.handle_startup_request_stream(request_stream)
.await
.context("Sessionmanager Startup request stream got an error.")?;
}
}
Ok(())
}
/// Serves a specified [`ManagerRequestStream`].
///
/// # Parameters
/// - `request_stream`: the ManagerRequestStream.
/// - `manager_proxy`: the ManagerProxy that will handle the relayed commands.
///
/// # Errors
/// When an error is encountered reading from the request stream.
pub async fn handle_manager_request_stream(
mut request_stream: felement::ManagerRequestStream,
manager_proxy: felement::ManagerProxy,
) -> Result<(), Error> {
while let Some(request) =
request_stream.try_next().await.context("Error handling Manager request stream")?
{
match request {
felement::ManagerRequest::ProposeElement { spec, controller, responder } => {
let mut result = manager_proxy.propose_element(spec, controller).await?;
responder.send(&mut result)?;
}
};
}
Ok(())
}
/// Serves a specified [`GraphicalPresenterRequestStream`].
///
/// # Parameters
/// - `request_stream`: the GraphicalPresenterRequestStream.
/// - `graphical_presenter_proxy`: the GraphicalPresenterProxy that will handle the relayed commands.
///
/// # Errors
/// When an error is encountered reading from the request stream.
pub async fn handle_graphical_presenter_request_stream(
mut request_stream: felement::GraphicalPresenterRequestStream,
graphical_presenter_proxy: felement::GraphicalPresenterProxy,
) -> Result<(), Error> {
while let Some(request) = request_stream
.try_next()
.await
.context("Error handling Graphical Presenter request stream")?
{
match request {
felement::GraphicalPresenterRequest::PresentView {
view_spec,
annotation_controller,
view_controller_request,
responder,
} => {
let mut result = graphical_presenter_proxy
.present_view(view_spec, annotation_controller, view_controller_request)
.await?;
responder.send(&mut result)?;
}
};
}
Ok(())
}
/// Serves a specified [`LauncherRequestStream`].
///
/// # Parameters
/// - `request_stream`: the LauncherRequestStream.
///
/// # Errors
/// When an error is encountered reading from the request stream.
pub async fn handle_launcher_request_stream(
&mut self,
mut request_stream: LauncherRequestStream,
) -> Result<(), Error> {
while let Some(request) =
request_stream.try_next().await.context("Error handling Launcher request stream")?
{
match request {
LauncherRequest::Launch { configuration, responder } => {
let mut result = self.handle_launch_request(configuration).await;
let _ = responder.send(&mut result);
}
};
}
Ok(())
}
pub async fn handle_startup_request_stream(
&mut self,
mut request_stream: StartupRequestStream,
) -> Result<(), Error> {
while let Some(request) =
request_stream.try_next().await.context("Error handling Startup request stream")?
{
match request {
_ => {
// No-op
info!("Received startup request.");
}
};
}
Ok(())
}
/// Serves a specified [`RestarterRequestStream`].
///
/// # Parameters
/// - `request_stream`: the RestarterRequestStream.
///
/// # Errors
/// When an error is encountered reading from the request stream.
pub async fn handle_restarter_request_stream(
&mut self,
mut request_stream: RestarterRequestStream,
) -> Result<(), Error> {
while let Some(request) =
request_stream.try_next().await.context("Error handling Restarter request stream")?
{
match request {
RestarterRequest::Restart { responder } => {
let mut result = self.handle_restart_request().await;
let _ = responder.send(&mut result);
}
};
}
Ok(())
}
/// Handles calls to Launcher.Launch().
///
/// # Parameters
/// - configuration: The launch configuration for the new session.
async fn handle_launch_request(
&mut self,
configuration: LaunchConfiguration,
) -> Result<(), LaunchError> {
if let Some(session_url) = configuration.session_url {
let mut state = self.state.lock().await;
startup::launch_session(&session_url, &state.realm)
.await
.map_err(|err| match err {
startup::StartupError::NotDestroyed { .. } => {
LaunchError::DestroyComponentFailed
}
startup::StartupError::NotCreated { err, .. } => match err {
fcomponent::Error::InstanceCannotResolve => LaunchError::NotFound,
_ => LaunchError::CreateComponentFailed,
},
startup::StartupError::ExposedDirNotOpened { .. } => {
LaunchError::CreateComponentFailed
}
startup::StartupError::NotLaunched { .. } => LaunchError::CreateComponentFailed,
})
.map(|session_exposed_dir_channel| {
state.session_url = Some(session_url);
state.session_exposed_dir_channel = Some(session_exposed_dir_channel);
})
} else {
Err(LaunchError::NotFound)
}
}
/// Handles calls to Restarter.Restart().
async fn handle_restart_request(&mut self) -> Result<(), RestartError> {
let mut state = self.state.lock().await;
if let Some(ref session_url) = state.session_url {
startup::launch_session(&session_url, &state.realm)
.await
.map_err(|err| match err {
startup::StartupError::NotDestroyed { .. } => {
RestartError::DestroyComponentFailed
}
startup::StartupError::NotCreated { err, .. } => match err {
fcomponent::Error::InstanceCannotResolve => RestartError::NotFound,
_ => RestartError::CreateComponentFailed,
},
startup::StartupError::ExposedDirNotOpened { .. } => {
RestartError::CreateComponentFailed
}
startup::StartupError::NotLaunched { .. } => {
RestartError::CreateComponentFailed
}
})
.map(|session_exposed_dir_channel| {
state.session_exposed_dir_channel = Some(session_exposed_dir_channel);
})
} else {
Err(RestartError::NotRunning)
}
}
}
#[cfg(test)]
mod tests {
use {
super::SessionManager,
fidl::endpoints::{create_proxy_and_stream, spawn_stream_handler},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_element as felement,
fidl_fuchsia_session::{
LaunchConfiguration, LauncherMarker, LauncherProxy, RestartError, RestarterMarker,
RestarterProxy,
},
futures::prelude::*,
session_testing::spawn_noop_directory_server,
};
fn serve_session_manager_services(
session_manager: SessionManager,
) -> (LauncherProxy, RestarterProxy) {
let (launcher_proxy, launcher_stream) =
create_proxy_and_stream::<LauncherMarker>().unwrap();
{
let mut session_manager_ = session_manager.clone();
fuchsia_async::Task::spawn(async move {
session_manager_
.handle_launcher_request_stream(launcher_stream)
.await
.expect("Session launcher request stream got an error.");
})
.detach();
}
let (restarter_proxy, restarter_stream) =
create_proxy_and_stream::<RestarterMarker>().unwrap();
{
let mut session_manager_ = session_manager.clone();
fuchsia_async::Task::spawn(async move {
session_manager_
.handle_restarter_request_stream(restarter_stream)
.await
.expect("Session restarter request stream got an error.");
})
.detach();
}
(launcher_proxy, restarter_proxy)
}
/// Verifies that Launcher.Launch creates a new session.
#[fuchsia::test]
async fn test_launch() {
let session_url = "session";
let realm = spawn_stream_handler(move |realm_request| async move {
match realm_request {
fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::CreateChild {
collection: _,
decl,
args: _,
responder,
} => {
assert_eq!(decl.url.unwrap(), session_url);
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
spawn_noop_directory_server(exposed_dir);
let _ = responder.send(&mut Ok(()));
}
_ => panic!("Realm handler received an unexpected request"),
};
})
.unwrap();
let session_manager = SessionManager::new(realm);
let (launcher, _restarter) = serve_session_manager_services(session_manager);
assert!(launcher
.launch(LaunchConfiguration {
session_url: Some(session_url.to_string()),
..LaunchConfiguration::EMPTY
})
.await
.is_ok());
}
/// Verifies that Launcher.Restart restarts an existing session.
#[fuchsia::test]
async fn test_restart() {
let session_url = "session";
let realm = spawn_stream_handler(move |realm_request| async move {
match realm_request {
fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::CreateChild {
collection: _,
decl,
args: _,
responder,
} => {
assert_eq!(decl.url.unwrap(), session_url);
let _ = responder.send(&mut Ok(()));
}
fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
spawn_noop_directory_server(exposed_dir);
let _ = responder.send(&mut Ok(()));
}
_ => panic!("Realm handler received an unexpected request"),
};
})
.unwrap();
let session_manager = SessionManager::new(realm);
let (launcher, restarter) = serve_session_manager_services(session_manager);
assert!(launcher
.launch(LaunchConfiguration {
session_url: Some(session_url.to_string()),
..LaunchConfiguration::EMPTY
})
.await
.expect("could not call Launch")
.is_ok());
assert!(restarter.restart().await.expect("could not call Restart").is_ok());
}
/// Verifies that Launcher.Restart return an error if there is no running existing session.
#[fuchsia::test]
async fn test_restart_error_not_running() {
let realm = spawn_stream_handler(move |_realm_request| async move {
panic!("Realm should not receive any requests as there is no session to launch")
})
.unwrap();
let session_manager = SessionManager::new(realm);
let (_launcher, restarter) = serve_session_manager_services(session_manager);
assert_eq!(
Err(RestartError::NotRunning),
restarter.restart().await.expect("could not call Restart")
);
}
#[fuchsia::test]
async fn handle_element_manager_request_stream_propagates_request_to_downstream_service() {
let (local_proxy, local_request_stream) =
create_proxy_and_stream::<felement::ManagerMarker>()
.expect("Failed to create local Manager proxy and stream");
let (downstream_proxy, mut downstream_request_stream) =
create_proxy_and_stream::<felement::ManagerMarker>()
.expect("Failed to create downstream Manager proxy and stream");
let element_url = "element_url";
let mut num_elements_proposed = 0;
let local_server_fut =
SessionManager::handle_manager_request_stream(local_request_stream, downstream_proxy);
let downstream_server_fut = async {
while let Some(request) = downstream_request_stream.try_next().await.unwrap() {
match request {
felement::ManagerRequest::ProposeElement { spec, responder, .. } => {
num_elements_proposed += 1;
assert_eq!(Some(element_url.to_string()), spec.component_url);
let _ = responder.send(&mut Ok(()));
}
}
}
};
let propose_and_drop_fut = async {
local_proxy
.propose_element(
felement::Spec {
component_url: Some(element_url.to_string()),
..felement::Spec::EMPTY
},
None,
)
.await
.expect("Failed to call ProposeElement")
.expect("Failed to propose element");
std::mem::drop(local_proxy); // Drop proxy to terminate `server_fut`.
};
let _ = future::join3(propose_and_drop_fut, local_server_fut, downstream_server_fut).await;
assert_eq!(num_elements_proposed, 1);
}
}