use std::{
collections::HashMap,
future::Future,
ops::Deref,
path::Path,
pin::Pin,
sync::{Arc, RwLock},
};
use anyhow::Context;
use virtual_fs::{AsyncReadExt, FileSystem};
use wasmer::FunctionEnvMut;
use wasmer_package::utils::from_bytes;
use wasmer_wasix_types::wasi::Errno;
mod binary_package;
mod exec;
pub use self::{
binary_package::*,
exec::{
run_exec, spawn_exec, spawn_exec_module, spawn_exec_wasm, spawn_load_module,
spawn_load_wasm, spawn_union_fs,
},
};
use crate::{
os::{command::Commands, task::TaskJoinHandle},
Runtime, SpawnError, WasiEnv,
};
#[derive(Debug, Clone)]
pub struct BinFactory {
pub(crate) commands: Commands,
runtime: Arc<dyn Runtime + Send + Sync + 'static>,
pub(crate) local: Arc<RwLock<HashMap<String, Option<BinaryPackage>>>>,
}
impl BinFactory {
pub fn new(runtime: Arc<dyn Runtime + Send + Sync + 'static>) -> BinFactory {
BinFactory {
commands: Commands::new_with_builtins(runtime.clone()),
runtime,
local: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn runtime(&self) -> &(dyn Runtime + Send + Sync) {
self.runtime.deref()
}
pub fn set_binary(&self, name: &str, binary: BinaryPackage) {
let mut cache = self.local.write().unwrap();
cache.insert(name.to_string(), Some(binary));
}
#[allow(clippy::await_holding_lock)]
pub async fn get_binary(
&self,
name: &str,
fs: Option<&dyn FileSystem>,
) -> Option<BinaryPackage> {
self.get_executable(name, fs)
.await
.and_then(|executable| match executable {
Executable::Wasm(_) => None,
Executable::BinaryPackage(pkg) => Some(pkg),
})
}
pub fn spawn<'a>(
&'a self,
name: String,
store: wasmer::Store,
env: WasiEnv,
) -> Pin<Box<dyn Future<Output = Result<TaskJoinHandle, SpawnError>> + 'a>> {
Box::pin(async move {
let res = self
.get_executable(name.as_str(), Some(env.fs_root()))
.await
.ok_or_else(|| SpawnError::BinaryNotFound {
binary: name.clone(),
});
if res.is_err() {
env.on_exit(Some(Errno::Noent.into())).await;
}
let executable = res?;
match executable {
Executable::Wasm(bytes) => {
spawn_exec_wasm(&bytes, name.as_str(), env, &self.runtime).await
}
Executable::BinaryPackage(pkg) => {
let cmd = if let Some(cmd) = pkg.get_command(name.as_str()) {
cmd
} else if let Some(cmd) = pkg.get_entrypoint_command() {
cmd
} else {
tracing::error!(
command=name,
pkg=%pkg.id,
"Unable to spawn a command because its package has no entrypoint",
);
env.on_exit(Some(Errno::Noexec.into())).await;
return Err(SpawnError::MissingEntrypoint {
package_id: pkg.id.clone(),
});
};
env.prepare_spawn(cmd);
spawn_exec(pkg, name.as_str(), store, env, &self.runtime).await
}
}
})
}
pub fn try_built_in(
&self,
name: String,
parent_ctx: Option<&FunctionEnvMut<'_, WasiEnv>>,
store: &mut Option<wasmer::Store>,
builder: &mut Option<WasiEnv>,
) -> Result<TaskJoinHandle, SpawnError> {
if let Some(parent_ctx) = parent_ctx {
if self.commands.exists(name.as_str()) {
return self
.commands
.exec(parent_ctx, name.as_str(), store, builder);
}
} else if self.commands.exists(name.as_str()) {
tracing::warn!("builtin command without a parent ctx - {}", name);
}
Err(SpawnError::BinaryNotFound { binary: name })
}
#[allow(clippy::await_holding_lock)]
pub async fn get_executable(
&self,
name: &str,
fs: Option<&dyn FileSystem>,
) -> Option<Executable> {
let name = name.to_string();
{
let cache = self.local.read().unwrap();
if let Some(data) = cache.get(&name) {
data.clone().map(Executable::BinaryPackage);
}
}
let mut cache = self.local.write().unwrap();
if let Some(data) = cache.get(&name) {
return data.clone().map(Executable::BinaryPackage);
}
if name.starts_with('/') {
if let Some(fs) = fs {
match load_executable_from_filesystem(fs, name.as_ref(), self.runtime()).await {
Ok(executable) => {
if let Executable::BinaryPackage(pkg) = &executable {
cache.insert(name, Some(pkg.clone()));
}
return Some(executable);
}
Err(e) => {
tracing::warn!(
path = name,
error = &*e,
"Unable to load the package from disk"
);
}
}
}
}
cache.insert(name, None);
None
}
}
pub enum Executable {
Wasm(bytes::Bytes),
BinaryPackage(BinaryPackage),
}
async fn load_executable_from_filesystem(
fs: &dyn FileSystem,
path: &Path,
rt: &(dyn Runtime + Send + Sync),
) -> Result<Executable, anyhow::Error> {
let mut f = fs
.new_open_options()
.read(true)
.open(path)
.context("Unable to open the file")?;
let mut data = Vec::with_capacity(f.size() as usize);
f.read_to_end(&mut data).await.context("Read failed")?;
let bytes: bytes::Bytes = data.into();
if let Ok(container) = from_bytes(bytes.clone()) {
let pkg = BinaryPackage::from_webc(&container, rt)
.await
.context("Unable to load the package")?;
Ok(Executable::BinaryPackage(pkg))
} else {
Ok(Executable::Wasm(bytes))
}
}