From 1df6e574d2b907341c64d1a2290ec580deb36ec6 Mon Sep 17 00:00:00 2001 From: Condorra Date: Fri, 6 Sep 2024 17:37:43 +1000 Subject: [PATCH] Work in progress towards implementing WebSocket connection. --- Cargo.lock | 15 ++-- Cargo.toml | 3 + src/lua_state.rs | 179 ++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 4 ++ src/term_view.rs | 4 +- src/websocket.rs | 140 ++++++++++++++++++++++++++++++++++++ 6 files changed, 330 insertions(+), 15 deletions(-) create mode 100644 src/websocket.rs diff --git a/Cargo.lock b/Cargo.lock index a3a6351..43ce28c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1015,9 +1015,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.206" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -1046,9 +1046,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.206" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1057,9 +1057,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", "memchr", @@ -1352,11 +1352,14 @@ version = "0.1.0" dependencies = [ "anyhow", "console_error_panic_hook", + "gc-arena", "im", "itertools", "minicrossterm", "nom", "piccolo", + "serde", + "serde_json", "thiserror", "unicode-segmentation", "unicode-width", diff --git a/Cargo.toml b/Cargo.toml index 2e2a68a..6343494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ minicrossterm = { git = "https://git.blastmud.org/blasthavers/minicrossterm.git" thiserror = "1.0.63" console_error_panic_hook = "0.1.7" anyhow = "1.0.86" +serde = "1.0.209" +serde_json = "1.0.127" +gc-arena = "0.5.3" diff --git a/src/lua_state.rs b/src/lua_state.rs index e0d6451..4207bad 100644 --- a/src/lua_state.rs +++ b/src/lua_state.rs @@ -1,13 +1,18 @@ use anyhow::Error; +use gc_arena::Rootable; use piccolo::{ - Callback, Closure, Context, Executor, FromValue, Function, IntoValue, Lua, StashedExecutor, - StaticError, Table, Value, Variadic, + self, Callback, Closure, Context, Executor, FromValue, Function, IntoValue, Lua, + StashedExecutor, StaticError, Table, UserData, Value, Variadic, }; +use wasm_bindgen::JsValue; +use web_sys::console; use yew::UseStateSetter; use crate::{ - command_handler::debrace, echo_to_term_frame, GlobalLayoutCell, GlobalLayoutState, - GlobalMemoCell, TermFrame, + command_handler::debrace, + echo_to_term_frame, + websocket::{connect_websocket, send_message_to_mud, WebSocketId}, + GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell, TermFrame, }; use std::{collections::VecDeque, rc::Rc, str}; @@ -28,7 +33,11 @@ impl LuaState { fn try_set_current_frame(&mut self, frame: &TermFrame) -> Result<(), StaticError> { self.interp.try_enter(|ctx| { let info_table = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"info")))?; - info_table.set(ctx, ctx.intern_static(b"current_frame"), frame.0 as i64)?; + info_table.set( + ctx, + ctx.intern_static(b"current_frame"), + wrap_frame(ctx, frame), + )?; Ok(()) }) } @@ -111,6 +120,7 @@ pub fn install_lua_globals( register_command!(echo); register_command!(echo_frame); register_command!(echo_frame_raw); + register_command!(sendmud_raw); register_command!(hsplit); register_command!(panel_merge); register_command!(vsplit); @@ -122,6 +132,10 @@ pub fn install_lua_globals( ctx.set_global(ctx.intern_static(b"info").into_value(ctx), info_table) .map(|_| ()) .map_err(|_| Error::msg("Can't set info key"))?; + let muds_table = Table::new(&ctx); + ctx.set_global(ctx.intern_static(b"muds").into_value(ctx), muds_table) + .map(|_| ()) + .map_err(|_| Error::msg("Can't set muds key"))?; Ok(()) }) @@ -137,12 +151,11 @@ fn echo_frame_raw<'gc, 'a>( ) -> Callback<'gc> { let global_memo = global_memo.clone(); Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { - let frame_no: u64 = stack.from_front(ctx)?; + let frame: TermFrame = try_unwrap_frame(ctx, &stack.pop_front())?; let message: piccolo::String = stack.from_front(ctx)?; let message_str = str::from_utf8(message.as_bytes()) .map_err(|_| "Expected message to echo to be UTF-8.".into_value(ctx))?; - echo_to_term_frame(&global_memo, &TermFrame(frame_no), message_str) - .map_err(|m| m.into_value(ctx))?; + echo_to_term_frame(&global_memo, &frame, message_str).map_err(|m| m.into_value(ctx))?; Ok(piccolo::CallbackReturn::Return) }) } @@ -267,3 +280,153 @@ fn panel_merge<'gc>( Ok(piccolo::CallbackReturn::Return) }) } + +fn wrap_frame<'gc>(ctx: Context<'gc>, frame: &TermFrame) -> Value<'gc> { + UserData::new::(&ctx, frame.clone()).into() +} + +fn try_unwrap_frame<'gc>( + ctx: Context<'gc>, + value: &Value<'gc>, +) -> Result> { + match u64::from_value(ctx, *value) { + Ok(v) => Ok(TermFrame(v)), + Err(_) => Ok(UserData::from_value(ctx, *value)? + .downcast::()? + .clone()), + } +} + +fn wrap_socketid<'gc>(ctx: Context<'gc>, socket: &WebSocketId) -> Value<'gc> { + UserData::new::(&ctx, socket.clone()).into() +} + +fn try_unwrap_socketid<'gc>( + ctx: Context<'gc>, + value: &Value<'gc>, +) -> Result> { + Ok(UserData::from_value(ctx, *value)? + .downcast::()? + .clone()) +} + +pub fn handle_websocket_output( + socket: &WebSocketId, + engine: &mut LuaState, + input: &[u8], +) -> Result<(), String> { + engine + .interp + .try_enter(|ctx| { + let handlers = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"handlers")))?; + let input_fn = + Function::from_value(ctx, handlers.get(ctx, ctx.intern_static(b"mudoutput")))?; + ctx.fetch(&engine.exec).restart( + ctx, + input_fn, + (wrap_socketid(ctx, socket), ctx.intern(input)), + ); + Ok(()) + }) + .map_err(|err| format!("{}", err))?; + engine + .interp + .execute::<()>(&engine.exec) + .map_err(|err| format!("{}", err)) +} + +pub fn handle_websocket_output_log_err(socket: &WebSocketId, engine: &mut LuaState, input: &[u8]) { + match handle_websocket_output(socket, engine, input) { + Ok(()) => {} + Err(e) => console::log_2( + &JsValue::from_str("An error occurred calling the WebSocket input handler"), + &JsValue::from_str(&e), + ), + } +} + +pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Result<(), String> { + engine + .interp + .try_enter(|ctx| { + let handlers = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"handlers")))?; + let input_fn = + Function::from_value(ctx, handlers.get(ctx, ctx.intern_static(b"mudclose")))?; + ctx.fetch(&engine.exec) + .restart(ctx, input_fn, wrap_socketid(ctx, socket)); + Ok(()) + }) + .map_err(|err| format!("{}", err))?; + engine + .interp + .execute::<()>(&engine.exec) + .map_err(|err| format!("{}", err)) +} + +pub fn handle_websocket_close_log_err(socket: &WebSocketId, engine: &mut LuaState) { + match handle_websocket_close(socket, engine) { + Ok(()) => {} + Err(e) => console::log_2( + &JsValue::from_str("An error occurred calling the WebSocket input handler"), + &JsValue::from_str(&e), + ), + } +} + +fn sendmud_raw<'gc>( + ctx: Context<'gc>, + global_memo: &GlobalMemoCell, + _global_layout: &UseStateSetter, +) -> Callback<'gc> { + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let mud: WebSocketId = try_unwrap_socketid(ctx, &stack.pop_front())?; + let msg: piccolo::String = stack.from_front(ctx)?; + send_message_to_mud(&mud, msg.as_bytes(), &global_memo)?; + Ok(piccolo::CallbackReturn::Return) + }) +} + +fn connect_mud<'gc>( + ctx: Context<'gc>, + global_memo: &GlobalMemoCell, + _global_layout: &UseStateSetter, +) -> Callback<'gc> { + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let trusted: bool = false; + let name: String = loop { + let v: String = stack.from_front(ctx)?; + if v == "-trust" { + trusted = true; + continue; + } + break v; + }; + let name: Value<'gc> = ctx.intern(debrace(&name).as_bytes()).into(); + + let muds = Table::from_value(ctx, ctx.get_global(ctx.intern_static(b"muds")))?; + if !muds.get_value(name).is_nil() { + Err(Error::msg( + "Attempt to create MUD connection using name that's already taken", + ))? + } + + let url: String = stack.from_front(ctx)?; + let new_socket = connect_websocket( + trusted, + &url, + &mut global_memo.ws_registry.borrow_mut(), + &global_memo, + )?; + + ctx.registry.singleton() + + muds.set(ctx, name, new_socket)?; + + let mud: WebSocketId = try_unwrap_socketid(ctx, &stack.pop_front())?; + let msg: piccolo::String = stack.from_front(ctx)?; + send_message_to_mud(&mud, msg.as_bytes(), &global_memo)?; + Ok(piccolo::CallbackReturn::Return) + }) +} diff --git a/src/main.rs b/src/main.rs index 8ae0775..2f833db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,15 +11,18 @@ pub mod parsing; pub mod split_panel; pub mod term_split; pub mod term_view; +pub mod websocket; use crate::lua_state::{install_lua_globals, LuaState}; use crate::split_panel::*; use crate::term_view::*; +use crate::websocket::RegisteredWebSockets; #[derive(Properties)] pub struct GlobalMemoState { // No strong references allowed between each of these groups of state. frame_registry: RefCell, lua_engine: RefCell, + ws_registry: RefCell, // A cache of the latest layout info (separate from the state). // Updating this doesn't force a relayout, so only update the cache when @@ -59,6 +62,7 @@ fn app() -> Html { }); let global_memo = use_memo((), |_| GlobalMemoState { frame_registry: RegisteredTermFrames::new().into(), + ws_registry: RegisteredWebSockets::new().into(), lua_engine: LuaState::setup().expect("Can create interpreter").into(), layout: RefCell::new((*global_layout).clone()), }); diff --git a/src/term_view.rs b/src/term_view.rs index 73840dc..1a0c885 100644 --- a/src/term_view.rs +++ b/src/term_view.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use gc_arena::Collect; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; use yew::prelude::*; @@ -65,7 +66,8 @@ extern "C" { fn fit(this: &FitAddon); } -#[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone, Debug)] +#[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone, Debug, Collect)] +#[collect(require_static)] pub struct TermFrame(pub u64); #[derive(Properties)] diff --git a/src/websocket.rs b/src/websocket.rs new file mode 100644 index 0000000..7769d19 --- /dev/null +++ b/src/websocket.rs @@ -0,0 +1,140 @@ +use std::collections::BTreeMap; + +use gc_arena::Collect; +use serde::Deserialize; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; +use web_sys::{ + console, + js_sys::{ArrayBuffer, JsString, Uint8Array}, + BinaryType, DomException, MessageEvent, WebSocket, +}; + +use crate::{ + lua_state::{handle_websocket_close_log_err, handle_websocket_output_log_err}, + GlobalMemoCell, +}; + +#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Clone, Collect)] +#[collect(require_static)] +pub struct WebSocketId(pub u64); +pub struct WebSocketData { + pub connection: WebSocket, + pub trusted: bool, // Is it allowed to submit Lua to be run? + pub closed: bool, + pub url: String, + pub retained_closures: Option<(Closure, Closure)>, +} +pub type RegisteredWebSockets = BTreeMap; + +#[derive(Deserialize)] +pub enum MessageFromServer { + RunLua(String), + #[serde(other)] + Unknown, +} + +pub fn connect_websocket( + trusted: bool, + url: &str, + sockets: &mut RegisteredWebSockets, + // Only used for callbacks, expected sockets already borrowed mut! + globals: &GlobalMemoCell, +) -> anyhow::Result { + let mut data = WebSocketData { + connection: WebSocket::new(url).map_err(|e| { + anyhow::Error::msg( + e.dyn_into::() + .map(|e| e.message().to_string()) + .unwrap_or("Unknown error connecting".to_owned()), + ) + })?, + trusted, + closed: false, + url: url.to_owned(), + retained_closures: None, + }; + data.connection.set_binary_type(BinaryType::Arraybuffer); + + let new_id = sockets + .last_key_value() + .map_or(WebSocketId(0), |v| WebSocketId(v.0 .0 + 1)); + let data_globals = globals.clone(); + let data_new_id = new_id.clone(); + let data_closure: Closure = Closure::new(move |ev: MessageEvent| { + let data = ev.data(); + if data.has_type::() { + handle_websocket_output_log_err( + &data_new_id, + &mut data_globals.lua_engine.borrow_mut(), + &Uint8Array::new(&data).to_vec(), + ); + } else if data.has_type::() { + let msg: String = data.unchecked_into::().into(); + if let Some(latest_data) = data_globals.ws_registry.borrow().get(&data_new_id) { + if latest_data.trusted { + match data_globals.lua_engine.borrow_mut().execute(&msg) { + Ok(()) => {} + Err(e) => console::log_3( + &JsValue::from_str("Error executing Lua from websocket"), + &JsValue::from_str(&latest_data.url), + &JsValue::from_str(&e), + ), + } + } else { + console::log_2( + &JsValue::from_str("Ignoring Lua from untrusted websocket."), + &JsValue::from_str(&latest_data.url), + ); + } + } + } + }); + let close_globals = globals.clone(); + let close_id = new_id.clone(); + let close_closure: Closure = + Closure::new( + move || match close_globals.ws_registry.borrow_mut().get_mut(&close_id) { + None => {} + Some(closed_socket) => { + closed_socket.closed = true; + closed_socket.retained_closures = None; + handle_websocket_close_log_err( + &close_id, + &mut close_globals.lua_engine.borrow_mut(), + ); + } + }, + ); + + data.connection + .add_event_listener_with_callback("message", data_closure.as_ref().unchecked_ref()) + .expect("Couldn't set message handler on WebSocket"); + data.connection + .add_event_listener_with_callback("close", close_closure.as_ref().unchecked_ref()) + .expect("Couldn't set close handler on WebSocket"); + data.connection + .add_event_listener_with_callback("error", close_closure.as_ref().unchecked_ref()) + .expect("Couldn't set error handler on WebSocket"); + data.retained_closures = Some((data_closure, close_closure)); + sockets.insert(new_id.clone(), data); + Ok(new_id) +} + +pub fn send_message_to_mud( + socket: &WebSocketId, + msg: &[u8], + global: &GlobalMemoCell, +) -> anyhow::Result<()> { + match global.ws_registry.borrow().get(socket) { + None => Err(anyhow::Error::msg("MUD connection not found")), + Some(sock_data) if sock_data.closed => Err(anyhow::Error::msg("MUD connection is closed")), + Some(sock_data) => { + sock_data.connection.send_with_u8_array(msg).map_err(|e| { + e.dyn_into::() + .map(|e| anyhow::Error::msg(e.message())) + .unwrap_or_else(|_| anyhow::Error::msg("Unexpected exception while sending")) + })?; + Ok(()) + } + } +}