Skip to main content

wash_runtime/host/
mod.rs

1//! Host runtime for managing WebAssembly workloads and plugins.
2//!
3//! The host module provides the runtime environment for executing WebAssembly
4//! workloads. It manages the lifecycle of components, coordinates with plugins
5//! to provide capabilities, and handles system resources.
6//!
7//! # Key Components
8//!
9//! - [`Host`] - The main runtime that manages workloads and plugins
10//! - [`HostBuilder`] - Builder for configuring host settings
11//! - [`HostApi`] - Trait defining the host's external API
12//! - [`HostWorkload`] - Internal representation of workload states
13//!
14//! # Architecture
15//!
16//! The host acts as the central coordinator between:
17//! - WebAssembly components that need execution
18//! - Plugins that provide WASI and other capabilities
19//! - System resources like networking and storage
20//! - External consumers through the HostApi
21//!
22//! # Example
23//!
24//! ```no_run
25//! use wash_runtime::host::{HostBuilder, HostApi};
26//! use wash_runtime::engine::Engine;
27//! use std::sync::Arc;
28//!
29//! # async fn example() -> anyhow::Result<()> {
30//! let engine = Engine::builder().build()?;
31//! let host = HostBuilder::new()
32//!     .with_engine(engine)
33//!     .with_friendly_name("my-host")
34//!     .build()?;
35//!
36//! let host = host.start().await?;
37//! let heartbeat = host.heartbeat().await?;
38//! println!("Host {} is running", heartbeat.friendly_name);
39//! # Ok(())
40//! # }
41//! ```
42
43use std::collections::{HashMap, HashSet};
44use std::future::Future;
45use std::sync::Arc;
46
47use anyhow::{Context, bail};
48use names::{Generator, Name};
49use tokio::sync::RwLock;
50use tracing::{debug, trace, warn};
51
52use crate::engine::Engine;
53use crate::engine::workload::ResolvedWorkload;
54use crate::plugin::HostPlugin;
55use crate::types::*;
56use crate::wit::WitWorld;
57
58mod sysinfo;
59use sysinfo::SystemMonitor;
60
61/// The API for interacting with a wasmcloud host.
62///
63/// This trait defines the core operations for managing workloads on a host,
64/// including starting, stopping, and querying workload status, as well as
65/// retrieving host health information.
66pub trait HostApi {
67    /// Request a heartbeat containing the host's current state and system information.
68    ///
69    /// # Returns
70    /// A `HostHeartbeat` containing system metrics, version info, and capability information.
71    ///
72    /// # Errors
73    /// Returns an error if system information cannot be retrieved.
74    fn heartbeat(&self) -> impl Future<Output = anyhow::Result<HostHeartbeat>>;
75    /// Start a new workload on this host.
76    ///
77    /// # Arguments
78    /// * `request` - Contains the workload configuration to start
79    ///
80    /// # Returns
81    /// A `WorkloadStartResponse` with the status of the started workload.
82    ///
83    /// # Errors
84    /// Returns an error if the workload fails to start or validate.
85    fn workload_start(
86        &self,
87        request: WorkloadStartRequest,
88    ) -> impl Future<Output = anyhow::Result<WorkloadStartResponse>>;
89    /// Query the status of a running workload.
90    ///
91    /// # Arguments
92    /// * `request` - Contains the workload ID to query
93    ///
94    /// # Returns
95    /// A `WorkloadStatusResponse` with the current state of the workload.
96    ///
97    /// # Errors
98    /// Returns an error if the workload is not found.
99    fn workload_status(
100        &self,
101        request: WorkloadStatusRequest,
102    ) -> impl Future<Output = anyhow::Result<WorkloadStatusResponse>>;
103    /// Stop a running workload on this host.
104    ///
105    /// # Arguments
106    /// * `request` - Contains the workload ID to stop
107    ///
108    /// # Returns
109    /// A `WorkloadStopResponse` with the final status of the stopped workload.
110    ///
111    /// # Errors
112    /// Returns an error if the workload cannot be stopped or is not found.
113    fn workload_stop(
114        &self,
115        request: WorkloadStopRequest,
116    ) -> impl Future<Output = anyhow::Result<WorkloadStopResponse>>;
117}
118
119// Helper trait impl that helps with Arc-ing the Host
120impl<T: HostApi> HostApi for Arc<T> {
121    async fn heartbeat(&self) -> anyhow::Result<HostHeartbeat> {
122        self.as_ref().heartbeat().await
123    }
124    async fn workload_start(
125        &self,
126        request: WorkloadStartRequest,
127    ) -> anyhow::Result<WorkloadStartResponse> {
128        self.as_ref().workload_start(request).await
129    }
130    async fn workload_stop(
131        &self,
132        request: WorkloadStopRequest,
133    ) -> anyhow::Result<WorkloadStopResponse> {
134        self.as_ref().workload_stop(request).await
135    }
136    async fn workload_status(
137        &self,
138        request: WorkloadStatusRequest,
139    ) -> anyhow::Result<WorkloadStatusResponse> {
140        self.as_ref().workload_status(request).await
141    }
142}
143
144/// Internal representation of a workload's state within the host.
145///
146/// This enum tracks the lifecycle stages of a workload from starting
147/// through running to stopping or error states.
148#[derive(Debug, Clone)]
149pub enum HostWorkload {
150    Starting,
151    // Boxed to reduce size of the enum
152    Running(Box<ResolvedWorkload>),
153    Stopping,
154    Error,
155}
156
157impl From<&HostWorkload> for WorkloadState {
158    fn from(hw: &HostWorkload) -> Self {
159        match hw {
160            HostWorkload::Starting => WorkloadState::Starting,
161            HostWorkload::Running(_) => WorkloadState::Running,
162            HostWorkload::Stopping => WorkloadState::Stopping,
163            HostWorkload::Error => WorkloadState::Error,
164        }
165    }
166}
167
168/// A wasmcloud host that manages WebAssembly workloads and plugins.
169///
170/// The `Host` is the primary runtime for executing workloads. It manages:
171/// - An engine for compiling and running WebAssembly components
172/// - A collection of workloads and their states
173/// - Plugins that extend host functionality
174/// - System monitoring and resource tracking
175pub struct Host {
176    engine: Engine,
177    /// Workloads mapped from ID to the workload and its current state
178    workloads: Arc<RwLock<HashMap<String, HostWorkload>>>,
179    /// Plugins in a map from their ID to the plugin itself
180    plugins: HashMap<&'static str, Arc<dyn HostPlugin>>,
181    /// Host metadata
182    id: String,
183    hostname: String,
184    friendly_name: String,
185    version: String,
186    labels: HashMap<String, String>,
187    started_at: chrono::DateTime<chrono::Utc>,
188    /// System monitor for tracking CPU/memory usage
189    system_monitor: Arc<RwLock<SystemMonitor>>,
190    // endpoints: HashMap<String, EndpointConfiguration>
191}
192
193impl Host {
194    /// Create a new builder for the host.
195    pub fn builder() -> HostBuilder {
196        HostBuilder::default()
197    }
198
199    /// Start the host and initialize all plugins.
200    ///
201    /// This method must be called before the host can accept workloads.
202    /// It starts all registered plugins and prepares the host for operation.
203    ///
204    /// # Returns
205    /// An `Arc` wrapped host ready to accept workloads.
206    ///
207    /// # Errors
208    /// Returns an error if any plugin fails to start.
209    pub async fn start(self) -> anyhow::Result<Arc<Self>> {
210        // Start all plugins, any errors means the host fails to start.
211        for (id, plugin) in &self.plugins {
212            if let Err(e) = plugin.start().await {
213                tracing::error!(id = id, err = ?e, "failed to start plugin");
214                bail!(e)
215            }
216        }
217
218        Ok(Arc::new(self))
219    }
220
221    /// Stop the host and shut down all plugins.
222    ///
223    /// Attempts to gracefully stop all plugins with a 3-second timeout
224    /// for each. Errors are logged but don't prevent other plugins from
225    /// being stopped.
226    ///
227    /// # Returns
228    /// Ok if the shutdown process completes (even with plugin errors).
229    pub async fn stop(self: Arc<Self>) -> anyhow::Result<()> {
230        // Stop all plugins, log errors but continue stopping others
231        for (id, plugin) in &self.plugins {
232            let stop_fut = plugin.stop();
233            match tokio::time::timeout(std::time::Duration::from_secs(3), stop_fut).await {
234                Ok(Err(e)) => {
235                    tracing::error!(id = id, err = ?e, "failed to stop plugin");
236                }
237                Err(_) => {
238                    tracing::error!(id = id, "plugin stop timed out after 3 seconds");
239                }
240                _ => {}
241            }
242        }
243
244        Ok(())
245    }
246
247    /// Get a label value by key.
248    ///
249    /// # Arguments
250    /// * `label` - The label key to look up
251    ///
252    /// # Returns
253    /// The label value if it exists, None otherwise.
254    pub fn label(&self, label: impl AsRef<str>) -> Option<&String> {
255        self.labels.get(label.as_ref())
256    }
257
258    /// Get the unique identifier for this host.
259    ///
260    /// # Returns
261    /// The host's unique ID string.
262    pub fn id(&self) -> &str {
263        &self.id
264    }
265
266    /// Get the human-readable name for this host.
267    ///
268    /// # Returns
269    /// The host's friendly name string.
270    pub fn friendly_name(&self) -> &str {
271        &self.friendly_name
272    }
273
274    /// Helper function to generate a unique ID for a workload
275    fn generate_workload_id(&self) -> String {
276        uuid::Uuid::new_v4().to_string()
277    }
278
279    /// Returns the WIT (imports, exports) that this host can provide to any component.
280    ///
281    /// Put another way, this represents a simplified version of the host world. For
282    /// example, this WIT world:
283    /// ```wit
284    /// package wasmcloud:host@0.1.0;
285    ///
286    /// interface foo {
287    /// ...
288    /// }
289    /// interface bar {
290    /// ...
291    /// }
292    ///
293    /// world host {
294    ///   import foo;
295    ///   export bar;
296    /// }
297    /// ```
298    ///
299    /// Would be returned as:
300    /// (
301    ///  vec![WitInterface { namespace: "wasmcloud", package: "host", interfaces: ["foo"], version: Some("0.1.0") }],
302    ///  vec![WitInterface { namespace: "wasmcloud", package: "host", interfaces: ["bar"], version: Some("0.1.0") }],
303    /// )
304    ///
305    /// This can be viewed as an inversion of the worlds that this host can support. In the above example,
306    /// this host can support any component that imports `bar` and exports `foo`. Other exports will be ignored,
307    /// and other imports that are unsatisfied will be rejected.
308    pub fn wit_world(&self) -> WitWorld {
309        let mut imports = HashSet::new();
310        // The host provides wasi@0.2 interfaces other than wasi:http
311        // <https://docs.rs/wasmtime-wasi/36.0.2/wasmtime_wasi/p2/index.html#wasip2-interfaces>
312        let mut exports = HashSet::from([
313            "wasi:io/poll,error,streams@0.2.0".into(),
314            "wasi:clocks/monotonic-clock,wall-time@0.2.0".into(),
315            "wasi:random/random@0.2.0".into(),
316            "wasi:cli/environment,exit,stderr,stdin,stdout,terminal-input,terminal-output,terminal-stderr,terminal-stdin,terminal-stdout@0.2.0".into(),
317            "wasi:clocks/monotonic-clock,wall-clock@0.2.0".into(),
318            "wasi:filesystem/preopens,types@0.2.0".into(),
319            "wasi:random/insecure-seed,insecure,random@0.2.0".into(),
320            "wasi:sockets/instance-network,ip-name-lookup,network,tcp-create-socket,tcp,udp-create-socket,udp@0.2.0".into(),
321        ]);
322
323        // Include imports and exports that plugins specify
324        imports.extend(
325            self.plugins
326                .values()
327                .flat_map(|p| p.world().imports.into_iter().collect::<Vec<_>>()),
328        );
329        exports.extend(
330            self.plugins
331                .values()
332                .flat_map(|p| p.world().exports.into_iter().collect::<Vec<_>>()),
333        );
334
335        WitWorld { imports, exports }
336    }
337
338    /// Returns a three-tuple of (OS architecture, OS name, OS kernel)
339    async fn get_system_info(&self) -> (String, String, String) {
340        // Get OS information
341        let os_name = std::env::consts::OS.to_string();
342        let os_arch = std::env::consts::ARCH.to_string();
343        let os_kernel = std::env::consts::FAMILY.to_string();
344        (os_arch, os_name, os_kernel)
345    }
346
347    /// Returns a tuple of (total memory, free memory)
348    async fn get_memory_info(&self) -> anyhow::Result<(u64, u64)> {
349        let monitor = self.system_monitor.read().await;
350        let mem = monitor.memory_usage();
351        Ok((mem.total_memory, mem.free_memory))
352    }
353
354    /// Returns the current global CPU usage as a percentage
355    async fn get_cpu_usage(&self) -> anyhow::Result<f32> {
356        let monitor = self.system_monitor.read().await;
357        Ok(monitor.cpu_usage().global_usage)
358    }
359}
360
361impl HostApi for Host {
362    async fn heartbeat(&self) -> anyhow::Result<HostHeartbeat> {
363        // Refresh system info before reporting
364        {
365            let mut monitor = self.system_monitor.write().await;
366            monitor.refresh();
367            monitor.report_usage();
368        }
369
370        let (os_arch, os_name, os_kernel) = self.get_system_info().await;
371        let (system_memory_total, system_memory_free) = self
372            .get_memory_info()
373            .await
374            .context("failed to get memory info")?;
375        let system_cpu_usage = self
376            .get_cpu_usage()
377            .await
378            .context("failed to get CPU usage")?;
379
380        // Count components and providers from workloads
381        let (workload_count, component_count) = {
382            let workloads = self.workloads.read().await;
383            let workload_count: u64 = workloads.len() as u64;
384            let mut component_count: u64 = 0;
385            for workload in workloads.values() {
386                if let HostWorkload::Running(workload) = workload {
387                    component_count += workload.component_count().await as u64;
388                }
389            }
390            (workload_count, component_count)
391        };
392
393        // Collect all imports and exports from the host and plugins
394        let mut imports = Vec::new();
395        let mut exports = Vec::new();
396
397        for plugin in self.plugins.values() {
398            let world = plugin.world();
399            imports.extend(world.imports.into_iter());
400            exports.extend(world.exports.into_iter());
401        }
402
403        Ok(HostHeartbeat {
404            id: self.id.clone(),
405            hostname: self.hostname.clone(),
406            friendly_name: self.friendly_name.clone(),
407            version: self.version.clone(),
408            labels: self.labels.clone(),
409            started_at: self.started_at,
410            os_arch,
411            os_name,
412            os_kernel,
413            system_cpu_usage,
414            system_memory_total,
415            system_memory_free,
416            component_count,
417            workload_count,
418            imports,
419            exports,
420        })
421    }
422
423    /// Start a workload
424    async fn workload_start(
425        &self,
426        request: WorkloadStartRequest,
427    ) -> anyhow::Result<WorkloadStartResponse> {
428        let workload_id = self.generate_workload_id();
429
430        // Store the workload with initial state
431        self.workloads
432            .write()
433            .await
434            .insert(workload_id.clone(), HostWorkload::Starting);
435
436        let service_present = request.workload.service.is_some();
437
438        // Initialize the workload using the engine, receiving the unresolved workload
439        let unresolved_workload = self
440            .engine
441            .initialize_workload(&workload_id, request.workload)?;
442
443        let mut resolved_workload = unresolved_workload.resolve(Some(&self.plugins)).await?;
444
445        // If the service didn't run and we had one, warn
446        if resolved_workload.execute_service().await? != service_present {
447            warn!(
448                workload_id = workload_id,
449                "service did not properly execute"
450            );
451        }
452
453        // Update the workload state to `Running`
454        self.workloads
455            .write()
456            .await
457            .entry(workload_id.clone())
458            .and_modify(|workload| {
459                *workload = HostWorkload::Running(Box::new(resolved_workload));
460            });
461
462        Ok(WorkloadStartResponse {
463            workload_status: WorkloadStatus {
464                workload_id,
465                workload_state: WorkloadState::Running,
466                message: "Workload started successfully".to_string(),
467            },
468        })
469    }
470
471    async fn workload_status(
472        &self,
473        request: WorkloadStatusRequest,
474    ) -> anyhow::Result<WorkloadStatusResponse> {
475        if let Some(workload) = self.workloads.read().await.get(&request.workload_id) {
476            let workload_state = workload.into();
477            Ok(WorkloadStatusResponse {
478                workload_status: WorkloadStatus {
479                    workload_id: request.workload_id,
480                    message: format!("Workload is {workload_state:?}"),
481                    workload_state,
482                },
483            })
484        } else {
485            anyhow::bail!("Workload not found: {}", request.workload_id)
486        }
487    }
488
489    async fn workload_stop(
490        &self,
491        request: WorkloadStopRequest,
492    ) -> anyhow::Result<WorkloadStopResponse> {
493        let has_workload = self
494            .workloads
495            .read()
496            .await
497            .contains_key(&request.workload_id);
498
499        let (workload_state, message) = if has_workload {
500            // Update state to stopping
501            let resolved_workload = {
502                let mut workloads = self.workloads.write().await;
503                trace!(
504                    workload_id = request.workload_id,
505                    "updating workload state to stopping"
506                );
507                // Insert Stopping state, extract the running workload if it was running
508                workloads
509                    .insert(request.workload_id.clone(), HostWorkload::Stopping)
510                    .and_then(|hw| match hw {
511                        HostWorkload::Running(rw) => Some(*rw),
512                        _ => None,
513                    })
514            };
515
516            // Stop the workload:
517            // 1. Unbind from all plugins
518            // 2. Clean up resources (drop will handle wasmtime cleanup)
519            // 3. Remove from active workloads
520            if let Some(resolved_workload) = resolved_workload {
521                debug!(
522                    workload_id = request.workload_id,
523                    workload_name = resolved_workload.name(),
524                    "stopping workload"
525                );
526
527                // Stop the service if running
528                resolved_workload.stop_service();
529
530                // Unbind all plugins from the workload
531                if let Err(e) = resolved_workload.unbind_all_plugins().await {
532                    warn!(
533                        workload_id = request.workload_id,
534                        error = ?e,
535                        "error unbinding plugins during workload stop, continuing"
536                    );
537                }
538            }
539
540            // Remove the workload from the active workloads map
541            // This will drop the workload and clean up wasmtime resources
542            self.workloads.write().await.remove(&request.workload_id);
543
544            debug!(
545                workload_id = request.workload_id,
546                "workload stopped successfully"
547            );
548
549            (
550                WorkloadState::Stopping,
551                "Workload stopped successfully".to_string(),
552            )
553        } else {
554            (WorkloadState::Unspecified, "Workload not found".to_string())
555        };
556
557        Ok(WorkloadStopResponse {
558            workload_status: WorkloadStatus {
559                workload_id: request.workload_id,
560                workload_state,
561                message,
562            },
563        })
564    }
565}
566
567impl std::fmt::Debug for Host {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        f.debug_struct("Host")
570            .field("id", &self.id)
571            .field("hostname", &self.hostname)
572            .field("friendly_name", &self.friendly_name)
573            .field("version", &self.version)
574            .field("labels", &self.labels)
575            .field("started_at", &self.started_at)
576            .field("workloads", &self.workloads)
577            .finish()
578    }
579}
580
581/// Builder for the [`Host`]
582#[derive(Default)]
583pub struct HostBuilder {
584    engine: Option<Engine>,
585    plugins: HashMap<&'static str, Arc<dyn HostPlugin>>,
586    hostname: Option<String>,
587    friendly_name: Option<String>,
588    labels: HashMap<String, String>,
589}
590
591impl HostBuilder {
592    pub fn new() -> Self {
593        Self::default()
594    }
595
596    pub fn with_engine(mut self, engine: Engine) -> Self {
597        self.engine = Some(engine);
598        self
599    }
600
601    pub fn with_plugin<T: HostPlugin>(mut self, plugin: Arc<T>) -> anyhow::Result<Self> {
602        let plugin_id = plugin.id();
603
604        // Check for duplicate plugin IDs
605        if self.plugins.contains_key(plugin_id) {
606            bail!("Duplicate plugin ID '{plugin_id}' - plugin IDs must be unique");
607        }
608
609        self.plugins.insert(plugin_id, plugin);
610        Ok(self)
611    }
612
613    /// Sets the hostname for this host.
614    ///
615    /// # Arguments
616    /// * `hostname` - The hostname to use
617    ///
618    /// # Returns
619    /// The builder instance for method chaining.
620    pub fn with_hostname(mut self, hostname: impl AsRef<str>) -> Self {
621        self.hostname = Some(hostname.as_ref().to_string());
622        self
623    }
624
625    /// Sets a human-readable friendly name for this host.
626    ///
627    /// # Arguments
628    /// * `name` - The friendly name to use
629    ///
630    /// # Returns
631    /// The builder instance for method chaining.
632    pub fn with_friendly_name(mut self, name: impl AsRef<str>) -> Self {
633        self.friendly_name = Some(name.as_ref().to_string());
634        self
635    }
636
637    /// Adds a label to the host.
638    ///
639    /// Labels are key-value pairs that can be used to categorize
640    /// or identify the host.
641    ///
642    /// # Arguments
643    /// * `key` - The label key
644    /// * `value` - The label value
645    ///
646    /// # Returns
647    /// The builder instance for method chaining.
648    pub fn with_label(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
649        self.labels
650            .insert(key.as_ref().to_string(), value.as_ref().to_string());
651        self
652    }
653
654    /// Builds and returns a configured [`Host`].
655    ///
656    /// This method finalizes the configuration and creates the host.
657    /// If no engine is provided, a default engine is created.
658    /// If no hostname is provided, the system hostname is used.
659    /// If no friendly name is provided, a random name is generated.
660    ///
661    /// # Returns
662    /// A new `Host` instance ready to be started.
663    ///
664    /// # Errors
665    /// Returns an error if the default engine cannot be created (when no engine is provided).
666    pub fn build(self) -> anyhow::Result<Host> {
667        let engine = if let Some(engine) = self.engine {
668            engine
669        } else {
670            Engine::builder().build()?
671        };
672
673        // Get hostname from system if not provided
674        let hostname = self.hostname.unwrap_or_else(|| {
675            hostname::get()
676                .map(|h| h.to_string_lossy().to_string())
677                .unwrap_or_else(|_| "unknown".to_string())
678        });
679
680        // Generate a friendly name if not provided
681        let friendly_name = self.friendly_name.unwrap_or_else(|| {
682            let mut generator = Generator::with_naming(Name::Numbered);
683            generator
684                .next()
685                .unwrap_or_else(|| format!("host-{}", uuid::Uuid::new_v4()))
686        });
687
688        Ok(Host {
689            engine,
690            workloads: Arc::default(),
691            plugins: self.plugins,
692            id: uuid::Uuid::new_v4().to_string(),
693            hostname,
694            friendly_name,
695            version: env!("CARGO_PKG_VERSION").to_string(),
696            labels: self.labels,
697            started_at: chrono::Utc::now(),
698            system_monitor: Arc::new(RwLock::new(SystemMonitor::new())),
699        })
700    }
701}