1mod bindings {
2 wasmtime::component::bindgen!({
3 world: "subcommands",
4 async: true,
5 });
6}
7
8pub use bindings::exports::wasmcloud::wash::subcommand::Metadata;
9use bindings::Subcommands;
10
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14use anyhow::{Context, Ok};
15use wasmtime::component::{Component, Linker};
16use wasmtime::{Config, Engine};
17use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder};
18use wasmtime_wasi_http::WasiHttpCtx;
19
20use super::Data;
21
22const DIRECTORY_ALLOW: DirPerms = DirPerms::all();
23const DIRECTORY_DENY: DirPerms = DirPerms::READ;
24
25struct InstanceData {
26 instance: Subcommands,
27 metadata: Metadata,
28 loaded_path: PathBuf,
29 store: wasmtime::Store<Data>,
30}
31
32pub struct SubcommandRunner {
34 engine: Engine,
35 plugins: HashMap<String, InstanceData>,
36}
37
38pub struct DirMapping {
40 pub host_path: PathBuf,
44 pub component_path: Option<String>,
46}
47
48impl SubcommandRunner {
49 pub fn new() -> anyhow::Result<Self> {
51 let mut config = Config::new();
52 if let Err(e) = config.cache_config_load_default() {
54 tracing::warn!(err = ?e, "Failed to load wasm cache");
55 }
56 config.wasm_component_model(true);
57 config.async_support(true);
58 let engine = Engine::new(&config)?;
59 Ok(Self {
60 engine,
61 plugins: HashMap::new(),
62 })
63 }
64
65 pub async fn new_with_plugins(
70 plugins: impl IntoIterator<Item = impl AsRef<Path>>,
71 ) -> anyhow::Result<Self> {
72 let mut runner = Self::new()?;
73 for plugin in plugins {
74 runner.add_plugin(plugin).await?;
75 }
76 Ok(runner)
77 }
78
79 pub async fn add_plugin(&mut self, path: impl AsRef<Path>) -> anyhow::Result<Metadata> {
84 self.add_plugin_internal(path, false).await
85 }
86
87 pub async fn update_plugin(&mut self, path: impl AsRef<Path>) -> anyhow::Result<Metadata> {
91 self.add_plugin_internal(path, true).await
92 }
93
94 async fn add_plugin_internal(
95 &mut self,
96 path: impl AsRef<Path>,
97 update: bool,
98 ) -> anyhow::Result<Metadata> {
99 let ctx = WasiCtxBuilder::new().build();
101
102 let ctx = Data {
103 table: wasmtime::component::ResourceTable::default(),
104 ctx,
105 http: WasiHttpCtx::new(),
106 };
107
108 let mut store = wasmtime::Store::new(&self.engine, ctx);
109
110 let component = Component::from_file(&self.engine, &path)?;
111 let mut linker = Linker::new(&self.engine);
112 wasmtime_wasi::add_to_linker_async(&mut linker)
113 .context("failed to link core WASI interfaces")?;
114 wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)
115 .context("failed to link `wasi:http`")?;
116
117 let instance = Subcommands::instantiate_async(&mut store, &component, &linker).await?;
118 let metadata = instance
119 .wasmcloud_wash_subcommand()
120 .call_register(&mut store)
121 .await?;
122 let maybe_existing = self.plugins.insert(
123 metadata.id.clone(),
124 InstanceData {
125 instance,
126 metadata: metadata.clone(),
127 loaded_path: path.as_ref().to_owned(),
128 store,
129 },
130 );
131
132 match (update, maybe_existing) {
133 (true, _) | (false, None) => Ok(metadata),
135 (false, Some(plugin)) => {
137 let id = plugin.metadata.id.clone();
139 self.plugins.insert(plugin.metadata.id.clone(), plugin);
140 Err(anyhow::anyhow!("Plugin with id {id} already exists"))
141 }
142 }
143 }
144
145 pub fn metadata(&self, id: &str) -> Option<&Metadata> {
147 self.plugins.get(id).map(|p| &p.metadata)
148 }
149
150 pub fn all_metadata(&self) -> Vec<&Metadata> {
152 self.plugins.values().map(|data| &data.metadata).collect()
153 }
154
155 pub fn path(&self, id: &str) -> Option<&Path> {
157 self.plugins.get(id).map(|p| p.loaded_path.as_path())
158 }
159
160 pub async fn run(
169 &mut self,
170 plugin_id: &str,
171 plugin_dir: PathBuf,
172 dirs: Vec<DirMapping>,
173 mut args: Vec<String>,
174 ) -> anyhow::Result<()> {
175 let plugin = self
176 .plugins
177 .get_mut(plugin_id)
178 .ok_or_else(|| anyhow::anyhow!("Plugin with id {plugin_id} does not exist"))?;
179
180 let env_prefix = format!("WASH_PLUGIN_{}_", plugin_id.to_uppercase());
181 let vars: Vec<_> = std::env::vars()
182 .filter(|(k, _)| k.starts_with(&env_prefix))
183 .collect();
184 let mut ctx = WasiCtxBuilder::new();
185 for dir in dirs {
186 let canonicalized = tokio::fs::canonicalize(&dir.host_path)
188 .await
189 .context("Error when canonicalizing given path")?;
190 let str_canonical = canonicalized.to_str().ok_or_else(|| anyhow::anyhow!("Canonicalized path cannot be converted to a string for use in a plugin. This is a limitation of the WASI API"))?.to_string();
192 let is_dir = tokio::fs::metadata(&canonicalized)
194 .await
195 .map(|m| m.is_dir())
196 .context("Error when checking if path is a file or a dir")?;
197 let (host_path, guest_path, dir_perms) = match (is_dir, dir.component_path) {
198 (true, Some(path)) => (canonicalized.clone(), path, DIRECTORY_ALLOW),
199 (false, Some(path)) => (
200 canonicalized
201 .parent()
202 .ok_or_else(|| anyhow::anyhow!("Could not get parent of given file"))?
203 .to_path_buf(),
204 path,
205 DIRECTORY_DENY,
206 ),
207 (true, None) => (
208 canonicalized.clone(),
209 str_canonical.clone(),
210 DIRECTORY_ALLOW,
211 ),
212 (false, None) => {
213 let parent = canonicalized
214 .parent()
215 .ok_or_else(|| anyhow::anyhow!("Could not get parent of given file"))?
216 .to_path_buf();
217 (
218 parent.clone(),
219 parent.to_str().unwrap().to_string(),
222 DIRECTORY_DENY,
223 )
224 }
225 };
226
227 #[cfg(target_family = "windows")]
230 let guest_path = guest_path.replace('\\', "/");
231 #[cfg(target_family = "windows")]
232 let str_canonical = str_canonical.replace('\\', "/");
233 ctx.preopened_dir(host_path, guest_path, dir_perms, FilePerms::all())
234 .context("Error when preopening path argument")?;
235 let matching = args
237 .iter_mut()
238 .find(|arg| {
239 <&mut std::string::String as std::convert::AsRef<Path>>::as_ref(arg)
240 == dir.host_path
241 })
242 .ok_or_else(|| {
243 anyhow::anyhow!(
244 "Could not find host path {} in args for replacement",
245 dir.host_path.display()
246 )
247 })?;
248 *matching = str_canonical;
249 }
250 ctx.socket_addr_check(|_, _| Box::pin(async { false }))
252 .inherit_stdio()
253 .preopened_dir(plugin_dir, "/", DIRECTORY_ALLOW, FilePerms::all())
254 .context("Error when preopening plugin dir")?
255 .args(&args)
256 .envs(&vars);
257
258 plugin.store.data_mut().ctx = ctx.build();
259 plugin
260 .instance
261 .wasi_cli_run()
262 .call_run(&mut plugin.store)
263 .await
264 .context("Error when running wasm component")?
265 .map_err(|_| anyhow::anyhow!("Error when running subcommand"))
266 }
267}