use std::{collections::HashMap, env, os::unix::fs::PermissionsExt, sync::Arc};
use ::extension_http::{Body, HttpRequestExt};
use anyhow::{Context, Result};
use async_trait::async_trait;
use futures::{lock::Mutex, TryFutureExt};
use log::{debug, error, info};
use wasmtime_wasi::WasiView;
use workoss::extension::{
extension_http::{self, HttpRequest, HttpResponse},
platform::{self, Arch, Os},
};
use crate::WasmState;
wasmtime::component::bindgen!({
path:"./wit",
world: "extension",
async: true,
trappable_imports: true,
with: {
"workoss:extension/extension-http/http-response-stream":ExtensionHttpResponseStream
}
});
pub type ExtensionHttpResponseStream =
Arc<Mutex<::extension_http::Response<::extension_http::Body>>>;
impl WasiView for WasmState {
fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable {
&mut self.table
}
fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx {
&mut self.ctx
}
}
#[async_trait]
impl ExtensionImports for WasmState {
#[doc = " 获取配置"]
#[must_use]
async fn get_settings(
&mut self,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
debug!("wasm: get_settings:{:?}", &key);
println!("import key:{:#?}", key.unwrap());
Ok("get_settings_value".to_string()).to_wasmtime_result()
}
#[doc = " 下载文件"]
#[must_use]
async fn download_file(
&mut self,
url: String,
file_path: String,
file_type: DownloadFileType,
) -> wasmtime::Result<Result<(), String>> {
debug!(
"wasm: download_file:{} {} {:?}",
&url, &file_path, &file_type
);
Ok(Ok(()))
}
#[doc = " 让下载后的文件变成可执行文件"]
#[must_use]
async fn make_file_executable(
&mut self,
file_path: String,
) -> wasmtime::Result<Result<(), String>> {
debug!("wasm: make_file_executable:{}", &file_path);
let path = file_path;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| format!("failed to set permissions for path {path:?}: {error:?}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
#[doc = " 执行命令 返回命令行执行结果"]
#[must_use]
async fn run_command(
&mut self,
cmd: TerminalCommand,
) -> wasmtime::Result<Result<Vec<u8>, String>> {
info!("wasm: run_command:{:#?}", &cmd);
let mut command = std::process::Command::new(cmd.program);
command.env_clear();
if let Some(dir) = cmd.current_dir {
command.current_dir(dir);
} else {
command.current_dir(env::current_dir().unwrap());
}
if let Some(args) = cmd.args {
command.args(args.as_slice());
}
if let Some(envs) = cmd.envs {
let mut env_map = HashMap::<String, String>::new();
for (k, v) in envs.iter() {
env_map.insert(k.clone(), v.clone());
}
command.envs(env_map);
}
let output = command.output().context("failed to run command")?;
if !output.status.success() {
anyhow::bail!(
"failed to run command:{}",
String::from_utf8_lossy(&output.stderr)
)
}
Ok(Ok(output.stdout))
}
}
#[async_trait]
impl platform::Host for WasmState {
#[doc = " get host platform"]
#[must_use]
async fn current_platform(&mut self) -> wasmtime::Result<Result<(Os, Arch), String>> {
(|| async {
Ok((
match env::consts::OS {
"macos" => platform::Os::Macos,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => anyhow::bail!("unsupported os"),
},
match env::consts::ARCH {
"x86_64" => platform::Arch::X8664,
"aarch64" => platform::Arch::Aarch64,
"x86" => platform::Arch::X8632,
_ => anyhow::bail!("unsupported architecture"),
},
))
})()
.map_err(|e| format!("{e:?}"))
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl extension_http::Host for WasmState {
#[doc = "普通http请求"]
#[must_use]
async fn fetch(&mut self, req: HttpRequest) -> wasmtime::Result<Result<HttpResponse, String>> {
(|| async {
let extension_req =
convert_request(&req).map_err(|e| format!("wasm http fetch error:{e}"))?;
let mut response = self
.host
.http_client
.send(extension_req, req.body)
.await
.map_err(|e| format!("wasm http fetch error:{e}"))?;
convert_response(&mut response)
.await
.map_err(|e| format!("wasm http fetch error:{e}"))
})()
.await
.to_wasmtime_result()
}
#[doc = "stream http请求"]
#[must_use]
async fn fetch_stream(
&mut self,
req: HttpRequest,
) -> wasmtime::Result<Result<wasmtime::component::Resource<ExtensionHttpResponseStream>, String>>
{
let extension_req = convert_request(&req)?;
let response = self.host.http_client.send(extension_req, req.body);
(|| async {
let response = response
.await
.map_err(|e| format!("wasm http fetch error:{e}"))?;
let stream = Arc::new(Mutex::new(response));
let resource = self
.table
.push(stream)
.map_err(|e| format!("wasm http fetch error:{e}"))?;
Ok(resource)
})()
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl extension_http::HostHttpResponseStream for WasmState {
#[must_use]
async fn next_chunk(
&mut self,
resource: wasmtime::component::Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
(|| async move {
let mut response = stream.lock().await;
let bytes_read = response
.body_mut()
.with_config()
.limit(8192)
.read_to_vec()
.map_err(|error| format!("wasm http next_chunk error {error}"));
match bytes_read {
Ok(bytes) => Ok(Some(bytes)),
Err(e) => {
error!("wasm http next_chunk error {e}");
Ok(None)
}
}
})()
.await
.to_wasmtime_result()
}
#[must_use]
async fn drop(
&mut self,
_resource: wasmtime::component::Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<()> {
Ok(())
}
}
fn convert_request(
extension_req: &extension_http::HttpRequest,
) -> Result<::extension_http::Builder, anyhow::Error> {
let mut request_builder = ::extension_http::Request::builder()
.method(::extension_http::Method::from(extension_req.method))
.uri(&extension_req.url)
.follow_redirects(match extension_req.redirect_policy {
extension_http::RedirectPolicy::NoFollow => ::extension_http::RedirectPolicy::NoFollow,
extension_http::RedirectPolicy::FollowLimit(limit) => {
::extension_http::RedirectPolicy::FollowLimit(limit)
}
extension_http::RedirectPolicy::FollowAll => {
::extension_http::RedirectPolicy::FollowAll
}
});
for (key, value) in &extension_req.headers {
request_builder = request_builder.header(key, value);
}
Ok(request_builder)
}
async fn convert_response(
response: &mut ::extension_http::Response<Body>,
) -> Result<extension_http::HttpResponse, anyhow::Error> {
let mut extension_resp = extension_http::HttpResponse {
status_code: response.status().as_u16(),
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_resp
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
match response.body_mut().read_to_vec() {
Ok(bytes) => {
extension_resp.body = bytes;
Ok(extension_resp)
}
Err(e) => {
anyhow::bail!("wasm http convert_response error: {e}")
}
}
}
impl From<extension_http::HttpMethod> for ::extension_http::Method {
fn from(value: extension_http::HttpMethod) -> Self {
match value {
extension_http::HttpMethod::Get => Self::GET,
extension_http::HttpMethod::Post => Self::POST,
extension_http::HttpMethod::Put => Self::PUT,
extension_http::HttpMethod::Delete => Self::DELETE,
extension_http::HttpMethod::Head => Self::HEAD,
extension_http::HttpMethod::Options => Self::OPTIONS,
extension_http::HttpMethod::Patch => Self::PATCH,
}
}
}
trait ToWasmtimeResult<T> {
fn to_wasmtime_result(self) -> wasmtime::Result<Result<T, String>>;
}
impl<T> ToWasmtimeResult<T> for Result<T, String> {
fn to_wasmtime_result(self) -> wasmtime::Result<Result<T, String>> {
Ok(self.map_err(|error| error.to_string()))
}
}