Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin): simple timers implemented via the set_timeout() call #394

Merged
merged 3 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* Completions are not assets anymore, but commands `option --generate-completion [shell]` (https://github.com/zellij-org/zellij/pull/369)
* Fixes in the default configuration `default.yaml` file. Adds initial tmux-compat keybindings `tmux.yaml` (https://github.com/zellij-org/zellij/pull/362)
* Added the `get_plugin_ids()` query function to the plugin API (https://github.com/zellij-org/zellij/pull/392)
* Implemented simple plugin timers via the `set_timeout()` call (https://github.com/zellij-org/zellij/pull/394)

## [0.5.1] - 2021-04-23
* Change config to flag (https://github.com/zellij-org/zellij/pull/300)
Expand Down
10 changes: 6 additions & 4 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use pty_bus::{PtyBus, PtyInstruction};
use screen::{Screen, ScreenInstruction};
use serde::{Deserialize, Serialize};
use utils::consts::ZELLIJ_IPC_PIPE;
use wasm_vm::{wasi_stdout, wasi_write_json, zellij_exports, PluginEnv, PluginInstruction};
use wasm_vm::{wasi_read_string, wasi_write_object, zellij_exports, PluginEnv, PluginInstruction};
use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value};
use wasmer_wasi::{Pipe, WasiState};
use zellij_tile::data::{EventType, ModeInfo};
Expand Down Expand Up @@ -457,6 +457,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
let send_pty_instructions = send_pty_instructions.clone();
let send_screen_instructions = send_screen_instructions.clone();
let send_app_instructions = send_app_instructions.clone();
let send_plugin_instructions = send_plugin_instructions.clone();

let store = Store::default();
let mut plugin_id = 0;
Expand Down Expand Up @@ -501,6 +502,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
send_pty_instructions: send_pty_instructions.clone(),
send_screen_instructions: send_screen_instructions.clone(),
send_app_instructions: send_app_instructions.clone(),
send_plugin_instructions: send_plugin_instructions.clone(),
wasi_env,
subscriptions: Arc::new(Mutex::new(HashSet::new())),
};
Expand All @@ -510,7 +512,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {

let start = instance.exports.get_function("_start").unwrap();

// This eventually calls the `.init()` method
// This eventually calls the `.load()` method
start.call(&[]).unwrap();

plugin_map.insert(plugin_id, (instance, plugin_env));
Expand All @@ -524,7 +526,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
let event_type = EventType::from_str(&event.to_string()).unwrap();
if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) {
let update = instance.exports.get_function("update").unwrap();
wasi_write_json(&plugin_env.wasi_env, &event);
wasi_write_object(&plugin_env.wasi_env, &event);
update.call(&[]).unwrap();
}
}
Expand All @@ -539,7 +541,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
.call(&[Value::I32(rows as i32), Value::I32(cols as i32)])
.unwrap();

buf_tx.send(wasi_stdout(&plugin_env.wasi_env)).unwrap();
buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap();
}
PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)),
PluginInstruction::Quit => break,
Expand Down
54 changes: 46 additions & 8 deletions src/common/wasm_vm.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use serde::Serialize;
use serde::{de::DeserializeOwned, Serialize};
use std::{
collections::HashSet,
path::PathBuf,
process,
sync::{mpsc::Sender, Arc, Mutex},
thread,
time::{Duration, Instant},
};
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
use wasmer_wasi::WasiEnv;
Expand All @@ -25,9 +27,11 @@ pub enum PluginInstruction {
#[derive(WasmerEnv, Clone)]
pub struct PluginEnv {
pub plugin_id: u32,
// FIXME: This should be a big bundle of all of the channels
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
pub send_app_instructions: SenderWithContext<AppInstruction>,
pub send_pty_instructions: SenderWithContext<PtyInstruction>, // FIXME: This should be a big bundle of all of the channels
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
pub wasi_env: WasiEnv,
pub subscriptions: Arc<Mutex<HashSet<EventType>>>,
}
Expand All @@ -54,18 +58,19 @@ pub fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject {
host_set_selectable,
host_get_plugin_ids,
host_open_file,
host_set_timeout,
}
}

fn host_subscribe(plugin_env: &PluginEnv) {
let mut subscriptions = plugin_env.subscriptions.lock().unwrap();
let new: HashSet<EventType> = serde_json::from_str(&wasi_stdout(&plugin_env.wasi_env)).unwrap();
let new: HashSet<EventType> = wasi_read_object(&plugin_env.wasi_env);
subscriptions.extend(new);
}

fn host_unsubscribe(plugin_env: &PluginEnv) {
let mut subscriptions = plugin_env.subscriptions.lock().unwrap();
let old: HashSet<EventType> = serde_json::from_str(&wasi_stdout(&plugin_env.wasi_env)).unwrap();
let old: HashSet<EventType> = wasi_read_object(&plugin_env.wasi_env);
subscriptions.retain(|k| !old.contains(k));
}

