blob: 6156a1bda1ca34cfb7cfdec6dda0b889da2310a0 [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_input_injection::{
InputDeviceRegistryMarker, InputDeviceRegistryProxy, InputDeviceRegistryRequest,
InputDeviceRegistryRequestStream,
},
fidl_fuchsia_session::{
ElementManagerMarker, ElementManagerProxy, ElementManagerRequest,
ElementManagerRequestStream, LaunchConfiguration, LaunchError, LauncherRequest,
LauncherRequestStream, RestartError, RestarterRequest, RestarterRequestStream,
},
fidl_fuchsia_sys2 as fsys, fidl_fuchsia_ui_lifecycle as fui_lifecycle,
fuchsia_component::server::ServiceFs,
fuchsia_syslog::fx_log_warn,
fuchsia_zircon as zx,
futures::lock::Mutex,
futures::{StreamExt, TryStreamExt},
std::sync::Arc,
};
/// The services exposed by the session manager.
enum ExposedServices {
Manager(felement::ManagerRequestStream),
ElementManager(ElementManagerRequestStream),
Launcher(LauncherRequestStream),
Restarter(RestarterRequestStream),
InputDeviceRegistry(InputDeviceRegistryRequestStream),
}
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: fsys::RealmProxy,
/// Proxy to the scenic lifecycle service.
scenic_lifecycle: Option<fui_lifecycle::LifecycleControllerProxy>,
}
/// 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.
/// - `scenic_lifecycle`: Proxy to the scenic lifecycle service.
pub fn new(
realm: fsys::RealmProxy,
scenic_lifecycle: Option<fui_lifecycle::LifecycleControllerProxy>,
) -> SessionManager {
let state = SessionManagerState {
session_url: None,
session_exposed_dir_channel: None,
realm,
scenic_lifecycle,
};
SessionManager { state: Arc::new(Mutex::new(state)) }
}
/// Launch the session specified in the session manager startup configuration, if any.
///
/// # Errors
/// Returns an error if the session could not be launched.
pub async fn launch_startup_session(&mut self) -> Result<(), Error> {
let mut state = self.state.lock().await;
if let Some(session_url) = startup::get_session_url() {
state.session_exposed_dir_channel =
Some(startup::launch_session(&session_url, &state.realm).await?);
state.session_url = Some(session_url);
} else {
fx_log_warn!("No startup session specified");
}
Ok(())
}
/// Starts serving [`ExposedServices`] 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 expose_services(&mut self) -> Result<(), Error> {
let mut fs = ServiceFs::new_local();
fs.dir("svc")
.add_fidl_service(ExposedServices::Manager)
.add_fidl_service(ExposedServices::ElementManager)
.add_fidl_service(ExposedServices::Launcher)
.add_fidl_service(ExposedServices::Restarter)
.add_fidl_service(ExposedServices::InputDeviceRegistry);
fs.take_and_serve_directory_handle()?;
while let Some(service_request) = fs.next().await {
match service_request {
ExposedServices::Manager(request_stream) => {
// Connect to element.Manager served by the session.
let (manager_proxy, server_end) =
fidl::endpoints::create_proxy::<felement::ManagerMarker>()
.expect("Failed to create ManagerProxy");
{
let state = self.state.lock().await;
let session_exposed_dir_channel =
state.session_exposed_dir_channel.as_ref().expect(
"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(),
)
.expect("Failed to connect to Manager service");
}
SessionManager::handle_manager_request_stream(request_stream, manager_proxy)
.await
.expect("Manager request stream got an error.");
}
ExposedServices::ElementManager(request_stream) => {
// Connect to ElementManager served by the session.
let (element_manager_proxy, server_end) =
fidl::endpoints::create_proxy::<ElementManagerMarker>()
.expect("Failed to create ElementManagerProxy");
{
let state = self.state.lock().await;
let session_exposed_dir_channel = state.session_exposed_dir_channel.as_ref()
.expect("Failed to connect to ElementManagerProxy because no session was started");
fdio::service_connect_at(
session_exposed_dir_channel,
"fuchsia.session.ElementManager",
server_end.into_channel(),
)
.expect("Failed to connect to ElementManager service");
}
SessionManager::handle_element_manager_request_stream(
request_stream,
element_manager_proxy,
)
.await
.expect("ElementManager request stream got an error.");
}
ExposedServices::Launcher(request_stream) => {
self.handle_launcher_request_stream(request_stream)
.await
.expect("Launcher request stream got an error.");
}
ExposedServices::Restarter(request_stream) => {
self.handle_restarter_request_stream(request_stream)
.await
.expect("Restarter request stream got an error.");
}
ExposedServices::InputDeviceRegistry(request_stream) => {
// Connect to InputDeviceRegistry served by the session.
let (input_device_registry_proxy, server_end) =
fidl::endpoints::create_proxy::<InputDeviceRegistryMarker>()
.expect("Failed to create InputDeviceRegistryProxy");
{
let state = self.state.lock().await;
let session_exposed_dir_channel = state.session_exposed_dir_channel.as_ref()
.expect("Failed to connect to InputDeviceRegistryProxy because no session was started");
fdio::service_connect_at(
session_exposed_dir_channel,
"fuchsia.input.injection.InputDeviceRegistry",
server_end.into_channel(),
)
.expect("Failed to connect to InputDeviceRegistry service");
}
SessionManager::handle_input_device_registry_request_stream(
request_stream,
input_device_registry_proxy,
)
.await
.expect("InputDeviceRegistry 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 [`ElementManagerRequestStream`].
///
/// # Parameters
/// - `request_stream`: the ElementManagerRequestStream.
/// - `element_manager_proxy`: the ElementManagerProxy that will handle the relayed commands.
///
/// # Errors
/// When an error is encountered reading from the request stream.
pub async fn handle_element_manager_request_stream(
mut request_stream: ElementManagerRequestStream,
element_manager_proxy: ElementManagerProxy,
) -> Result<(), Error> {
while let Some(request) = request_stream
.try_next()
.await
.context("Error handling Element Manager request stream")?
{
match request {
ElementManagerRequest::ProposeElement { spec, element_controller, responder } => {
let mut result =
element_manager_proxy.propose_element(spec, element_controller).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(())
}
/// 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(())
}
/// Serves a specified [`InputDeviceRegistryRequestStream`].
///
/// # Parameters
/// - `request_stream`: the InputDeviceRegistryRequestStream.
/// - `input_device_registry_proxy`: the downstream InputDeviceRegistryProxy
/// to which requests will be relayed.
///
/// # Errors
/// When an error is encountered reading from the request stream.
pub async fn handle_input_device_registry_request_stream(
mut request_stream: InputDeviceRegistryRequestStream,
input_device_registry_proxy: InputDeviceRegistryProxy,
) -> Result<(), Error> {
while let Some(request) = request_stream
.try_next()
.await
.context("Error handling input device registry request stream")?
{
match request {
InputDeviceRegistryRequest::Register { device, .. } => {
input_device_registry_proxy
.register(device)
.context("Error handling InputDeviceRegistryRequest::Register")?;
}
}
}
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 {
name: _,
collection: _,
url: _,
err: sys_err,
} => match sys_err {
fcomponent::Error::InstanceCannotResolve => LaunchError::NotFound,
_ => LaunchError::CreateComponentFailed,
},
startup::StartupError::NotBound { .. } => 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(scenic_lifecycle) = &state.scenic_lifecycle {
if let Err(_) = scenic_lifecycle.terminate() {
return Err(RestartError::NotRunning);
}
}
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 {
name: _,
collection: _,
url: _,
err: sys_err,
} => match sys_err {
fcomponent::Error::InstanceCannotResolve => RestartError::NotFound,
_ => RestartError::CreateComponentFailed,
},
startup::StartupError::NotBound { .. } => 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::encoding::Decodable,
fidl::endpoints::{create_endpoints, create_proxy_and_stream},
fidl_fuchsia_input_injection::{InputDeviceRegistryMarker, InputDeviceRegistryRequest},
fidl_fuchsia_input_report::InputDeviceMarker,
fidl_fuchsia_session::{
ElementManagerMarker, ElementManagerRequest, ElementSpec, LaunchConfiguration,
LauncherMarker, LauncherProxy, RestartError, RestarterMarker, RestarterProxy,
},
fidl_fuchsia_sys2 as fsys, fuchsia_async as fasync,
futures::prelude::*,
matches::assert_matches,
};
/// Spawns a local `fidl_fuchsia_sys2::Realm` server, and returns a proxy to the spawned server.
/// The provided `request_handler` is notified when an incoming request is received.
///
/// # Parameters
/// - `request_handler`: A function which is called with incoming requests to the spawned
/// `Realm` server.
/// # Returns
/// A `RealmProxy` to the spawned server.
fn spawn_realm_server<F: 'static>(mut request_handler: F) -> fsys::RealmProxy
where
F: FnMut(fsys::RealmRequest) + Send,
{
let (realm_proxy, mut realm_server) = create_proxy_and_stream::<fsys::RealmMarker>()
.expect("Failed to create realm proxy and server.");
fasync::Task::spawn(async move {
while let Some(realm_request) = realm_server.try_next().await.unwrap() {
request_handler(realm_request);
}
})
.detach();
realm_proxy
}
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.
#[fasync::run_until_stalled(test)]
async fn test_launch() {
let session_url = "session";
let realm = spawn_realm_server(move |realm_request| {
match realm_request {
fsys::RealmRequest::DestroyChild { child: _, responder } => {
let _ = responder.send(&mut Ok(()));
}
fsys::RealmRequest::CreateChild { collection: _, decl, responder } => {
assert_eq!(decl.url.unwrap(), session_url);
let _ = responder.send(&mut Ok(()));
}
fsys::RealmRequest::BindChild { child: _, exposed_dir: _, responder } => {
let _ = responder.send(&mut Ok(()));
}
_ => {
assert!(false);
}
};
});
let session_manager = SessionManager::new(realm, None);
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.
#[fasync::run_until_stalled(test)]
async fn test_restart() {
let session_url = "session";
let realm = spawn_realm_server(move |realm_request| {
match realm_request {
fsys::RealmRequest::DestroyChild { child: _, responder } => {
let _ = responder.send(&mut Ok(()));
}
fsys::RealmRequest::CreateChild { collection: _, decl, responder } => {
assert_eq!(decl.url.unwrap(), session_url);
let _ = responder.send(&mut Ok(()));
}
fsys::RealmRequest::BindChild { child: _, exposed_dir: _, responder } => {
let _ = responder.send(&mut Ok(()));
}
_ => {
assert!(false);
}
};
});
let session_manager = SessionManager::new(realm, None);
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.
#[fasync::run_until_stalled(test)]
async fn test_restart_error_not_running() {
let realm = spawn_realm_server(move |_realm_request| {
assert!(false);
});
let session_manager = SessionManager::new(realm, None);
let (_launcher, restarter) = serve_session_manager_services(session_manager);
assert_eq!(
Err(RestartError::NotRunning),
restarter.restart().await.expect("could not call Restart")
);
}
#[fasync::run_until_stalled(test)]
async fn handle_input_device_registry_request_stream_propagates_request_to_downstream_service()
{
let (local_proxy, local_request_stream) =
create_proxy_and_stream::<InputDeviceRegistryMarker>()
.expect("Failed to create local InputDeviceRegistry proxy and stream");
let (downstream_proxy, mut downstream_request_stream) =
create_proxy_and_stream::<InputDeviceRegistryMarker>()
.expect("Failed to create downstream InputDeviceRegistry proxy and stream");
let mut num_devices_registered = 0;
let local_server_fut = SessionManager::handle_input_device_registry_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 {
InputDeviceRegistryRequest::Register { .. } => num_devices_registered += 1,
}
}
};
let (input_device_client, _input_device_server) = create_endpoints::<InputDeviceMarker>()
.expect("Failed to create InputDevice endpoints");
local_proxy
.register(input_device_client)
.expect("Failed to send registration request locally");
std::mem::drop(local_proxy); // Drop proxy to terminate `server_fut`.
assert_matches!(local_server_fut.await, Ok(()));
downstream_server_fut.await;
assert_eq!(num_devices_registered, 1);
}
#[fasync::run_until_stalled(test)]
async fn handle_element_manager_request_stream_propagates_request_to_downstream_service() {
let (local_proxy, local_request_stream) = create_proxy_and_stream::<ElementManagerMarker>()
.expect("Failed to create local ElementManager proxy and stream");
let (downstream_proxy, mut downstream_request_stream) =
create_proxy_and_stream::<ElementManagerMarker>()
.expect("Failed to create downstream ElementManager proxy and stream");
let element_url = "element_url";
let mut num_elements_proposed = 0;
let local_server_fut = SessionManager::handle_element_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 {
ElementManagerRequest::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(
ElementSpec {
component_url: Some(element_url.to_string()),
..ElementSpec::new_empty()
},
None,
)
.await
.expect("Failed to propose element")
.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);
}
}