1#![allow(clippy::let_unit_value)]
6
7use ::fidl::endpoints::RequestStream as _;
8use anyhow::{Context as _, Error, anyhow};
9use fidl_fuchsia_component_sandbox as fsandbox;
10use fidl_fuchsia_io as fio;
11use fidl_fuchsia_paver as fpaver;
12use fidl_fuchsia_process_lifecycle as flifecycle;
13use fidl_fuchsia_update as fupdate;
14use fidl_fuchsia_update_verify::HealthVerificationMarker;
15use fuchsia_async as fasync;
16use fuchsia_component::client::connect_to_protocol;
17use fuchsia_component::escrow::EscrowOperation;
18use fuchsia_component::server::ServiceFs;
19use fuchsia_inspect::health::Reporter as _;
20use fuchsia_inspect::{self as finspect};
21use futures::channel::oneshot;
22use futures::future::{FutureExt as _, TryFutureExt as _};
23use futures::stream::StreamExt as _;
24use log::{error, info, warn};
25use std::sync::Arc;
26
27mod fidl;
28mod metadata;
29mod reboot;
30
31const MINIMUM_REBOOT_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
35
36#[fuchsia::main(logging_tags = ["system-update-committer"])]
37pub fn main() -> Result<(), Error> {
38 info!("starting system-update-committer");
39
40 let mut executor = fasync::LocalExecutorBuilder::new().build();
41 let () = executor.run_singlethreaded(main_async()).map_err(|err| {
42 let err = anyhow!(err);
44 error!("error running system-update-committer: {:#}", err);
45 err
46 })?;
47
48 info!("stopping system-update-committer");
49 Ok(())
50}
51
52async fn main_async() -> Result<(), Error> {
53 match fuchsia_runtime::take_startup_handle(fuchsia_runtime::HandleInfo::new(
54 fuchsia_runtime::HandleType::EscrowedDictionary,
55 0,
56 )) {
57 Some(dictionary) => {
58 resume_from_escrow(fsandbox::DictionaryRef { token: dictionary.into() })
59 .await
60 .context("resume_from_idle_stop")
61 }
62 None => fresh_run().await.context("first_run"),
63 }
64}
65
66struct EscrowState {
67 p_internal: ::fidl::NullableHandle,
69 p_external: ::fidl::NullableHandle,
70 frozen_inspect: Option<::fidl::NullableHandle>,
72}
73
74async fn fresh_run() -> Result<(), Error> {
94 let reboot_deadline = std::time::Instant::now() + MINIMUM_REBOOT_WAIT;
95
96 let inspector = finspect::Inspector::default();
97 let inspect_controller =
98 inspect_runtime::publish(&inspector, inspect_runtime::PublishOptions::default());
99
100 let config = system_update_committer_config::Config::take_from_startup_handle();
101 let idle_timeout = if config.stop_on_idle_timeout_millis >= 0 {
102 zx::MonotonicDuration::from_millis(config.stop_on_idle_timeout_millis)
103 } else {
104 zx::MonotonicDuration::INFINITE
105 };
106 let commit_timeout = if config.commit_timeout_seconds >= 0 {
107 zx::MonotonicDuration::from_seconds(config.commit_timeout_seconds)
108 } else {
109 zx::MonotonicDuration::INFINITE
110 };
111 inspector
112 .root()
113 .record_child("structured_config", |config_node| config.record_inspect(config_node));
114
115 let verification_node = inspector.root().create_child("verification");
116 let commit_node = metadata::CommitInspect::new(inspector.root().create_child("commit"));
117 let mut health_node = finspect::health::Node::new(inspector.root());
118 let verification_node_ref = &verification_node;
119 let commit_node_ref = &commit_node;
120 let health_node_ref = &mut health_node;
121
122 let paver =
123 connect_to_protocol::<fpaver::PaverMarker>().context("while connecting to paver")?;
124 let (boot_manager, boot_manager_server_end) = ::fidl::endpoints::create_proxy();
125 paver
126 .find_boot_manager(boot_manager_server_end)
127 .context("transport error while calling find_boot_manager()")?;
128 let reboot_proxy =
129 connect_to_protocol::<fidl_fuchsia_hardware_power_statecontrol::AdminMarker>()
130 .context("while connecting to power state control")?;
131
132 let health_verification = connect_to_protocol::<HealthVerificationMarker>()
133 .context("while connecting to health verification")?;
134
135 let (p_internal, p_external) = zx::EventPair::create();
136 let p_internal_clone =
137 p_internal.duplicate_handle(zx::Rights::BASIC).context("while duplicating p_internal")?;
138
139 let (unblocker, blocker) = oneshot::channel();
140
141 let commit_fut = async move {
144 match metadata::put_metadata_in_happy_state(
145 &boot_manager,
146 &p_internal,
147 unblocker,
148 &health_verification,
149 commit_timeout,
150 verification_node_ref,
151 commit_node_ref,
152 )
153 .await
154 {
155 Err(e) => {
156 let msg = format!(
157 "Failed to commit system. Rebooting at {:?} given error {:#} and {:?}",
158 reboot_deadline,
159 anyhow!(e),
160 config
161 );
162 health_node_ref.set_unhealthy(&msg);
163 warn!("{msg}");
164 reboot::wait_and_reboot(fasync::Timer::new(reboot_deadline), &reboot_proxy).await;
165 }
166 Ok(commit_result) => {
167 info!("{}", commit_result.log_msg());
168 health_node_ref.set_ok();
169 }
170 }
171 }
172 .fuse();
173 let mut commit_fut = std::pin::pin!(commit_fut);
174
175 let p_external_clone =
176 p_external.duplicate_handle(zx::Rights::BASIC).context("while duplicating p_external")?;
177 let fidl_server = Arc::new(fidl::FuchsiaUpdateFidlServer::new(
178 p_external_clone,
179 blocker.map_err(|e| e.to_string()),
180 idle_timeout,
181 ));
182 let mut fs = ServiceFs::new_local();
183 fs.take_and_serve_directory_handle()
184 .context("while taking directory handle")?
185 .dir("svc")
186 .add_fidl_service(Services::CommitStatusProvider);
187 let fs = fs.until_stalled(idle_timeout);
188 let active_guard = fs.try_active_guard().unwrap();
189
190 let mut service_fut = async move {
191 let out_dir = fuchsia_sync::Mutex::new(None);
192 let () = fs
193 .for_each_concurrent(None, |item| async {
194 use fuchsia_component::server::Item;
195 match item {
196 Item::Request(Services::CommitStatusProvider(stream), _active_guard) => {
197 let () = fidl_server
198 .clone()
199 .handle_commit_status_provider_stream(stream)
200 .await
201 .unwrap_or_else(|e| {
202 warn!("handling CommitStatusProviderStream {e:#}");
203 });
204 }
205 Item::Stalled(outgoing_dir) => {
206 *out_dir.lock() = Some(outgoing_dir);
207 }
208 }
209 })
210 .await;
211 let out_dir = out_dir
212 .lock()
213 .take()
214 .expect("StallableServiceFs should return the out dir before ending");
215 Ok({
216 let frozen_inspect = if let Some(inspect_controller) = inspect_controller {
217 Some(
218 inspect_controller
219 .escrow_frozen(inspect_runtime::EscrowOptions::default())
220 .await
221 .context("freezing inspect")?
222 .token
223 .into_handle(),
224 )
225 } else {
226 None
227 };
228 (
229 EscrowState {
230 p_internal: p_internal_clone.into(),
231 p_external: p_external.into(),
232 frozen_inspect,
233 },
234 out_dir.into(),
235 )
236 })
237 }
238 .boxed_local()
239 .fuse();
240 let service_fut = futures::select! {
241 () = commit_fut => {
242 drop(active_guard);
245 service_fut
246 },
247 _ = service_fut => {
248 panic!("fidl service fut completed before commit fut. this should be impossible \
249 because of the active guard");
250 }
251 };
252
253 run_until_idle_or_component_stopped(service_fut).await
254}
255
256async fn resume_from_escrow(escrowed_state: fsandbox::DictionaryRef) -> Result<(), Error> {
258 let EscrowState { p_internal, p_external, frozen_inspect } =
259 EscrowState::load(escrowed_state).await.context("loading escrowed state")?;
260
261 let config = system_update_committer_config::Config::take_from_startup_handle();
262 let idle_timeout = if config.stop_on_idle_timeout_millis >= 0 {
263 zx::MonotonicDuration::from_millis(config.stop_on_idle_timeout_millis)
264 } else {
265 zx::MonotonicDuration::INFINITE
266 };
267
268 let p_external_clone =
269 p_external.duplicate_handle(zx::Rights::BASIC).context("while duplicating p_external")?;
270 let fidl_server = Arc::new(fidl::FuchsiaUpdateFidlServer::new(
271 p_external_clone.into(),
272 futures::future::ready(Ok(())),
273 idle_timeout,
274 ));
275 let mut fs = ServiceFs::new_local();
276 fs.take_and_serve_directory_handle()
277 .context("while taking directory handle")?
278 .dir("svc")
279 .add_fidl_service(Services::CommitStatusProvider);
280 let fs = fs.until_stalled(idle_timeout);
281
282 let service_fut = async move {
283 let out_dir = fuchsia_sync::Mutex::new(None);
284 let () = fs
285 .for_each_concurrent(None, |item| async {
286 use fuchsia_component::server::Item;
287 match item {
288 Item::Request(Services::CommitStatusProvider(stream), _active_guard) => {
289 let () = fidl_server
290 .clone()
291 .handle_commit_status_provider_stream(stream)
292 .await
293 .unwrap_or_else(|e| {
294 warn!("handling CommitStatusProviderStream {e:#}");
295 });
296 }
297 Item::Stalled(outgoing_dir) => {
298 *out_dir.lock() = Some(outgoing_dir);
299 }
300 }
301 })
302 .await;
303 let out_dir = out_dir
304 .lock()
305 .take()
306 .expect("StallableServiceFs should return the out dir before ending");
307 Ok((EscrowState { p_internal, p_external, frozen_inspect }, out_dir.into()))
308 }
309 .boxed_local()
310 .fuse();
311 run_until_idle_or_component_stopped(service_fut).await
312}
313
314#[allow(clippy::type_complexity)]
317async fn run_until_idle_or_component_stopped(
318 mut service_fut: futures::future::Fuse<
319 futures::future::LocalBoxFuture<
320 '_,
321 Result<(EscrowState, ::fidl::endpoints::ServerEnd<fio::DirectoryMarker>), Error>,
322 >,
323 >,
324) -> Result<(), Error> {
325 let lifecycle = fuchsia_runtime::take_startup_handle(fuchsia_runtime::HandleInfo::new(
332 fuchsia_runtime::HandleType::Lifecycle,
333 0,
334 ))
335 .context("taking lifecycle handle")?;
336 let lifecycle: ::fidl::endpoints::ServerEnd<flifecycle::LifecycleMarker> = lifecycle.into();
337 let (mut lifecycle_request_stream, lifecycle_controller) =
338 lifecycle.into_stream_and_control_handle();
339 let escrow_operation = EscrowOperation::new_with_control_handle(lifecycle_controller);
340
341 futures::select! {
342 res = service_fut => {
343 let (state, out_dir) = res?;
344 let escrowed_dictionary = state.store().await.context("escrowing state")?;
345 escrow_operation.with_fsandbox_dictionary(escrowed_dictionary);
346 escrow_operation.run(out_dir).context("failed to run escrow operation")?;
347 Ok(())
348 },
349 req = lifecycle_request_stream.next() => {
350 match req.ok_or_else(|| anyhow::anyhow!("LifecycleRequest stream closed unexpectedly"))?
351 .context("error reading from LifecycleRequest stream")?
352 {
353 flifecycle::LifecycleRequest::Stop{ control_handle} => {
354 info!(
357 "received flifecycle::LifecycleRequest::Stop. Any client connections will \
358 be closed. This should only happen during shutdown."
359 );
360 drop((control_handle, escrow_operation));
368 let (inner, _terminated): (_, bool) = lifecycle_request_stream.into_inner();
372 let inner = std::sync::Arc::try_unwrap(inner).map_err(
373 |_: std::sync::Arc<_>| {
374 anyhow::anyhow!("failed to extract lifecycle channel from Arc")
375 },
376 )?;
377 let inner: zx::Channel = inner.into_channel().into_zx_channel();
378 std::mem::forget(inner);
379 Ok(())
380 }
381 }
382 }
383 }
384}
385
386impl EscrowState {
387 const INTERNAL_EVENTPAIR: &'static str = "p_internal";
388 const EXTERNAL_EVENTPAIR: &'static str = "p_external";
389 const INSPECT: &'static str = "frozen_inspect";
390
391 async fn load(dict: fsandbox::DictionaryRef) -> Result<Self, Error> {
392 let store =
393 fuchsia_component::client::connect_to_protocol::<fsandbox::CapabilityStoreMarker>()?;
394 let id_generator = sandbox::CapabilityIdGenerator::new();
395
396 let dict_id = id_generator.next();
397 let () = store
398 .import(dict_id, fsandbox::Capability::Dictionary(dict))
399 .await?
400 .map_err(|e| anyhow!("{e:?}"))?;
401
402 let remove_from_dict = |key: &'static str| async {
403 let id = id_generator.next();
404 match store
405 .dictionary_remove(dict_id, key, Some(&fsandbox::WrappedNewCapabilityId { id }))
406 .await?
407 {
408 Ok(()) => {
409 let fsandbox::Capability::Handle(handle) =
410 store.export(id).await?.map_err(|e| anyhow!("{e:?}"))?
411 else {
412 anyhow::bail!("Bad capability type from dictionary");
413 };
414 Ok(Some(handle))
415 }
416 Err(fsandbox::CapabilityStoreError::ItemNotFound) => Ok(None),
417 Err(e) => {
418 anyhow::bail!("exporting frozen inspect {e:?}");
419 }
420 }
421 };
422
423 let p_internal = remove_from_dict(Self::INTERNAL_EVENTPAIR)
424 .await?
425 .ok_or_else(|| anyhow!("internal eventpair missing from escrow"))?;
426 let p_external = remove_from_dict(Self::EXTERNAL_EVENTPAIR)
427 .await?
428 .ok_or_else(|| anyhow!("external eventpair missing from escrow"))?;
429 let frozen_inspect = remove_from_dict(Self::INSPECT).await?;
430
431 Ok(Self { p_internal, p_external, frozen_inspect })
436 }
437
438 async fn store(self) -> Result<fsandbox::DictionaryRef, Error> {
439 let Self { p_internal, p_external, frozen_inspect } = self;
440 let store =
441 fuchsia_component::client::connect_to_protocol::<fsandbox::CapabilityStoreMarker>()?;
442 let id_generator = sandbox::CapabilityIdGenerator::new();
443 let dict_id = id_generator.next();
444 let () = store.dictionary_create(dict_id).await?.map_err(|e| anyhow!("{e:?}"))?;
445
446 let insert_in_dict = |handle, key| async {
447 let id = id_generator.next();
448 let () = store
449 .import(id, fsandbox::Capability::Handle(handle))
450 .await?
451 .map_err(|e| anyhow!("{e:?}"))?;
452 let () = store
453 .dictionary_insert(dict_id, &fsandbox::DictionaryItem { key, value: id })
454 .await?
455 .map_err(|e| anyhow!("{e:?}"))?;
456 Result::<_, anyhow::Error>::Ok(())
457 };
458
459 if let Some(frozen_inspect) = frozen_inspect {
460 let () = insert_in_dict(frozen_inspect, Self::INSPECT.into()).await?;
461 }
462 let () = insert_in_dict(p_internal, Self::INTERNAL_EVENTPAIR.into()).await?;
463 let () = insert_in_dict(p_external, Self::EXTERNAL_EVENTPAIR.into()).await?;
464
465 let fsandbox::Capability::Dictionary(dictionary_ref) =
466 store.export(dict_id).await?.map_err(|e| anyhow!("{e:?}"))?
467 else {
468 anyhow::bail!("Bad capability type from dictionary");
469 };
470 Ok(dictionary_ref)
471 }
472}
473
474enum Services {
475 CommitStatusProvider(fupdate::CommitStatusProviderRequestStream),
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[fasync::run_singlethreaded(test)]
483 async fn escrow_state_round_trip() {
484 let (p_internal, p_external) = zx::EventPair::create();
485 let frozen_inspect = Some(zx::Event::create().into());
486
487 let state = EscrowState {
488 p_internal: p_internal.into(),
489 p_external: p_external.into(),
490 frozen_inspect,
491 };
492
493 let stored = state.store().await.unwrap();
494 let loaded = EscrowState::load(stored).await.unwrap();
495 assert!(loaded.frozen_inspect.is_some());
496 }
497
498 #[fasync::run_singlethreaded(test)]
499 async fn escrow_state_round_trip_missing_inspect() {
500 let (p_internal, p_external) = zx::EventPair::create();
501
502 let state = EscrowState {
503 p_internal: p_internal.into(),
504 p_external: p_external.into(),
505 frozen_inspect: None,
506 };
507
508 let stored = state.store().await.unwrap();
509 let loaded = EscrowState::load(stored).await.unwrap();
510 assert_eq!(loaded.frozen_inspect, None);
511 }
512}