Skip to main content

underhill_core/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This module implements the interactive control process and the entry point
5//! for the underhill environment.
6
7#![cfg(target_os = "linux")]
8#![expect(missing_docs)]
9#![forbid(unsafe_code)]
10
11mod diag;
12mod dispatch;
13mod emuplat;
14mod get_tracing;
15mod inspect_internal;
16mod inspect_proc;
17mod livedump;
18mod loader;
19#[cfg(feature = "product_policy")]
20mod measured_product_policy;
21mod nvme_manager;
22mod options;
23mod reference_time;
24mod servicing;
25mod threadpool_vm_task_backend;
26mod vmbus_relay_unit;
27mod vmgs_logger;
28mod vp;
29mod vpci;
30mod worker;
31mod wrapped_partition;
32
33// `pub` so that the missing_docs warning fires for options without
34// documentation.
35pub use options::Options;
36
37use crate::diag::DiagWorker;
38use crate::dispatch::UhVmRpc;
39use crate::worker::UnderhillEnvCfg;
40use crate::worker::UnderhillRemoteConsoleCfg;
41use crate::worker::UnderhillVmWorker;
42use crate::worker::UnderhillWorkerParameters;
43use anyhow::Context;
44use bootloader_fdt_parser::BootTimes;
45use cvm_tracing::CVM_ALLOWED;
46use framebuffer::FRAMEBUFFER_SIZE;
47use framebuffer::FramebufferAccess;
48use futures::StreamExt;
49use futures_concurrency::stream::Merge;
50use get_tracing::init_tracing;
51use get_tracing::init_tracing_backend;
52use inspect::Inspect;
53use inspect::SensitivityLevel;
54use mesh::CancelContext;
55use mesh::CancelReason;
56use mesh::MeshPayload;
57use mesh::error::RemoteError;
58use mesh::rpc::Rpc;
59use mesh::rpc::RpcSend;
60use mesh_process::Mesh;
61use mesh_process::ProcessConfig;
62use mesh_process::try_run_mesh_host;
63use mesh_tracing::RemoteTracer;
64use mesh_tracing::TracingBackend;
65use mesh_worker::RegisteredWorkers;
66use mesh_worker::WorkerEvent;
67use mesh_worker::WorkerHandle;
68use mesh_worker::WorkerHost;
69use mesh_worker::WorkerHostRunner;
70use mesh_worker::launch_local_worker;
71use mesh_worker::register_workers;
72use pal_async::DefaultDriver;
73use pal_async::DefaultPool;
74use pal_async::task::Spawn;
75#[cfg(feature = "profiler")]
76use profiler_worker::ProfilerWorker;
77#[cfg(feature = "profiler")]
78use profiler_worker::ProfilerWorkerParameters;
79use std::time::Duration;
80use vmsocket::VmAddress;
81use vmsocket::VmListener;
82use vnc_worker_defs::VncParameters;
83
84fn new_underhill_remote_console_cfg(
85    framebuffer_gpa_base: Option<u64>,
86) -> anyhow::Result<(
87    UnderhillRemoteConsoleCfg,
88    Option<FramebufferAccess>,
89    Option<mesh::Receiver<Vec<video_core::DirtyRect>>>,
90)> {
91    if let Some(framebuffer_gpa_base) = framebuffer_gpa_base {
92        // Underhill accesses the framebuffer by using /dev/mshv_vtl_low to read
93        // from a second mapping placed after the end of RAM at a static
94        // location specified by the host.
95        //
96        // Open the file directly rather than use the `hcl` crate to avoid
97        // leaking `hcl` stuff into this crate.
98        //
99        // FUTURE: use an approach that doesn't require double mapping the
100        // framebuffer from the host.
101        let gpa_fd = fs_err::OpenOptions::new()
102            .read(true)
103            .write(true)
104            .open("/dev/mshv_vtl_low")
105            .context("failed to open gpa device")?;
106
107        let vram = sparse_mmap::new_mappable_from_file(gpa_fd.file(), true, false)?;
108        let (fb, fba) = framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, framebuffer_gpa_base)
109            .context("allocating framebuffer")?;
110        tracing::debug!("framebuffer_gpa_base: {:#x}", framebuffer_gpa_base);
111
112        let (dirt_send, dirt_recv) = mesh::channel();
113
114        Ok((
115            UnderhillRemoteConsoleCfg {
116                synth_keyboard: true,
117                synth_mouse: true,
118                synth_video: true,
119                input: mesh::Receiver::new(),
120                framebuffer: Some(fb),
121                dirt_send: Some(dirt_send),
122            },
123            Some(fba),
124            Some(dirt_recv),
125        ))
126    } else {
127        Ok((
128            UnderhillRemoteConsoleCfg {
129                synth_keyboard: false,
130                synth_mouse: false,
131                synth_video: false,
132                input: mesh::Receiver::new(),
133                framebuffer: None,
134                dirt_send: None,
135            },
136            None,
137            None,
138        ))
139    }
140}
141
142pub fn main() -> anyhow::Result<()> {
143    // Install a panic hook to prefix the current async task name before the
144    // standard panic output.
145    install_task_name_panic_hook();
146
147    if let Some(path) = std::env::var_os("OPENVMM_WRITE_SAVED_STATE_PROTO") {
148        if cfg!(debug_assertions) {
149            mesh::payload::protofile::DescriptorWriter::new(
150                vmcore::save_restore::saved_state_roots(),
151            )
152            .write_to_path(path)
153            .context("failed to write protobuf descriptors")?;
154            return Ok(());
155        } else {
156            // The generated code for this is too large for release builds.
157            anyhow::bail!(".proto output only supported in debug builds");
158        }
159    }
160
161    // FUTURE: create and use the affinitized threadpool here.
162    let (_, tracing_driver) = DefaultPool::spawn_on_thread("tracing");
163
164    // Try to run as a worker host, sending a remote tracer that will forward
165    // tracing events back to the initial process for logging to the host. See
166    // [`get_tracing`] doc comments for more details.
167    //
168    // On success the worker runs to completion and then exits the process (does
169    // not return). Any worker host setup errors are return and bubbled up.
170    try_run_mesh_host("underhill", {
171        let tracing_driver = tracing_driver.clone();
172        async |params: MeshHostParams| {
173            if let Some(remote_tracer) = params.tracer {
174                init_tracing(tracing_driver, remote_tracer).context("failed to init tracing")?;
175            }
176            params.runner.run(RegisteredWorkers).await;
177            Ok(())
178        }
179    })?;
180
181    // Initialize the tracing backend used by this and all subprocesses.
182    let mut tracing = init_tracing_backend(tracing_driver.clone())?;
183    // Initialize tracing from the backend.
184    init_tracing(tracing_driver, tracing.tracer()).context("failed to init tracing")?;
185    DefaultPool::run_with(|driver| do_main(driver, tracing))
186}
187
188fn install_task_name_panic_hook() {
189    use std::io::Write;
190
191    let panic_hook = std::panic::take_hook();
192    std::panic::set_hook(Box::new(move |info| {
193        pal_async::task::with_current_task_metadata(|metadata| {
194            if let Some(metadata) = metadata {
195                let _ = write!(std::io::stderr(), "task '{}', ", metadata.name());
196            }
197        });
198        // This will proceed with writing "thread ... panicked at ..."
199        panic_hook(info);
200    }));
201}
202
203async fn do_main(driver: DefaultDriver, mut tracing: TracingBackend) -> anyhow::Result<()> {
204    let opt = Options::parse(Vec::new(), Vec::new())?;
205
206    let crate_name = build_info::get().crate_name();
207    let crate_revision = build_info::get().scm_revision();
208    let openhcl_version = build_info::get().openhcl_version();
209    tracing::info!(
210        CVM_ALLOWED,
211        ?crate_name,
212        ?crate_revision,
213        ?openhcl_version,
214        "VMM process"
215    );
216    log_boot_times().context("failure logging boot times")?;
217
218    // Write the current pid to a file.
219    if let Some(pid_path) = &opt.pid {
220        std::fs::write(pid_path, std::process::id().to_string())
221            .with_context(|| format!("failed to write pid to {}", pid_path.display()))?;
222    }
223
224    let mesh = Mesh::new("underhill".to_string()).context("failed to create mesh")?;
225
226    let r = run_control(driver, &mesh, opt, &mut tracing).await;
227    if let Err(err) = &r {
228        tracing::error!(
229            CVM_ALLOWED,
230            error = err.as_ref() as &dyn std::error::Error,
231            "VM failure"
232        );
233    }
234
235    // Wait a few seconds for child processes to terminate and tracing to finish.
236    CancelContext::new()
237        .with_timeout(Duration::from_secs(10))
238        .until_cancelled(async {
239            mesh.shutdown().await;
240            tracing.shutdown().await;
241        })
242        .await
243        .ok();
244
245    r
246}
247
248fn log_boot_times() -> anyhow::Result<()> {
249    fn diff(start: Option<u64>, end: Option<u64>) -> Option<tracing::field::DebugValue<Duration>> {
250        use reference_time::ReferenceTime;
251        Some(tracing::field::debug(
252            ReferenceTime::new(end?).since(ReferenceTime::new(start?))?,
253        ))
254    }
255
256    // Read boot times provided by the bootloader.
257    let BootTimes {
258        start,
259        end,
260        sidecar_start,
261        sidecar_end,
262    } = BootTimes::new().context("failed to parse boot times")?;
263    tracing::info!(
264        CVM_ALLOWED,
265        start,
266        end,
267        sidecar_start,
268        sidecar_end,
269        elapsed = diff(start, end),
270        sidecar_elapsed = diff(sidecar_start, sidecar_end),
271        "boot loader times"
272    );
273    Ok(())
274}
275
276struct DiagState {
277    _worker: WorkerHandle,
278    request_recv: mesh::Receiver<diag_server::DiagRequest>,
279}
280
281impl DiagState {
282    async fn new() -> anyhow::Result<Self> {
283        // Start the diagnostics worker immediately.
284        let (request_send, request_recv) = mesh::channel();
285        let worker = launch_local_worker::<DiagWorker>(diag::DiagWorkerParameters { request_send })
286            .await
287            .context("failed to launch diagnostics worker")?;
288        Ok(Self {
289            _worker: worker,
290            request_recv,
291        })
292    }
293}
294
295#[derive(Inspect)]
296struct Workers {
297    #[inspect(safe)]
298    vm: WorkerHandle,
299    #[inspect(skip)]
300    vm_rpc: mesh::Sender<UhVmRpc>,
301    vnc: Option<WorkerHandle>,
302    #[cfg(feature = "gdb")]
303    gdb: Option<WorkerHandle>,
304}
305
306#[derive(MeshPayload)]
307struct MeshHostParams {
308    tracer: Option<RemoteTracer>,
309    runner: WorkerHostRunner,
310}
311
312async fn launch_mesh_host(
313    mesh: &Mesh,
314    name: &str,
315    tracer: Option<RemoteTracer>,
316) -> anyhow::Result<WorkerHost> {
317    let (host, runner) = mesh_worker::worker_host();
318    mesh.launch_host(ProcessConfig::new(name), MeshHostParams { tracer, runner })
319        .await?;
320    Ok(host)
321}
322
323async fn launch_workers(
324    mesh: &Mesh,
325    tracing: &mut TracingBackend,
326    control_send: mesh::Sender<ControlRequest>,
327    opt: Options,
328) -> anyhow::Result<Workers> {
329    let env_cfg = UnderhillEnvCfg {
330        vmbus_max_version: opt.vmbus_max_version,
331        vmbus_enable_mnf: opt.vmbus_enable_mnf,
332        vmbus_force_confidential_external_memory: opt.vmbus_force_confidential_external_memory,
333        vmbus_channel_unstick_delay: (opt.vmbus_channel_unstick_delay_ms != 0)
334            .then(|| Duration::from_millis(opt.vmbus_channel_unstick_delay_ms)),
335        cmdline_append: opt.cmdline_append.clone(),
336        reformat_vmgs: opt.reformat_vmgs,
337        vtl0_starts_paused: opt.vtl0_starts_paused,
338        emulated_serial_wait_for_rts: opt.serial_wait_for_rts,
339        force_load_vtl0_image: opt.force_load_vtl0_image,
340        nvme_vfio: opt.nvme_vfio,
341        halt_on_guest_halt: opt.halt_on_guest_halt,
342        no_sidecar_hotplug: opt.no_sidecar_hotplug,
343        gdbstub: opt.gdbstub,
344        hide_isolation: opt.hide_isolation,
345        nvme_keep_alive: opt.nvme_keep_alive,
346        mana_keep_alive: opt.mana_keep_alive,
347        nvme_always_flr: opt.nvme_always_flr,
348        test_configuration: opt.test_configuration,
349        disable_uefi_frontpage: opt.disable_uefi_frontpage,
350        default_boot_always_attempt: opt.default_boot_always_attempt,
351        guest_state_lifetime: opt.guest_state_lifetime,
352        guest_state_encryption_policy: opt.guest_state_encryption_policy,
353        hardware_sealing_policy: opt.hardware_sealing_policy,
354        efi_diagnostics_log_level: opt.efi_diagnostics_log_level,
355        efi_diagnostics_rate_limit: opt.efi_diagnostics_rate_limit,
356        strict_encryption_policy: opt.strict_encryption_policy,
357        attempt_ak_cert_callback: opt.attempt_ak_cert_callback,
358        enable_vpci_relay: opt.enable_vpci_relay,
359        disable_proxy_redirect: opt.disable_proxy_redirect,
360        disable_lower_vtl_timer_virt: opt.disable_lower_vtl_timer_virt,
361        config_timeout_in_seconds: opt.config_timeout_in_seconds,
362        servicing_timeout_dump_collection_in_ms: opt.servicing_timeout_dump_collection_in_ms,
363    };
364
365    let (mut remote_console_cfg, framebuffer_access, dirty_rect_recv) =
366        new_underhill_remote_console_cfg(opt.framebuffer_gpa_base)?;
367
368    let mut vnc_worker = None;
369    if let Some(framebuffer) = framebuffer_access {
370        let listener = VmListener::bind(VmAddress::vsock_any(opt.vnc_port))
371            .context("failed to bind socket")?;
372
373        let input_send = remote_console_cfg.input.sender();
374
375        let vnc_host = launch_mesh_host(mesh, "vnc", Some(tracing.tracer()))
376            .await
377            .context("spawning vnc process failed")?;
378
379        vnc_worker = Some(
380            vnc_host
381                .launch_worker(
382                    vnc_worker_defs::VNC_WORKER_VMSOCKET,
383                    VncParameters {
384                        listener,
385                        framebuffer,
386                        input_send,
387                        dirty_recv: dirty_rect_recv,
388                        max_clients: 16,
389                        evict_oldest: false,
390                    },
391                )
392                .await?,
393        )
394    }
395
396    #[cfg(feature = "gdb")]
397    let mut gdbstub_worker = None;
398    #[cfg_attr(not(feature = "gdb"), expect(unused_mut))]
399    let mut debugger_rpc = None;
400    #[cfg(feature = "gdb")]
401    if opt.gdbstub {
402        let listener = VmListener::bind(VmAddress::vsock_any(opt.gdbstub_port))
403            .context("failed to bind socket")?;
404
405        let gdb_host = launch_mesh_host(mesh, "gdb", Some(tracing.tracer()))
406            .await
407            .context("failed to spawn gdb host process")?;
408
409        // Get the VP count of this machine. It's too early to read it directly
410        // from IGVM parameters, but the kernel already has the IGVM parsed VP
411        // count via the boot loader anyways.
412        let vp_count =
413            pal::unix::affinity::max_present_cpu().context("failed to get max present cpu")? + 1;
414
415        let (send, recv) = mesh::channel();
416        debugger_rpc = Some(recv);
417        gdbstub_worker = Some(
418            gdb_host
419                .launch_worker(
420                    debug_worker_defs::DEBUGGER_VSOCK_WORKER,
421                    debug_worker_defs::DebuggerParameters {
422                        listener,
423                        req_chan: send,
424                        vp_count,
425                        target_arch: if cfg!(guest_arch = "x86_64") {
426                            debug_worker_defs::TargetArch::X86_64
427                        } else {
428                            debug_worker_defs::TargetArch::Aarch64
429                        },
430                    },
431                )
432                .await?,
433        );
434    }
435    let (vm_rpc, vm_rpc_rx) = mesh::channel();
436
437    // Spawn the worker in a separate process in case the diagnostics server (in
438    // this process) is used to run gdbserver against it, or in case it needs to
439    // be restarted.
440    let host = launch_mesh_host(mesh, "vm", Some(tracing.tracer()))
441        .await
442        .context("failed to launch worker process")?;
443
444    let vm_worker = host
445        .start_worker(
446            worker::UNDERHILL_WORKER,
447            UnderhillWorkerParameters {
448                env_cfg,
449                remote_console_cfg,
450                debugger_rpc,
451                vm_rpc: vm_rpc_rx,
452                control_send,
453            },
454        )
455        .context("failed to launch worker")?;
456
457    Ok(Workers {
458        vm: vm_worker,
459        vm_rpc,
460        vnc: vnc_worker,
461        #[cfg(feature = "gdb")]
462        gdb: gdbstub_worker,
463    })
464}
465
466/// State for inspect only.
467#[derive(Inspect)]
468enum ControlState {
469    WaitingForStart,
470    Starting,
471    Started,
472    Restarting,
473}
474
475#[derive(MeshPayload)]
476pub enum ControlRequest {
477    FlushLogs(Rpc<CancelContext, Result<(), CancelReason>>),
478    MakeWorker(Rpc<String, Result<WorkerHost, RemoteError>>),
479}
480
481async fn run_control(
482    driver: DefaultDriver,
483    mesh: &Mesh,
484    opt: Options,
485    mut tracing: &mut TracingBackend,
486) -> anyhow::Result<()> {
487    let (control_send, mut control_recv) = mesh::channel();
488    let mut control_send = Some(control_send);
489
490    if opt.signal_vtl0_started {
491        signal_vtl0_started(&driver)
492            .await
493            .context("failed to signal vtl0 started")?;
494    }
495
496    let mut diag = DiagState::new().await?;
497
498    let (diag_reinspect_send, mut diag_reinspect_recv) = mesh::channel();
499    #[cfg(feature = "profiler")]
500    let mut profiler_host = None;
501    let mut state;
502    let mut workers = if opt.wait_for_start {
503        state = ControlState::WaitingForStart;
504        None
505    } else {
506        state = ControlState::Starting;
507        let workers = launch_workers(mesh, tracing, control_send.take().unwrap(), opt)
508            .await
509            .context("failed to launch workers")?;
510        Some(workers)
511    };
512
513    enum Event {
514        Diag(diag_server::DiagRequest),
515        Worker(WorkerEvent),
516        Control(ControlRequest),
517    }
518
519    let mut restart_rpc = None;
520    #[cfg(feature = "mem-profile-tracing")]
521    let mut profiler = mem_profile_tracing::HeapProfiler::new();
522    loop {
523        let event = {
524            let mut stream = (
525                (&mut diag.request_recv).map(Event::Diag),
526                (&mut diag_reinspect_recv)
527                    .map(|req| Event::Diag(diag_server::DiagRequest::Inspect(req))),
528                (&mut control_recv).map(Event::Control),
529                futures::stream::select_all(workers.as_mut().map(|w| &mut w.vm)).map(Event::Worker),
530            )
531                .merge();
532
533            let Some(event) = stream.next().await else {
534                break;
535            };
536            event
537        };
538
539        match event {
540            Event::Diag(request) => {
541                match request {
542                    diag_server::DiagRequest::Start(rpc) => {
543                        rpc.handle_failable(async |params| {
544                            if workers.is_some() {
545                                Err(anyhow::anyhow!("workers have already been started"))?;
546                            }
547                            let new_opt = Options::parse(params.args, params.env)
548                                .context("failed to parse new options")?;
549
550                            workers = Some(
551                                launch_workers(
552                                    mesh,
553                                    tracing,
554                                    control_send.take().unwrap(),
555                                    new_opt,
556                                )
557                                .await?,
558                            );
559                            state = ControlState::Starting;
560                            anyhow::Ok(())
561                        })
562                        .await
563                    }
564                    diag_server::DiagRequest::Inspect(deferred) => deferred.respond(|resp| {
565                        resp.sensitivity_field("mesh", SensitivityLevel::Safe, mesh)
566                            .sensitivity_field_mut("trace", SensitivityLevel::Safe, &mut tracing)
567                            .sensitivity_field(
568                                "build_info",
569                                SensitivityLevel::Safe,
570                                build_info::get(),
571                            )
572                            .sensitivity_child(
573                                "proc",
574                                SensitivityLevel::Safe,
575                                inspect_proc::inspect_proc,
576                            )
577                            .sensitivity_field("control_state", SensitivityLevel::Safe, &state)
578                            // This node can not be renamed due to stability guarantees.
579                            // See the comment at the top of inspect_internal for more details.
580                            .sensitivity_child("uhdiag", SensitivityLevel::Safe, |req| {
581                                inspect_internal::inspect_internal_diagnostics(
582                                    req,
583                                    &diag_reinspect_send,
584                                    &driver,
585                                )
586                            });
587
588                        resp.merge(&workers);
589                    }),
590                    diag_server::DiagRequest::Crash(pid) => {
591                        mesh.crash(pid);
592                    }
593                    diag_server::DiagRequest::Restart(rpc) => {
594                        let Some(workers) = &mut workers else {
595                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
596                                "worker has not been started yet"
597                            ))));
598                            continue;
599                        };
600
601                        let r = async {
602                            if restart_rpc.is_some() {
603                                anyhow::bail!("previous restart still in progress");
604                            }
605
606                            let host = launch_mesh_host(mesh, "vm", Some(tracing.tracer()))
607                                .await
608                                .context("failed to launch worker process")?;
609
610                            workers.vm.restart(&host);
611                            Ok(())
612                        }
613                        .await;
614
615                        if r.is_err() {
616                            rpc.complete(r.map_err(RemoteError::new));
617                        } else {
618                            state = ControlState::Restarting;
619                            restart_rpc = Some(rpc);
620                        }
621                    }
622                    diag_server::DiagRequest::Pause(rpc) => {
623                        let Some(workers) = &mut workers else {
624                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
625                                "worker has not been started yet"
626                            ))));
627                            continue;
628                        };
629
630                        // create the req future output the spawn, so that
631                        // we don't need to clone + move vm_rpc.
632                        let req = workers.vm_rpc.call(UhVmRpc::Pause, ());
633
634                        // FUTURE: consider supporting cancellation
635                        driver
636                            .spawn("diag-pause", async move {
637                                let was_paused = req.await.expect("failed to pause VM");
638                                rpc.handle_failable_sync(|_| {
639                                    if !was_paused {
640                                        Err(anyhow::anyhow!("VM is already paused"))
641                                    } else {
642                                        Ok(())
643                                    }
644                                });
645                            })
646                            .detach();
647                    }
648                    diag_server::DiagRequest::PacketCapture(rpc) => {
649                        let Some(workers) = &mut workers else {
650                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
651                                "worker has not been started yet"
652                            ))));
653                            continue;
654                        };
655
656                        workers.vm_rpc.send(UhVmRpc::PacketCapture(rpc));
657                    }
658                    #[cfg(feature = "mem-profile-tracing")]
659                    diag_server::DiagRequest::MemoryProfileTrace(rpc) => {
660                        rpc.handle_failable(async |pid| {
661                            if pid == std::process::id() as i32 {
662                                anyhow::Ok(profiler.capture_and_restart())
663                            } else {
664                                let Some(workers) = &mut workers else {
665                                    anyhow::bail!("workers have not been started yet");
666                                };
667
668                                let result = workers
669                                    .vm_rpc
670                                    .call(UhVmRpc::MemoryProfileTrace, pid)
671                                    .await
672                                    .context("failed to get memory profile from worker process")?;
673                                Ok(result?)
674                            }
675                        })
676                        .await
677                    }
678                    diag_server::DiagRequest::Resume(rpc) => {
679                        let Some(workers) = &mut workers else {
680                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
681                                "worker has not been started yet"
682                            ))));
683                            continue;
684                        };
685
686                        let was_resumed = workers
687                            .vm_rpc
688                            .call(UhVmRpc::Resume, ())
689                            .await
690                            .context("failed to resumed VM")?;
691
692                        let was_halted = workers
693                            .vm_rpc
694                            .call(UhVmRpc::ClearHalt, ())
695                            .await
696                            .context("failed to clear halt from VPs")?;
697
698                        rpc.handle_sync(|_| {
699                            if was_resumed || was_halted {
700                                Ok(())
701                            } else {
702                                Err(RemoteError::new(anyhow::anyhow!("VM is currently running")))
703                            }
704                        });
705                    }
706                    diag_server::DiagRequest::Save(rpc) => {
707                        let Some(workers) = &mut workers else {
708                            rpc.complete(Err(RemoteError::new(anyhow::anyhow!(
709                                "worker has not been started yet"
710                            ))));
711                            continue;
712                        };
713
714                        workers.vm_rpc.send(UhVmRpc::Save(rpc));
715                    }
716                    #[cfg(feature = "profiler")]
717                    diag_server::DiagRequest::Profile(rpc) => {
718                        let (rpc_params, rpc_sender) = rpc.split();
719                        // Create profiler host if there is none created before
720                        if profiler_host.is_none() {
721                            match launch_mesh_host(mesh, "profiler", Some(tracing.tracer()))
722                                .await
723                                .context("failed to launch profiler host")
724                            {
725                                Ok(host) => {
726                                    profiler_host = Some(host);
727                                }
728                                Err(e) => {
729                                    rpc_sender.complete(Err(RemoteError::new(e)));
730                                    continue;
731                                }
732                            }
733                        }
734
735                        let profiling_duration = rpc_params.duration;
736                        let host = profiler_host.as_ref().unwrap();
737                        let mut profiler_worker;
738                        match host
739                            .launch_worker(
740                                profiler_worker::PROFILER_WORKER,
741                                ProfilerWorkerParameters {
742                                    profiler_request: rpc_params,
743                                },
744                            )
745                            .await
746                        {
747                            Ok(worker) => {
748                                profiler_worker = worker;
749                            }
750                            Err(e) => {
751                                rpc_sender.complete(Err(RemoteError::new(e)));
752                                continue;
753                            }
754                        }
755
756                        driver
757                            .spawn("profiler_worker", async move {
758                                let result = CancelContext::new()
759                                    .with_timeout(Duration::from_secs(profiling_duration + 30))
760                                    .until_cancelled(profiler_worker.join())
761                                    .await
762                                    .context("profiler worker cancelled")
763                                    .and_then(|result| result.context("profiler worker failed"))
764                                    .map_err(RemoteError::new);
765
766                                rpc_sender.complete(result);
767                            })
768                            .detach();
769                    }
770                }
771            }
772            Event::Worker(event) => match event {
773                WorkerEvent::Started => {
774                    if let Some(response) = restart_rpc.take() {
775                        tracing::info!(CVM_ALLOWED, "restart complete");
776                        response.complete(Ok(()));
777                    } else {
778                        tracing::info!(CVM_ALLOWED, "vm worker started");
779                    }
780                    state = ControlState::Started;
781                }
782                WorkerEvent::Stopped => {
783                    anyhow::bail!("worker unexpectedly stopped");
784                }
785                WorkerEvent::Failed(err) => {
786                    return Err(anyhow::Error::from(err)).context("vm worker failed");
787                }
788                WorkerEvent::RestartFailed(err) => {
789                    tracing::error!(
790                        CVM_ALLOWED,
791                        error = &err as &dyn std::error::Error,
792                        "restart failed"
793                    );
794                    restart_rpc.take().unwrap().complete(Err(err));
795                    state = ControlState::Started;
796                }
797            },
798            Event::Control(req) => match req {
799                ControlRequest::FlushLogs(rpc) => {
800                    rpc.handle(async |mut ctx| {
801                        tracing::info!(CVM_ALLOWED, "flushing logs");
802                        ctx.until_cancelled(tracing.flush()).await?;
803                        Ok(())
804                    })
805                    .await
806                }
807                ControlRequest::MakeWorker(rpc) => {
808                    rpc.handle_failable(async |name| {
809                        launch_mesh_host(mesh, &name, Some(tracing.tracer())).await
810                    })
811                    .await
812                }
813            },
814        }
815    }
816
817    Ok(())
818}
819
820async fn signal_vtl0_started(driver: &DefaultDriver) -> anyhow::Result<()> {
821    tracing::info!(CVM_ALLOWED, "signaling vtl0 started early");
822    let (client, task) = guest_emulation_transport::spawn_get_worker(driver.clone())
823        .await
824        .context("failed to spawn GET")?;
825    client.complete_start_vtl0(None).await;
826    // Disconnect the GET so that it can be reused.
827    drop(client);
828    task.await.unwrap();
829    tracing::info!(CVM_ALLOWED, "signaled vtl0 start");
830    Ok(())
831}
832
833// The "base" workers for Underhill. Other workers are defined in the
834// `underhill_resources` crate.
835//
836// FUTURE: split these workers into separate crates and move them to
837// `underhill_resources`, too.
838register_workers! {
839    UnderhillVmWorker,
840    DiagWorker,
841    #[cfg(feature = "profiler")]
842    ProfilerWorker,
843}