/
githubmirror
/
lapce
Обзор
Документация
Войти
/
githubmirror
/
lapce
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
lapce-proxy/src/plugin/wasi.rs
629 строк
19 KB
Jakub Panek
chore: were spellcecked nauw (#3887)
03 апр 2026, 20:56
Не верифицирован
03 апр 2026, 20:56
c9e4c33
Код
Авторство
О чём код?
#[cfg(test)] mod tests; use std::{ collections::{HashMap, VecDeque}, fs, io::{Read, Seek, Write}, path::{Path, PathBuf}, process, sync::{Arc, RwLock}, thread, }; use anyhow::{Result, anyhow}; use jsonrpc_lite::{Id, Params}; use lapce_core::directory::Directory; use lapce_rpc::{ RpcError, plugin::{PluginId, VoltID, VoltInfo, VoltMetadata}, style::LineStyle, }; use lapce_xi_rope::{Rope, RopeDelta}; use lsp_types::{ DocumentFilter, InitializeParams, InitializedParams, TextDocumentContentChangeEvent, TextDocumentIdentifier, Url, VersionedTextDocumentIdentifier, WorkDoneProgressParams, WorkspaceFolder, notification::Initialized, request::Initialize, }; use parking_lot::Mutex; use psp_types::{Notification, Request}; use serde_json::Value; use wasi_experimental_http_wasmtime::{HttpCtx, HttpState}; use wasmtime_wasi::WasiCtxBuilder; use super::{ PluginCatalogRpcHandler, client_capabilities, psp::{ PluginHandlerNotification, PluginHostHandler, PluginServerHandler, PluginServerRpc, ResponseSender, RpcCallback, handle_plugin_server_message, }, volt_icon, }; use crate::plugin::psp::PluginServerRpcHandler; #[derive(Default)] pub struct WasiPipe { buffer: VecDeque<u8>, } impl WasiPipe { pub fn new() -> Self { Self::default() } } impl Read for WasiPipe { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { let amt = std::cmp::min(buf.len(), self.buffer.len()); for (i, byte) in self.buffer.drain(..amt).enumerate() { buf[i] = byte; } Ok(amt) } } impl Write for WasiPipe { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.buffer.extend(buf); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } impl Seek for WasiPipe { fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> { Err(std::io::Error::other("can not seek in a pipe")) } } pub struct Plugin { #[allow(dead_code)] id: PluginId, host: PluginHostHandler, configurations: Option<HashMap<String, serde_json::Value>>, } impl PluginServerHandler for Plugin { fn method_registered(&mut self, method: &str) -> bool { self.host.method_registered(method) } fn document_supported( &mut self, language_id: Option<&str>, path: Option<&Path>, ) -> bool { self.host.document_supported(language_id, path) } fn handle_handler_notification( &mut self, notification: PluginHandlerNotification, ) { use PluginHandlerNotification::*; match notification { Initialize => { self.initialize(); } InitializeResult(result) => { self.host.server_capabilities = result.capabilities; } Shutdown => { self.shutdown(); } SpawnedPluginLoaded { plugin_id } => { self.host.handle_spawned_plugin_loaded(plugin_id); } } } fn handle_host_notification( &mut self, method: String, params: Params, from: String, ) { if let Err(err) = self.host.handle_notification(method, params, from) { tracing::error!("{:?}", err); } } fn handle_host_request( &mut self, id: Id, method: String, params: Params, resp: ResponseSender, ) { self.host.handle_request(id, method, params, resp); } fn handle_did_save_text_document( &self, language_id: String, path: PathBuf, text_document: TextDocumentIdentifier, text: Rope, ) { self.host.handle_did_save_text_document( language_id, path, text_document, text, ); } fn handle_did_change_text_document( &mut self, language_id: String, document: VersionedTextDocumentIdentifier, delta: RopeDelta, text: Rope, new_text: Rope, change: Arc< Mutex<( Option<TextDocumentContentChangeEvent>, Option<TextDocumentContentChangeEvent>, )>, >, ) { self.host.handle_did_change_text_document( language_id, document, delta, text, new_text, change, ); } fn format_semantic_tokens( &self, tokens: lsp_types::SemanticTokens, text: Rope, f: Box<dyn RpcCallback<Vec<LineStyle>, RpcError>>, ) { self.host.format_semantic_tokens(tokens, text, f); } } impl Plugin { fn initialize(&mut self) { let workspace = self.host.workspace.clone(); let configurations = self.configurations.as_ref().map(unflatten_map); let root_uri = workspace.map(|p| Url::from_directory_path(p).unwrap()); let server_rpc = self.host.server_rpc.clone(); self.host.server_rpc.server_request_async( Initialize::METHOD, #[allow(deprecated)] InitializeParams { process_id: Some(process::id()), root_path: None, root_uri: root_uri.clone(), capabilities: client_capabilities(), trace: None, client_info: None, locale: None, initialization_options: configurations, workspace_folders: root_uri.map(|uri| { vec![WorkspaceFolder { name: uri.as_str().to_string(), uri, }] }), work_done_progress_params: WorkDoneProgressParams::default(), }, None, None, false, move |value| match value { Ok(value) => { if let Ok(result) = serde_json::from_value(value) { server_rpc.handle_rpc(PluginServerRpc::Handler( PluginHandlerNotification::InitializeResult(result), )); server_rpc.server_notification( Initialized::METHOD, InitializedParams {}, None, None, false, ); } } Err(err) => { tracing::error!("{:?}", err); } }, ); } fn shutdown(&self) {} } pub fn load_all_volts( plugin_rpc: PluginCatalogRpcHandler, extra_plugin_paths: &[PathBuf], disabled_volts: Vec<VoltID>, ) { let all_volts = find_all_volts(extra_plugin_paths); let volts = all_volts .into_iter() .filter_map(|meta| { meta.wasm.as_ref()?; let icon = volt_icon(&meta); plugin_rpc.core_rpc.volt_installed(meta.clone(), icon); if disabled_volts.contains(&meta.id()) { return None; } Some(meta) }) .collect(); if let Err(err) = plugin_rpc.unactivated_volts(volts) { tracing::error!("{:?}", err); } } /// Find all installed volts. /// `plugin_dev_path` allows launching Lapce with a plugin on your local system for testing /// purposes. /// As well, this function skips any volt in the typical plugin directory that match the name /// of the dev plugin so as to support developing a plugin you actively use. pub fn find_all_volts(extra_plugin_paths: &[PathBuf]) -> Vec<VoltMetadata> { let Some(plugin_dir) = Directory::plugins_directory() else { return Vec::new(); }; let mut plugins: Vec<VoltMetadata> = plugin_dir .read_dir() .ok() .into_iter() .flatten() .filter_map(|result| { let entry = result.ok()?; let metadata = entry.metadata().ok()?; // Ignore any loose files or '.' prefixed hidden directories if metadata.is_file() || entry.file_name().to_str()?.starts_with('.') { return None; } Some(entry.path()) }) .filter_map(|path| match load_volt(&path) { Ok(metadata) => Some(metadata), Err(e) => { tracing::error!("Failed to load plugin: {:?}", e); None } }) .collect(); for plugin_path in extra_plugin_paths { let mut metadata = match load_volt(plugin_path) { Ok(metadata) => metadata, Err(e) => { tracing::error!("Failed to load extra plugin: {:?}", e); continue; } }; let pos = plugins.iter().position(|meta| { meta.name == metadata.name && meta.author == metadata.author }); if let Some(pos) = pos { std::mem::swap(&mut plugins[pos], &mut metadata); } else { plugins.push(metadata); } } plugins } /// Returns an instance of "VoltMetadata" or an error if there is no file in the path, /// the contents of the file cannot be read into a string, or the content read cannot /// be converted to an instance of "VoltMetadata". /// /// # Examples /// /// ``` /// use std::fs::File; /// use std::io::Write; /// use lapce_proxy::plugin::wasi::load_volt; /// use lapce_rpc::plugin::VoltMetadata; /// /// let parent_path = std::env::current_dir().unwrap(); /// let mut file = File::create(parent_path.join("volt.toml")).unwrap(); /// let _ = writeln!(file, "name = \"plugin\" \n version = \"0.1\""); /// let _ = writeln!(file, "display-name = \"Plugin\" \n author = \"Author\""); /// let _ = writeln!(file, "description = \"Useful plugin\"");/// /// let volt_metadata = match load_volt(&parent_path) { /// Ok(volt_metadata) => volt_metadata, /// Err(error) => panic!("{}", error), /// }; /// assert_eq!( /// volt_metadata, /// VoltMetadata { /// name: "plugin".to_string(), /// version: "0.1".to_string(), /// display_name: "Plugin".to_string(), /// author: "Author".to_string(), /// description: "Useful plugin".to_string(), /// icon: None, /// repository: None, /// wasm: None, /// color_themes: None, /// icon_themes: None, /// dir: parent_path.canonicalize().ok(), /// activation: None, /// config: None /// } /// ); /// let _ = std::fs::remove_file(parent_path.join("volt.toml")); /// ``` pub fn load_volt(path: &Path) -> Result<VoltMetadata> { let path = path.canonicalize()?; let mut file = fs::File::open(path.join("volt.toml"))?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let mut meta: VoltMetadata = toml::from_str(&contents)?; meta.dir = Some(path.clone()); meta.wasm = meta.wasm.as_ref().and_then(|wasm| { Some(path.join(wasm).canonicalize().ok()?.to_str()?.to_string()) }); // FIXME: This does `meta.color_themes = Some([])` in case, for example, // it cannot find matching files, but in that case it should do `meta.color_themes = None` meta.color_themes = meta.color_themes.as_ref().map(|themes| { themes .iter() .filter_map(|theme| { Some(path.join(theme).canonicalize().ok()?.to_str()?.to_string()) }) .collect() }); // FIXME: This does `meta.icon_themes = Some([])` in case, for example, // it cannot find matching files, but in that case it should do `meta.icon_themes = None` meta.icon_themes = meta.icon_themes.as_ref().map(|themes| { themes .iter() .filter_map(|theme| { Some(path.join(theme).canonicalize().ok()?.to_str()?.to_string()) }) .collect() }); Ok(meta) } pub fn enable_volt( plugin_rpc: PluginCatalogRpcHandler, volt: VoltInfo, ) -> Result<()> { let path = Directory::plugins_directory() .ok_or_else(|| anyhow!("can't get plugin directory"))? .join(volt.id().to_string()); let meta = load_volt(&path)?; plugin_rpc.unactivated_volts(vec![meta])?; Ok(()) } pub fn start_volt( workspace: Option<PathBuf>, configurations: Option<HashMap<String, serde_json::Value>>, plugin_rpc: PluginCatalogRpcHandler, meta: VoltMetadata, ) -> Result<()> { let engine = wasmtime::Engine::default(); let module = wasmtime::Module::from_file( &engine, meta.wasm .as_ref() .ok_or_else(|| anyhow!("no wasm in plugin"))?, )?; let mut linker = wasmtime::Linker::new(&engine); wasmtime_wasi::add_to_linker(&mut linker, |s| s)?; HttpState::new()?.add_to_linker(&mut linker, |_| HttpCtx { allowed_hosts: Some(vec!["insecure:allow-all".to_string()]), max_concurrent_requests: Some(100), })?; let volt_path = meta .dir .as_ref() .ok_or_else(|| anyhow!("plugin meta doesn't have dir"))?; #[cfg(target_os = "linux")] let volt_libc = { match std::process::Command::new("ldd").arg("--version").output() { Ok(cmd) => { if String::from_utf8_lossy(&cmd.stdout) .to_lowercase() .split_terminator('\n') .next() .unwrap_or("") .contains("musl") { "musl" } else { "glibc" } } _ => "glibc", } }; #[cfg(not(target_os = "linux"))] let volt_libc = ""; let stdin = Arc::new(RwLock::new(WasiPipe::new())); let stdout = Arc::new(RwLock::new(WasiPipe::new())); let stderr = Arc::new(RwLock::new(WasiPipe::new())); let wasi = WasiCtxBuilder::new() .inherit_env()? .env("VOLT_OS", std::env::consts::OS)? .env("VOLT_ARCH", std::env::consts::ARCH)? .env("VOLT_LIBC", volt_libc)? .env( "VOLT_URI", Url::from_directory_path(volt_path) .map_err(|_| anyhow!("can't convert folder path to uri"))? .as_ref(), )? .stdin(Box::new(wasi_common::pipe::ReadPipe::from_shared( stdin.clone(), ))) .stdout(Box::new(wasi_common::pipe::WritePipe::from_shared( stdout.clone(), ))) .stderr(Box::new(wasi_common::pipe::WritePipe::from_shared( stderr.clone(), ))) .preopened_dir( wasmtime_wasi::Dir::open_ambient_dir( volt_path, wasmtime_wasi::ambient_authority(), )?, "/", )? .build(); let mut store = wasmtime::Store::new(&engine, wasi); let (io_tx, io_rx) = crossbeam_channel::unbounded(); let rpc = PluginServerRpcHandler::new(meta.id(), None, None, io_tx); let local_rpc = rpc.clone(); let local_stdin = stdin.clone(); let volt_name = format!("volt {}", meta.name); linker.func_wrap("lapce", "host_handle_rpc", move || { if let Ok(msg) = wasi_read_string(&stdout) { if let Some(resp) = handle_plugin_server_message(&local_rpc, &msg, &volt_name) { if let Ok(msg) = serde_json::to_string(&resp) { if let Err(err) = writeln!(local_stdin.write().unwrap(), "{msg}") { tracing::error!("{:?}", err); } } } } })?; let plugin_meta = meta.clone(); linker.func_wrap("lapce", "host_handle_stderr", move || { if let Ok(msg) = wasi_read_string(&stderr) { tracing_log::log::log!(target: &format!("lapce_proxy::plugin::wasi::{}::{}", plugin_meta.author, plugin_meta.name), tracing_log::log::Level::Debug, "{msg}"); } })?; linker.module(&mut store, "", &module)?; let local_rpc = rpc.clone(); thread::spawn(move || { let mut exist_id = None; { let instance = linker.instantiate(&mut store, &module).unwrap(); let handle_rpc = instance .get_func(&mut store, "handle_rpc") .ok_or_else(|| anyhow!("can't convert to function")) .unwrap() .typed::<(), ()>(&mut store) .unwrap(); for msg in io_rx { if msg .get_method() .map(|x| x == lsp_types::request::Shutdown::METHOD) .unwrap_or_default() { exist_id = msg.get_id(); break; } if let Ok(msg) = serde_json::to_string(&msg) { if let Err(err) = writeln!(stdin.write().unwrap(), "{msg}") { tracing::error!("{:?}", err); } } if let Err(err) = handle_rpc.call(&mut store, ()) { tracing::error!("{:?}", err); } } } if let Some(id) = exist_id { local_rpc.handle_server_response(id, Ok(Value::Null)); } }); let id = PluginId::next(); let mut plugin = Plugin { id, host: PluginHostHandler::new( workspace, meta.dir.clone(), meta.id(), meta.display_name.clone(), meta.activation .iter() .flat_map(|m| m.language.iter().flatten()) .cloned() .map(|s| DocumentFilter { language: Some(s), pattern: None, scheme: None, }) .chain( meta.activation .iter() .flat_map(|m| m.workspace_contains.iter().flatten()) .cloned() .map(|s| DocumentFilter { language: None, pattern: Some(s), scheme: None, }), ) .collect(), plugin_rpc.core_rpc.clone(), rpc.clone(), plugin_rpc.clone(), ), configurations, }; let local_rpc = rpc.clone(); thread::spawn(move || { local_rpc.mainloop(&mut plugin); }); if plugin_rpc.plugin_server_loaded(rpc.clone()).is_err() { rpc.shutdown(); } Ok(()) } fn wasi_read_string(stdout: &Arc<RwLock<WasiPipe>>) -> Result<String> { let mut buf = String::new(); stdout.write().unwrap().read_to_string(&mut buf)?; Ok(buf) } fn unflatten_map(map: &HashMap<String, serde_json::Value>) -> serde_json::Value { let mut new = serde_json::json!({}); for (key, value) in map.iter() { let mut current = new.as_object_mut().unwrap(); let parts: Vec<&str> = key.split('.').collect(); let total_parts = parts.len(); for (i, part) in parts.into_iter().enumerate() { if i + 1 < total_parts { if !current.get(part).map(|v| v.is_object()).unwrap_or(false) { current.insert(part.to_string(), serde_json::json!({})); } current = current.get_mut(part).unwrap().as_object_mut().unwrap(); } else { current.insert(part.to_string(), value.clone()); } } } new }