Work in progress towards implementing WebSocket connection.

This commit is contained in:
Condorra 2024-09-06 17:37:43 +10:00
parent 1fa196da1f
commit 1df6e574d2
6 changed files with 330 additions and 15 deletions

15
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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::<Rootable![TermFrame]>(&ctx, frame.clone()).into()
}
fn try_unwrap_frame<'gc>(
ctx: Context<'gc>,
value: &Value<'gc>,
) -> Result<TermFrame, piccolo::Error<'gc>> {
match u64::from_value(ctx, *value) {
Ok(v) => Ok(TermFrame(v)),
Err(_) => Ok(UserData::from_value(ctx, *value)?
.downcast::<Rootable![TermFrame]>()?
.clone()),
}
}
fn wrap_socketid<'gc>(ctx: Context<'gc>, socket: &WebSocketId) -> Value<'gc> {
UserData::new::<Rootable![WebSocketId]>(&ctx, socket.clone()).into()
}
fn try_unwrap_socketid<'gc>(
ctx: Context<'gc>,
value: &Value<'gc>,
) -> Result<WebSocketId, piccolo::Error<'gc>> {
Ok(UserData::from_value(ctx, *value)?
.downcast::<Rootable![WebSocketId]>()?
.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<GlobalLayoutCell>,
) -> 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<GlobalLayoutCell>,
) -> 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)
})
}

View File

@ -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<RegisteredTermFrames>,
lua_engine: RefCell<LuaState>,
ws_registry: RefCell<RegisteredWebSockets>,
// 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()),
});

View File

@ -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)]

140
src/websocket.rs Normal file
View File

@ -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<dyn FnMut(MessageEvent)>, Closure<dyn FnMut()>)>,
}
pub type RegisteredWebSockets = BTreeMap<WebSocketId, WebSocketData>;
#[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<WebSocketId> {
let mut data = WebSocketData {
connection: WebSocket::new(url).map_err(|e| {
anyhow::Error::msg(
e.dyn_into::<DomException>()
.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<dyn FnMut(MessageEvent)> = Closure::new(move |ev: MessageEvent| {
let data = ev.data();
if data.has_type::<ArrayBuffer>() {
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::<JsString>() {
let msg: String = data.unchecked_into::<JsString>().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<dyn FnMut()> =
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::<DomException>()
.map(|e| anyhow::Error::msg(e.message()))
.unwrap_or_else(|_| anyhow::Error::msg("Unexpected exception while sending"))
})?;
Ok(())
}
}
}