Expand Down Expand Up @@ -107,21 +112,49 @@ fn host_get_plugin_ids(plugin_env: &PluginEnv) {
plugin_id: plugin_env.plugin_id,
zellij_pid: process::id(),
};
wasi_write_json(&plugin_env.wasi_env, &ids);
wasi_write_object(&plugin_env.wasi_env, &ids);
}

fn host_open_file(plugin_env: &PluginEnv) {
let path = PathBuf::from(wasi_stdout(&plugin_env.wasi_env).lines().next().unwrap());
let path: PathBuf = wasi_read_object(&plugin_env.wasi_env);
plugin_env
.send_pty_instructions
.send(PtyInstruction::SpawnTerminal(Some(path)))
.unwrap();
}

fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) {
// There is a fancy, high-performance way to do this with zero additional threads:
// If the plugin thread keeps a BinaryHeap of timer structs, it can manage multiple and easily `.peek()` at the
// next time to trigger in O(1) time. Once the wake-up time is known, the `wasm` thread can use `recv_timeout()`
// to wait for an event with the timeout set to be the time of the next wake up. If events come in in the meantime,
// they are handled, but if the timeout triggers, we replace the event from `recv()` with an
// `Update(pid, TimerEvent)` and pop the timer from the Heap (or reschedule it). No additional threads for as many
// timers as we'd like.
//
// But that's a lot of code, and this is a few lines:
let send_plugin_instructions = plugin_env.send_plugin_instructions.clone();
let update_target = Some(plugin_env.plugin_id);
thread::spawn(move || {
let start_time = Instant::now();
thread::sleep(Duration::from_secs_f64(secs));
// FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the
// time it takes an event to actually reach the plugin after it's sent to the `wasm` thread.
let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64();

send_plugin_instructions
.send(PluginInstruction::Update(
update_target,
Event::Timer(elapsed_time),
))
.unwrap();
});
}

// Helper Functions ---------------------------------------------------------------------------------------------------

// FIXME: Unwrap city
pub fn wasi_stdout(wasi_env: &WasiEnv) -> String {
pub fn wasi_read_string(wasi_env: &WasiEnv) -> String {
let mut state = wasi_env.state();
let wasi_file = state.fs.stdout_mut().unwrap().as_mut().unwrap();
let mut buf = String::new();
Expand All @@ -135,6 +168,11 @@ pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) {
writeln!(wasi_file, "{}\r", buf).unwrap();
}

pub fn wasi_write_json(wasi_env: &WasiEnv, object: &impl Serialize) {
pub fn wasi_write_object(wasi_env: &WasiEnv, object: &impl Serialize) {
wasi_write_string(wasi_env, &serde_json::to_string(&object).unwrap());
}

pub fn wasi_read_object<T: DeserializeOwned>(wasi_env: &WasiEnv) -> T {
let json = wasi_read_string(wasi_env);
serde_json::from_str(&json).unwrap()
}
5 changes: 2 additions & 3 deletions zellij-tile/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ pub enum Key {
Esc,
}

#[derive(
Debug, Clone, PartialEq, Eq, Hash, EnumDiscriminants, ToString, Serialize, Deserialize,
)]
#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)]
#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
#[strum_discriminants(name(EventType))]
#[non_exhaustive]
pub enum Event {
ModeUpdate(ModeInfo),
TabUpdate(Vec<TabInfo>),
KeyPress(Key),
Timer(f64),
}

/// Describes the different input modes, which change the way that keystrokes will be interpreted.
Expand Down
18 changes: 14 additions & 4 deletions zellij-tile/src/shim.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use serde::de::DeserializeOwned;
use serde::{de::DeserializeOwned, Serialize};
use std::{io, path::Path};

use crate::data::*;

// Subscription Handling

pub fn subscribe(event_types: &[EventType]) {
println!("{}", serde_json::to_string(event_types).unwrap());
object_to_stdout(&event_types);
unsafe { host_subscribe() };
}

pub fn unsubscribe(event_types: &[EventType]) {
println!("{}", serde_json::to_string(event_types).unwrap());
object_to_stdout(&event_types);
unsafe { host_unsubscribe() };
}

Expand All @@ -38,10 +38,14 @@ pub fn get_plugin_ids() -> PluginIds {
// Host Functions

pub fn open_file(path: &Path) {
println!("{}", path.to_string_lossy());
object_to_stdout(&path);
unsafe { host_open_file() };
}

pub fn set_timeout(secs: f64) {
unsafe { host_set_timeout(secs) };
}

// Internal Functions

#[doc(hidden)]
Expand All @@ -51,6 +55,11 @@ pub fn object_from_stdin<T: DeserializeOwned>() -> T {
serde_json::from_str(&json).unwrap()
}

#[doc(hidden)]
pub fn object_to_stdout(object: &impl Serialize) {
println!("{}", serde_json::to_string(object).unwrap());
}

#[link(wasm_import_module = "zellij")]
extern "C" {
fn host_subscribe();
Expand All @@ -60,4 +69,5 @@ extern "C" {
fn host_set_invisible_borders(invisible_borders: i32);
fn host_get_plugin_ids();
fn host_open_file();
fn host_set_timeout(secs: f64);
}