lapce/lapce-proxy/src/plugin.rs

365 lines
11 KiB
Rust

use anyhow::{anyhow, Result};
use home::home_dir;
use hotwatch::Hotwatch;
use lapce_rpc::counter::Counter;
use lapce_rpc::plugin::{PluginDescription, PluginId, PluginInfo};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;
use std::time::Duration;
use toml;
use wasmer::ChainableNamedResolver;
use wasmer::ImportObject;
use wasmer::Store;
use wasmer::WasmerEnv;
use wasmer_wasi::Pipe;
use wasmer_wasi::WasiEnv;
use wasmer_wasi::WasiState;
use crate::dispatch::Dispatcher;
pub type PluginName = String;
#[derive(WasmerEnv, Clone)]
pub(crate) struct PluginEnv {
wasi_env: WasiEnv,
desc: PluginDescription,
dispatcher: Dispatcher,
}
#[derive(Clone)]
pub(crate) struct PluginNew {
instance: wasmer::Instance,
env: PluginEnv,
}
pub struct PluginCatalog {
id_counter: Counter,
pub items: HashMap<PluginName, PluginDescription>,
plugins: HashMap<PluginName, PluginNew>,
store: wasmer::Store,
}
impl PluginCatalog {
pub fn new() -> PluginCatalog {
PluginCatalog {
id_counter: Counter::new(),
items: HashMap::new(),
plugins: HashMap::new(),
store: wasmer::Store::default(),
}
}
pub fn stop(&mut self) {
self.items.clear();
self.plugins.clear();
}
pub fn reload(&mut self) {
self.items.clear();
self.plugins.clear();
self.load();
}
pub fn load(&mut self) {
let all_plugins = find_all_plugins();
for plugin_path in &all_plugins {
match load_plugin(plugin_path) {
Err(_e) => (),
Ok(plugin) => {
self.items.insert(plugin.name.clone(), plugin);
}
}
}
}
pub fn install_plugin(
&mut self,
dispatcher: Dispatcher,
plugin: PluginDescription,
) -> Result<()> {
let home = home_dir().unwrap();
let path = home.join(".lapce").join("plugins").join(&plugin.name);
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path)?;
{
let mut file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path.join("plugin.toml"))?;
file.write_all(&toml::to_vec(&plugin)?)?;
}
{
let url = format!(
"https://raw.githubusercontent.com/{}/master/{}",
plugin.repository, plugin.wasm
);
let mut resp = ureq::get(&url).call()?.into_reader();
let mut file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path.join(&plugin.wasm))?;
std::io::copy(&mut resp, &mut file)?;
}
let mut plugin = plugin;
plugin.dir = Some(path.clone());
plugin.wasm = path
.join(&plugin.wasm)
.to_str()
.ok_or_else(|| anyhow!("path can't to string"))?
.to_string();
let p = self.start_plugin(dispatcher, plugin.clone())?;
self.items.insert(plugin.name.clone(), plugin.clone());
self.plugins.insert(plugin.name, p);
Ok(())
}
pub fn start_all(&mut self, dispatcher: Dispatcher) {
for (_, plugin) in self.items.clone().iter() {
if let Ok(p) = self.start_plugin(dispatcher.clone(), plugin.clone()) {
self.plugins.insert(plugin.name.clone(), p);
}
}
}
fn start_plugin(
&mut self,
dispatcher: Dispatcher,
plugin_desc: PluginDescription,
) -> Result<PluginNew> {
let module = wasmer::Module::from_file(&self.store, &plugin_desc.wasm)?;
let output = Pipe::new();
let input = Pipe::new();
let mut wasi_env = WasiState::new("Lapce")
.map_dir("/", plugin_desc.dir.clone().unwrap())?
.stdin(Box::new(input))
.stdout(Box::new(output))
.finalize()?;
let wasi = wasi_env.import_object(&module)?;
let plugin_env = PluginEnv {
wasi_env,
desc: plugin_desc.clone(),
dispatcher,
};
let lapce = lapce_exports(&self.store, &plugin_env);
let instance = wasmer::Instance::new(&module, &lapce.chain_back(wasi))?;
let plugin = PluginNew {
instance,
env: plugin_env,
};
let local_plugin = plugin.clone();
thread::spawn(move || {
let initialize = local_plugin
.instance
.exports
.get_function("initialize")
.unwrap();
wasi_write_object(
&local_plugin.env.wasi_env,
&PluginInfo {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
configuration: plugin_desc.configuration,
},
);
initialize.call(&[]).unwrap();
});
Ok(plugin)
}
pub fn next_plugin_id(&mut self) -> PluginId {
PluginId(self.id_counter.next())
}
}
impl Default for PluginCatalog {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn lapce_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject {
macro_rules! lapce_export {
($($host_function:ident),+ $(,)?) => {
wasmer::imports! {
"lapce" => {
$(stringify!($host_function) =>
wasmer::Function::new_native_with_env(store, plugin_env.clone(), $host_function),)+
}
}
}
}
lapce_export! {
host_handle_notification,
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "method", content = "params")]
pub enum PluginNotification {
StartLspServer {
exec_path: String,
language_id: String,
options: Option<Value>,
},
DownloadFile {
url: String,
path: PathBuf,
},
LockFile {
path: PathBuf,
},
MakeFileExecutable {
path: PathBuf,
},
}
fn host_handle_notification(plugin_env: &PluginEnv) {
let notification: Result<PluginNotification> =
wasi_read_object(&plugin_env.wasi_env);
if let Ok(notification) = notification {
match notification {
PluginNotification::StartLspServer {
exec_path,
language_id,
options,
} => {
plugin_env.dispatcher.lsp.lock().start_server(
plugin_env
.desc
.dir
.clone()
.unwrap()
.join(&exec_path)
.to_str()
.unwrap(),
&language_id,
options,
);
}
PluginNotification::DownloadFile { url, path } => {
let mut resp = ureq::get(&url)
.call()
.expect("request failed")
.into_reader();
let mut out = std::fs::File::create(
plugin_env.desc.dir.clone().unwrap().join(path),
)
.expect("failed to create file");
std::io::copy(&mut resp, &mut out).expect("failed to copy content");
}
PluginNotification::LockFile { path } => {
let path = plugin_env.desc.dir.clone().unwrap().join(path);
let mut n = 0;
loop {
if let Ok(_file) = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
return;
}
if n > 10 {
return;
}
n += 1;
let mut hotwatch =
Hotwatch::new().expect("hotwatch failed to initialize!");
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = hotwatch.watch(&path, move |_event| {
#[allow(deprecated)]
let _ = tx.send(0);
});
let _ = rx.recv_timeout(Duration::from_secs(10));
}
}
PluginNotification::MakeFileExecutable { path } => {
let _ = Command::new("chmod")
.arg("+x")
.arg(&plugin_env.desc.dir.clone().unwrap().join(path))
.output();
}
}
}
}
pub fn wasi_read_string(wasi_env: &WasiEnv) -> Result<String> {
let mut state = wasi_env.state();
let wasi_file = state
.fs
.stdout_mut()?
.as_mut()
.ok_or_else(|| anyhow!("can't get stdout"))?;
let mut buf = String::new();
wasi_file.read_to_string(&mut buf)?;
Ok(buf)
}
pub fn wasi_read_object<T: DeserializeOwned>(wasi_env: &WasiEnv) -> Result<T> {
let json = wasi_read_string(wasi_env)?;
Ok(serde_json::from_str(&json)?)
}
pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) {
let mut state = wasi_env.state();
let wasi_file = state.fs.stdin_mut().unwrap().as_mut().unwrap();
writeln!(wasi_file, "{}\r", buf).unwrap();
}
pub fn wasi_write_object(wasi_env: &WasiEnv, object: &(impl Serialize + ?Sized)) {
wasi_write_string(wasi_env, &serde_json::to_string(&object).unwrap());
}
#[derive(Serialize, Deserialize, Debug)]
pub enum PluginRequest {}
pub struct PluginHandler {}
fn find_all_plugins() -> Vec<PathBuf> {
let mut plugin_paths = Vec::new();
let home = home_dir().unwrap();
let path = home.join(".lapce").join("plugins");
let _ = path.read_dir().map(|dir| {
dir.flat_map(|item| item.map(|p| p.path()).ok())
.map(|dir| dir.join("plugin.toml"))
.filter(|f| f.exists())
.for_each(|f| plugin_paths.push(f))
});
plugin_paths
}
fn load_plugin(path: &Path) -> Result<PluginDescription> {
let mut file = fs::File::open(&path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let mut plugin: PluginDescription = toml::from_str(&contents)?;
plugin.dir = Some(path.parent().unwrap().canonicalize()?);
plugin.wasm = path
.parent()
.unwrap()
.join(&plugin.wasm)
.canonicalize()?
.to_str()
.ok_or_else(|| anyhow!("path can't to string"))?
.to_string();
Ok(plugin)
}