1use crate::cobalt;
6use anyhow::anyhow;
7use fidl::endpoints::{create_proxy, ServerEnd};
8use log::info;
9use thiserror::Error;
10use {
11 fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
12 fidl_fuchsia_component_sandbox as fsandbox, fidl_fuchsia_io as fio,
13 fidl_fuchsia_session as fsession, fuchsia_async as fasync,
14};
15
16#[derive(Debug, Error, Clone, PartialEq)]
18pub enum StartupError {
19 #[error("Existing session not destroyed at \"{}/{}\": {:?}", collection, name, err)]
20 NotDestroyed { name: String, collection: String, err: fcomponent::Error },
21
22 #[error("Session {} not created at \"{}/{}\": Bedrock error {:?}", url, collection, name, err)]
23 BedrockError { name: String, collection: String, url: String, err: String },
24
25 #[error("Session {} not created at \"{}/{}\": {:?}", url, collection, name, err)]
26 NotCreated { name: String, collection: String, url: String, err: fcomponent::Error },
27
28 #[error(
29 "Exposed directory of session {} at \"{}/{}\" not opened: {:?}",
30 url,
31 collection,
32 name,
33 err
34 )]
35 ExposedDirNotOpened { name: String, collection: String, url: String, err: fcomponent::Error },
36
37 #[error("Session {} not launched at \"{}/{}\": {:?}", url, collection, name, err)]
38 NotLaunched { name: String, collection: String, url: String, err: fcomponent::Error },
39
40 #[error("Attempt to restart a not running session")]
41 NotRunning,
42}
43
44impl From<StartupError> for fsession::LaunchError {
45 fn from(e: StartupError) -> fsession::LaunchError {
46 match e {
47 StartupError::NotDestroyed { .. } => fsession::LaunchError::DestroyComponentFailed,
48 StartupError::NotCreated { err, .. } => match err {
49 fcomponent::Error::InstanceCannotResolve => fsession::LaunchError::NotFound,
50 _ => fsession::LaunchError::CreateComponentFailed,
51 },
52 StartupError::ExposedDirNotOpened { .. }
53 | StartupError::BedrockError { .. }
54 | StartupError::NotLaunched { .. } => fsession::LaunchError::CreateComponentFailed,
55 StartupError::NotRunning => fsession::LaunchError::NotFound,
56 }
57 }
58}
59
60impl From<StartupError> for fsession::RestartError {
61 fn from(e: StartupError) -> fsession::RestartError {
62 match e {
63 StartupError::NotDestroyed { .. } => fsession::RestartError::DestroyComponentFailed,
64 StartupError::NotCreated { err, .. } => match err {
65 fcomponent::Error::InstanceCannotResolve => fsession::RestartError::NotFound,
66 _ => fsession::RestartError::CreateComponentFailed,
67 },
68 StartupError::ExposedDirNotOpened { .. }
69 | StartupError::BedrockError { .. }
70 | StartupError::NotLaunched { .. } => fsession::RestartError::CreateComponentFailed,
71 StartupError::NotRunning => fsession::RestartError::NotRunning,
72 }
73 }
74}
75
76impl From<StartupError> for fsession::LifecycleError {
77 fn from(e: StartupError) -> fsession::LifecycleError {
78 match e {
79 StartupError::NotDestroyed { .. } => fsession::LifecycleError::DestroyComponentFailed,
80 StartupError::NotCreated { err, .. } => match err {
81 fcomponent::Error::InstanceCannotResolve => {
82 fsession::LifecycleError::ResolveComponentFailed
83 }
84 _ => fsession::LifecycleError::CreateComponentFailed,
85 },
86 StartupError::ExposedDirNotOpened { .. }
87 | StartupError::BedrockError { .. }
88 | StartupError::NotLaunched { .. } => fsession::LifecycleError::CreateComponentFailed,
89 StartupError::NotRunning => fsession::LifecycleError::NotFound,
90 }
91 }
92}
93
94const SESSION_NAME: &str = "session";
96
97const SESSION_CHILD_COLLECTION: &str = "session";
100
101pub async fn launch_session(
116 session_url: &str,
117 config_capabilities: Vec<fdecl::Configuration>,
118 exposed_dir: ServerEnd<fio::DirectoryMarker>,
119 realm: &fcomponent::RealmProxy,
120) -> Result<fcomponent::ExecutionControllerProxy, StartupError> {
121 info!(session_url; "Launching session");
122
123 let start_time = zx::MonotonicInstant::get();
124 let controller = set_session(session_url, config_capabilities, realm, exposed_dir).await?;
125 let end_time = zx::MonotonicInstant::get();
126
127 fasync::Task::local(async move {
128 if let Ok(cobalt_logger) = cobalt::get_logger() {
129 let _ = cobalt::log_session_launch_time(cobalt_logger, start_time, end_time).await;
132 }
133 })
134 .detach();
135
136 Ok(controller)
137}
138
139pub async fn stop_session(realm: &fcomponent::RealmProxy) -> Result<(), StartupError> {
147 realm_management::destroy_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm)
148 .await
149 .map_err(|err| StartupError::NotDestroyed {
150 name: SESSION_NAME.to_string(),
151 collection: SESSION_CHILD_COLLECTION.to_string(),
152 err,
153 })
154}
155
156async fn create_config_dict(
157 config_capabilities: Vec<fdecl::Configuration>,
158) -> Result<Option<fsandbox::DictionaryRef>, anyhow::Error> {
159 if config_capabilities.is_empty() {
160 return Ok(None);
161 }
162 let dict_store =
163 fuchsia_component::client::connect_to_protocol::<fsandbox::CapabilityStoreMarker>()?;
164 let dict_id = 1;
165 dict_store.dictionary_create(dict_id).await?.map_err(|e| anyhow!("{:#?}", e))?;
166 let mut config_id = 2;
167 for config in config_capabilities {
168 let Some(value) = config.value else { continue };
169 let Some(key) = config.name else { continue };
170
171 dict_store
172 .import(
173 config_id,
174 fsandbox::Capability::Data(fsandbox::Data::Bytes(fidl::persist(&value)?)),
175 )
176 .await?
177 .map_err(|e| anyhow!("{:#?}", e))?;
178
179 dict_store
180 .dictionary_insert(dict_id, &fsandbox::DictionaryItem { key, value: config_id })
181 .await?
182 .map_err(|e| anyhow!("{:#?}", e))?;
183 config_id += 1;
184 }
185 let dict = dict_store.export(dict_id).await?.map_err(|e| anyhow!("{:#?}", e))?;
186 let fsandbox::Capability::Dictionary(dict) = dict else {
187 return Err(anyhow!("Bad bedrock capability type"));
188 };
189 Ok(Some(dict))
190}
191
192async fn set_session(
206 session_url: &str,
207 config_capabilities: Vec<fdecl::Configuration>,
208 realm: &fcomponent::RealmProxy,
209 exposed_dir: ServerEnd<fio::DirectoryMarker>,
210) -> Result<fcomponent::ExecutionControllerProxy, StartupError> {
211 realm_management::destroy_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm)
212 .await
213 .or_else(|err: fcomponent::Error| match err {
214 fcomponent::Error::InvalidArguments
217 | fcomponent::Error::InstanceNotFound
218 | fcomponent::Error::CollectionNotFound => Ok(()),
219 _ => Err(err),
220 })
221 .map_err(|err| StartupError::NotDestroyed {
222 name: SESSION_NAME.to_string(),
223 collection: SESSION_CHILD_COLLECTION.to_string(),
224 err,
225 })?;
226
227 let (controller, controller_server_end) = create_proxy::<fcomponent::ControllerMarker>();
228 let create_child_args = fcomponent::CreateChildArgs {
229 controller: Some(controller_server_end),
230 dictionary: create_config_dict(config_capabilities).await.map_err(|err| {
231 StartupError::BedrockError {
232 name: SESSION_NAME.to_string(),
233 collection: SESSION_CHILD_COLLECTION.to_string(),
234 url: session_url.to_string(),
235 err: format!("{err:#?}"),
236 }
237 })?,
238 ..Default::default()
239 };
240 realm_management::create_child_component(
241 SESSION_NAME,
242 session_url,
243 SESSION_CHILD_COLLECTION,
244 create_child_args,
245 realm,
246 )
247 .await
248 .map_err(|err| StartupError::NotCreated {
249 name: SESSION_NAME.to_string(),
250 collection: SESSION_CHILD_COLLECTION.to_string(),
251 url: session_url.to_string(),
252 err,
253 })?;
254
255 realm_management::open_child_component_exposed_dir(
256 SESSION_NAME,
257 SESSION_CHILD_COLLECTION,
258 realm,
259 exposed_dir,
260 )
261 .await
262 .map_err(|err| StartupError::ExposedDirNotOpened {
263 name: SESSION_NAME.to_string(),
264 collection: SESSION_CHILD_COLLECTION.to_string(),
265 url: session_url.to_string(),
266 err,
267 })?;
268
269 let (execution_controller, execution_controller_server_end) =
271 create_proxy::<fcomponent::ExecutionControllerMarker>();
272 controller
273 .start(fcomponent::StartChildArgs::default(), execution_controller_server_end)
274 .await
275 .map_err(|_| fcomponent::Error::Internal)
276 .and_then(std::convert::identity)
277 .map_err(|_err| StartupError::NotLaunched {
278 name: SESSION_NAME.to_string(),
279 collection: SESSION_CHILD_COLLECTION.to_string(),
280 url: session_url.to_string(),
281 err: fcomponent::Error::InstanceCannotStart,
282 })?;
283
284 Ok(execution_controller)
285}
286
287#[cfg(test)]
288#[allow(clippy::unwrap_used)]
289mod tests {
290 use super::{set_session, stop_session, SESSION_CHILD_COLLECTION, SESSION_NAME};
291 use anyhow::Error;
292 use fidl::endpoints::create_endpoints;
293 use fidl_test_util::spawn_stream_handler;
294 use session_testing::{spawn_directory_server, spawn_server};
295 use std::sync::LazyLock;
296 use test_util::Counter;
297 use {fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio};
298
299 #[fuchsia::test]
300 async fn set_session_calls_realm_methods_in_appropriate_order() -> Result<(), Error> {
301 static NUM_REALM_REQUESTS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
303
304 let session_url = "session";
305
306 let directory_request_handler = move |directory_request| match directory_request {
307 fio::DirectoryRequest::DeprecatedOpen { path: _, .. } => {
308 assert_eq!(NUM_REALM_REQUESTS.get(), 4);
309 }
310 _ => panic!("Directory handler received an unexpected request"),
311 };
312
313 let realm = spawn_stream_handler(move |realm_request| async move {
314 match realm_request {
315 fcomponent::RealmRequest::DestroyChild { child, responder } => {
316 assert_eq!(NUM_REALM_REQUESTS.get(), 0);
317 assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
318 assert_eq!(child.name, SESSION_NAME);
319
320 let _ = responder.send(Ok(()));
321 }
322 fcomponent::RealmRequest::CreateChild { collection, decl, args, responder } => {
323 assert_eq!(NUM_REALM_REQUESTS.get(), 1);
324 assert_eq!(decl.url.unwrap(), session_url);
325 assert_eq!(decl.name.unwrap(), SESSION_NAME);
326 assert_eq!(&collection.name, SESSION_CHILD_COLLECTION);
327
328 spawn_server(args.controller.unwrap(), move |controller_request| {
329 match controller_request {
330 fcomponent::ControllerRequest::Start { responder, .. } => {
331 let _ = responder.send(Ok(()));
332 }
333 fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(),
334 fcomponent::ControllerRequest::GetExposedDictionary { .. } => {
335 unimplemented!()
336 }
337 fcomponent::ControllerRequest::Destroy { .. } => {
338 unimplemented!()
339 }
340 fcomponent::ControllerRequest::_UnknownMethod { .. } => {
341 unimplemented!()
342 }
343 }
344 });
345
346 let _ = responder.send(Ok(()));
347 }
348 fcomponent::RealmRequest::OpenExposedDir { child, exposed_dir, responder } => {
349 assert_eq!(NUM_REALM_REQUESTS.get(), 2);
350 assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
351 assert_eq!(child.name, SESSION_NAME);
352
353 spawn_directory_server(exposed_dir, directory_request_handler);
354 let _ = responder.send(Ok(()));
355 }
356 _ => panic!("Realm handler received an unexpected request"),
357 }
358 NUM_REALM_REQUESTS.inc();
359 });
360
361 let (_exposed_dir, exposed_dir_server_end) = create_endpoints::<fio::DirectoryMarker>();
362 let _controller = set_session(session_url, vec![], &realm, exposed_dir_server_end).await?;
363
364 Ok(())
365 }
366
367 #[fuchsia::test]
368 async fn set_session_starts_component() -> Result<(), Error> {
369 static NUM_START_CALLS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
370
371 let session_url = "session";
372
373 let realm = spawn_stream_handler(move |realm_request| async move {
374 match realm_request {
375 fcomponent::RealmRequest::DestroyChild { responder, .. } => {
376 let _ = responder.send(Ok(()));
377 }
378 fcomponent::RealmRequest::CreateChild { args, responder, .. } => {
379 spawn_server(args.controller.unwrap(), move |controller_request| {
380 match controller_request {
381 fcomponent::ControllerRequest::Start { responder, .. } => {
382 NUM_START_CALLS.inc();
383 let _ = responder.send(Ok(()));
384 }
385 fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(),
386 fcomponent::ControllerRequest::GetExposedDictionary { .. } => {
387 unimplemented!()
388 }
389 fcomponent::ControllerRequest::Destroy { .. } => {
390 unimplemented!()
391 }
392 fcomponent::ControllerRequest::_UnknownMethod { .. } => {
393 unimplemented!()
394 }
395 }
396 });
397 let _ = responder.send(Ok(()));
398 }
399 fcomponent::RealmRequest::OpenExposedDir { responder, .. } => {
400 let _ = responder.send(Ok(()));
401 }
402 _ => panic!("Realm handler received an unexpected request"),
403 }
404 });
405
406 let (_exposed_dir, exposed_dir_server_end) = create_endpoints::<fio::DirectoryMarker>();
407 let _controller = set_session(session_url, vec![], &realm, exposed_dir_server_end).await?;
408 assert_eq!(NUM_START_CALLS.get(), 1);
409
410 Ok(())
411 }
412
413 #[fuchsia::test]
414 async fn stop_session_calls_destroy_child() -> Result<(), Error> {
415 static NUM_DESTROY_CHILD_CALLS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
416
417 let realm = spawn_stream_handler(move |realm_request| async move {
418 match realm_request {
419 fcomponent::RealmRequest::DestroyChild { child, responder } => {
420 assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 0);
421 assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
422 assert_eq!(child.name, SESSION_NAME);
423
424 let _ = responder.send(Ok(()));
425 }
426 _ => panic!("Realm handler received an unexpected request"),
427 }
428 NUM_DESTROY_CHILD_CALLS.inc();
429 });
430
431 stop_session(&realm).await?;
432 assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 1);
433
434 Ok(())
435 }
436}