Fix websocket communication front logic closing too early

This commit is contained in:
NGnius (Graham) 2023-09-01 18:04:34 -04:00
parent 72c7f111e8
commit b7b42a8c6d
8 changed files with 111 additions and 90 deletions

39
Cargo.lock generated
View file

@ -68,17 +68,6 @@ version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]] [[package]]
name = "async-lock" name = "async-lock"
version = "2.7.0" version = "2.7.0"
@ -197,15 +186,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "concurrent-queue"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -251,15 +231,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "ctr" name = "ctr"
version = "0.8.0" version = "0.8.0"
@ -557,9 +528,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]] [[package]]
name = "gloo-net" name = "gloo-net"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3000ef231a67d5bfee6b35f2c0f6f5c8d45b3381ef5bbbea603690ec4e539762" checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -578,9 +549,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-utils" name = "gloo-utils"
version = "0.1.7" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde", "serde",
@ -1593,10 +1564,10 @@ dependencies = [
name = "usdpl-front" name = "usdpl-front"
version = "0.11.0" version = "0.11.0"
dependencies = [ dependencies = [
"async-channel",
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
"futures", "futures",
"futures-channel",
"gloo-net", "gloo-net",
"hex", "hex",
"js-sys", "js-sys",

View file

@ -10,6 +10,8 @@ exclude = [
"templates/decky/backend" "templates/decky/backend"
] ]
resolver = "2"
[profile.release] [profile.release]
# Tell `rustc` to optimize for small code size. # Tell `rustc` to optimize for small code size.
opt-level = "s" opt-level = "s"

View file

@ -98,7 +98,7 @@ impl WebsocketServer {
RatchetError::with_cause(ratchet_rs::ErrorKind::Protocol, e.to_string()) RatchetError::with_cause(ratchet_rs::ErrorKind::Protocol, e.to_string())
})?; })?;
output_stream.for_each_concurrent(None, |result| async { output_stream.for_each(|result| async {
match result { match result {
Ok(msg) => { Ok(msg) => {
let mut ws_lock = websocket.lock().await; let mut ws_lock = websocket.lock().await;
@ -112,6 +112,11 @@ impl WebsocketServer {
} }
}).await; }).await;
websocket.lock().await.close(ratchet_rs::CloseReason {
code: ratchet_rs::CloseCode::Normal,
description: None,
}).await?;
/*let mut buf = BytesMut::new(); /*let mut buf = BytesMut::new();
loop { loop {
match websocket.read(&mut buf).await? { match websocket.read(&mut buf).await? {

View file

@ -20,8 +20,9 @@ encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
[dependencies] [dependencies]
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
gloo-net = { version = "0.3", features = ["websocket"] } gloo-net = { version = "0.4", features = ["websocket"] }
futures = "0.3" futures = "0.3"
futures-channel = "0.3"
console_log = { version = "1.0", optional = true, features = ["color"] } console_log = { version = "1.0", optional = true, features = ["color"] }
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
@ -37,11 +38,10 @@ web-sys = { version = "0.3", features = [
'RequestMode', 'RequestMode',
'Response', 'Response',
'Window', 'Window',
'console',
]} ]}
js-sys = { version = "0.3" } js-sys = { version = "0.3" }
async-channel = "1.8"
obfstr = { version = "0.3", optional = true } obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true } hex = { version = "0.4", optional = true }

View file

@ -1,7 +1,7 @@
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use futures::{SinkExt, StreamExt, future::{select, Either}}; use futures::{SinkExt, StreamExt, future::{select, Either}};
use gloo_net::websocket::{futures::WebSocket, Message, State}; use gloo_net::websocket::{futures::WebSocket, Message};
use nrpc::{ClientHandler, ServiceError, ServiceClientStream, _helpers::async_trait, _helpers::bytes}; use nrpc::{ClientHandler, ServiceError, ServiceClientStream, _helpers::async_trait, _helpers::bytes};
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
@ -13,23 +13,42 @@ pub struct WebSocketHandler {
port: u16, port: u16,
} }
async fn send_recv_ws<'a>(tx: async_channel::Sender<Result<bytes::Bytes, String>>, url: String, mut input: ServiceClientStream<'a, bytes::Bytes>) { #[inline]
let ws = match WebSocket::open(&url).map_err(|e| e.to_string()) { fn ws_is_alive(ws_state: &gloo_net::websocket::State) -> bool {
match ws_state {
gloo_net::websocket::State::Connecting | gloo_net::websocket::State::Open => true,
gloo_net::websocket::State::Closing | gloo_net::websocket::State::Closed => false,
}
}
async fn send_recv_ws<'a>(mut tx: futures_channel::mpsc::Sender<Result<bytes::Bytes, String>>, url: String, mut input: ServiceClientStream<'a, bytes::Bytes>) {
let ws = match WebSocket::open_with_protocol(&url, "usdpl-nrpc").map_err(|e| e.to_string()) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
log::error!("ws open error: {}", e);
tx.send(Err(e.to_string())).await.unwrap_or(()); tx.send(Err(e.to_string())).await.unwrap_or(());
return; return;
} }
}; };
#[cfg(feature = "debug")]
web_sys::console::log_1(&format!("ws opened successfully with url `{}`", url).into());
let (mut input_done, mut output_done) = (false, false); let (mut input_done, mut output_done) = (false, false);
let mut last_ws_state = ws.state(); let mut last_ws_state = ws.state();
#[cfg(feature = "debug")]
web_sys::console::log_1(&format!("ws with url `{}` initial state: {:?}", url, last_ws_state).into());
let (mut ws_sink, mut ws_stream) = ws.split(); let (mut ws_sink, mut ws_stream) = ws.split();
let (mut left, mut right) = (input.next(), ws_stream.next()); let (mut left, mut right) = (input.next(), ws_stream.next());
while let State::Open = last_ws_state { while ws_is_alive(&last_ws_state) {
if !input_done && !output_done { if !input_done && !output_done {
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Input and output streams are both alive").into());
match select(left, right).await { match select(left, right).await {
Either::Left((next, outstanding)) => { Either::Left((next, outstanding)) => {
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Got message to send over websocket").into());
if let Some(next) = next { if let Some(next) = next {
match next { match next {
Ok(next) => { Ok(next) => {
@ -46,6 +65,8 @@ async fn send_recv_ws<'a>(tx: async_channel::Sender<Result<bytes::Bytes, String>
left = input.next(); left = input.next();
}, },
Either::Right((response, outstanding)) => { Either::Right((response, outstanding)) => {
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Received message from websocket").into());
if let Some(next) = response { if let Some(next) = response {
match next { match next {
Ok(Message::Bytes(b)) => tx.send(Ok(b.into())).await.unwrap_or(()), Ok(Message::Bytes(b)) => tx.send(Ok(b.into())).await.unwrap_or(()),
@ -63,7 +84,11 @@ async fn send_recv_ws<'a>(tx: async_channel::Sender<Result<bytes::Bytes, String>
} }
} }
} else if input_done { } else if input_done {
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Input stream is complete").into());
if let Some(next) = right.await { if let Some(next) = right.await {
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Received message from websocket").into());
match next { match next {
Ok(Message::Bytes(b)) => tx.send(Ok(b.into())).await.unwrap_or(()), Ok(Message::Bytes(b)) => tx.send(Ok(b.into())).await.unwrap_or(()),
Ok(_) => tx.send(Err("Message::Text not allowed".into())).await.unwrap_or(()), Ok(_) => tx.send(Err("Message::Text not allowed".into())).await.unwrap_or(()),
@ -78,9 +103,34 @@ async fn send_recv_ws<'a>(tx: async_channel::Sender<Result<bytes::Bytes, String>
(ws_sink, ws_stream) = ws.split(); (ws_sink, ws_stream) = ws.split();
right = ws_stream.next(); right = ws_stream.next();
} else { } else {
// output_done is true
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Output stream is complete").into());
if let Some(next) = left.await {
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("Got message to send over websocket").into());
match next {
Ok(next) => {
if let Err(e) = ws_sink.send(Message::Bytes(next.into())).await {
tx.send(Err(e.to_string())).await.unwrap_or(());
}
},
Err(e) => tx.send(Err(e.to_string())).await.unwrap_or(())
}
} else {
input_done = true;
}
//right = outstanding;
let ws = ws_stream.reunite(ws_sink).unwrap();
last_ws_state = ws.state();
(ws_sink, ws_stream) = ws.split();
left = input.next();
right = ws_stream.next(); // this should always resolve to None (but compiler is unhappy without this)
} }
} }
#[cfg(feature = "debug")]
web_sys::console::debug_1(&format!("ws with url `{}` has closed", url).into());
/*spawn_local(async move { /*spawn_local(async move {
while let State::Open = ws.state() { while let State::Open = ws.state() {
if let Some(next) = input.next().await { if let Some(next) = input.next().await {
@ -147,7 +197,9 @@ impl ClientHandler<'static> for WebSocketHandler {
"ws://usdpl-ws-{}.localhost:{}/{}.{}/{}", "ws://usdpl-ws-{}.localhost:{}/{}.{}/{}",
id, self.port, package, service, method, id, self.port, package, service, method,
); );
let (tx, rx) = async_channel::bounded(CHANNEL_BOUND); #[cfg(feature = "debug")]
web_sys::console::log_1(&format!("doing send/receive on ws url `{}`", url).into());
let (tx, rx) = futures_channel::mpsc::channel(CHANNEL_BOUND);
spawn_local(send_recv_ws(tx, url, input)); spawn_local(send_recv_ws(tx, url, input));
Ok(Box::new(rx.map(|buf_result: Result<bytes::Bytes, String>| buf_result Ok(Box::new(rx.map(|buf_result: Result<bytes::Bytes, String>| buf_result

View file

@ -1,10 +1,10 @@
use js_sys::JsString; //use js_sys::JsString;
use js_sys::JSON::{parse, stringify}; //use js_sys::JSON::{parse, stringify};
use wasm_bindgen::prelude::JsValue; use wasm_bindgen::prelude::JsValue;
use usdpl_core::serdes::Primitive; //use usdpl_core::serdes::Primitive;
pub(crate) fn primitive_to_js(primitive: Primitive) -> JsValue { /*pub(crate) fn primitive_to_js(primitive: Primitive) -> JsValue {
match primitive { match primitive {
Primitive::Empty => JsValue::null(), Primitive::Empty => JsValue::null(),
Primitive::String(s) => JsValue::from_str(&s), Primitive::String(s) => JsValue::from_str(&s),
@ -33,11 +33,11 @@ pub(crate) fn js_to_primitive(val: JsValue) -> Primitive {
} else { } else {
Primitive::Empty Primitive::Empty
} }
} }*/
pub(crate) fn str_to_js<S: std::string::ToString>(s: S) -> JsString { /*pub(crate) fn str_to_js<S: std::string::ToString>(s: S) -> JsString {
s.to_string().into() s.to_string().into()
} }*/
pub(crate) fn js_to_str(js: JsValue) -> String { pub(crate) fn js_to_str(js: JsValue) -> String {
if let Some(s) = js.as_string() { if let Some(s) = js.as_string() {

View file

@ -1,16 +0,0 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[cfg(feature = "debug")]
#[wasm_bindgen(js_namespace = console, js_name = log)]
pub fn console_log(s: &str);
#[cfg(feature = "debug")]
#[wasm_bindgen(js_namespace = console, js_name = warn)]
pub fn console_warn(s: &str);
#[cfg(feature = "debug")]
#[wasm_bindgen(js_namespace = console, js_name = error)]
pub fn console_error(s: &str);
}

View file

@ -7,9 +7,8 @@
mod client_handler; mod client_handler;
pub use client_handler::WebSocketHandler; pub use client_handler::WebSocketHandler;
mod connection; //mod connection;
mod convert; mod convert;
mod imports;
pub mod wasm; pub mod wasm;
/*#[allow(missing_docs)] // existence is pain otherwise /*#[allow(missing_docs)] // existence is pain otherwise
@ -27,21 +26,21 @@ pub mod _helpers {
pub use nrpc; pub use nrpc;
} }
use std::sync::atomic::{AtomicU64, Ordering}; //use std::sync::atomic::{AtomicU64, Ordering};
use js_sys::Array; //use js_sys::Array;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use usdpl_core::{socket::Packet, RemoteCall}; //use usdpl_core::{socket::Packet, RemoteCall};
//const REMOTE_CALL_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); //const REMOTE_CALL_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
//const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337); //const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337);
static mut CTX: UsdplContext = UsdplContext { /*static mut CTX: UsdplContext = UsdplContext {
port: 0, port: 0,
id: AtomicU64::new(0), //id: AtomicU64::new(0),
#[cfg(feature = "encrypt")] #[cfg(feature = "encrypt")]
key: Vec::new(), key: Vec::new(),
}; };*/
static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None; static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None;
@ -52,6 +51,7 @@ fn encryption_key() -> Vec<u8> {
hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap() hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap()
} }
/*
//#[wasm_bindgen] //#[wasm_bindgen]
#[derive(Debug)] #[derive(Debug)]
struct UsdplContext { struct UsdplContext {
@ -68,33 +68,39 @@ fn get_port() -> u16 {
#[cfg(feature = "encrypt")] #[cfg(feature = "encrypt")]
fn get_key() -> Vec<u8> { fn get_key() -> Vec<u8> {
unsafe { CTX.key.clone() } unsafe { CTX.key.clone() }
} }*/
fn increment_id() -> u64 { /*fn increment_id() -> u64 {
let atomic = unsafe { &CTX.id }; let atomic = unsafe { &CTX.id };
atomic.fetch_add(1, Ordering::SeqCst) atomic.fetch_add(1, Ordering::SeqCst)
} }*/
/// Initialize the front-end library /// Initialize the front-end library
#[wasm_bindgen] #[wasm_bindgen]
pub fn init_usdpl(port: u16) { pub fn init_usdpl(port: u16) {
#[cfg(feature = "debug")]
web_sys::console::log_1(&format!("init_usdpl(port={})", port).into());
#[cfg(feature = "console_error_panic_hook")] #[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
#[cfg(feature = "console_log")] #[cfg(feature = "console_log")]
console_log::init_with_level(log::Level::Debug).expect("USDPL: error initializing console log"); console_log::init_with_level(log::Level::Debug).expect("USDPL: error initializing console log");
//REMOTE_PORT.store(port, std::sync::atomic::Ordering::SeqCst);
unsafe { /*unsafe {
CTX = UsdplContext { CTX = UsdplContext {
port: port, port: port,
id: AtomicU64::new(0), //id: AtomicU64::new(0),
#[cfg(feature = "encrypt")] #[cfg(feature = "encrypt")]
key: encryption_key(), key: encryption_key(),
}; };
} }*/
unsafe { unsafe {
CACHE = Some(std::collections::HashMap::new()); CACHE = Some(std::collections::HashMap::new());
} }
#[cfg(feature = "debug")]
web_sys::console::log_1(&format!("USDPL:{} init succeeded", port).into());
log::info!("USDPL:{} init succeeded", port);
} }
/// Get the targeted plugin framework, or "any" if unknown /// Get the targeted plugin framework, or "any" if unknown
@ -134,16 +140,17 @@ pub fn get_value(key: String) -> JsValue {
} }
} }
/*
/// Call a function on the back-end. /// Call a function on the back-end.
/// Returns null (None) if this fails for any reason. /// Returns null (None) if this fails for any reason.
#[wasm_bindgen] #[wasm_bindgen]
pub async fn call_backend(name: String, parameters: Vec<JsValue>) -> JsValue { pub async fn call_backend(name: String, parameters: Vec<JsValue>) -> JsValue {
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
imports::console_log(&format!( web_sys::console::log_1(&format!(
"call_backend({}, [params; {}])", "call_backend({}, [params; {}])",
name, name,
parameters.len() parameters.len()
)); ).into());
let next_id = increment_id(); let next_id = increment_id();
let mut params = Vec::with_capacity(parameters.len()); let mut params = Vec::with_capacity(parameters.len());
for val in parameters { for val in parameters {
@ -151,7 +158,7 @@ pub async fn call_backend(name: String, parameters: Vec<JsValue>) -> JsValue {
} }
let port = get_port(); let port = get_port();
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
imports::console_log(&format!("USDPL: Got port {}", port)); web_sys::console::log_1(&format!("USDPL: Got port {}", port).into());
let results = connection::send_call( let results = connection::send_call(
next_id, next_id,
Packet::Call(RemoteCall { Packet::Call(RemoteCall {
@ -169,7 +176,7 @@ pub async fn call_backend(name: String, parameters: Vec<JsValue>) -> JsValue {
#[allow(unused_variables)] #[allow(unused_variables)]
Err(e) => { Err(e) => {
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
imports::console_error(&format!("USDPL: Got error while calling {}: {:?}", name, e)); web_sys::console::error_1(&format!("USDPL: Got error while calling {}: {:?}", name, e).into());
return JsValue::NULL; return JsValue::NULL;
} }
}; };
@ -197,7 +204,7 @@ pub async fn init_tr(locale: String) {
{ {
Ok(Packet::Translations(translations)) => { Ok(Packet::Translations(translations)) => {
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
imports::console_log(&format!("USDPL: Got translations for {}", locale)); web_sys::console::log_1(&format!("USDPL: Got translations for {}", locale).into());
// convert translations into map // convert translations into map
let mut tr_map = std::collections::HashMap::with_capacity(translations.len()); let mut tr_map = std::collections::HashMap::with_capacity(translations.len());
for (key, val) in translations { for (key, val) in translations {
@ -207,17 +214,17 @@ pub async fn init_tr(locale: String) {
} }
Ok(_) => { Ok(_) => {
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
imports::console_error(&format!("USDPL: Got wrong packet response for init_tr")); web_sys::console::error_1(&format!("USDPL: Got wrong packet response for init_tr").into());
unsafe { TRANSLATIONS = None } unsafe { TRANSLATIONS = None }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
Err(e) => { Err(e) => {
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
imports::console_error(&format!("USDPL: Got wrong error for init_tr: {:#?}", e)); web_sys::console::error_1(&format!("USDPL: Got wrong error for init_tr: {:#?}", e).into());
unsafe { TRANSLATIONS = None } unsafe { TRANSLATIONS = None }
} }
} }
} }*/
/// Translate a phrase, equivalent to tr_n(msg_id, 0) /// Translate a phrase, equivalent to tr_n(msg_id, 0)
#[wasm_bindgen] #[wasm_bindgen]