extension_host 0.0.2

wasm host
Documentation
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);
        //TODO 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()))
    }
}