From 7fddf3065731d21296c16b401c8468fcbdb704b0 Mon Sep 17 00:00:00 2001 From: Condorra Date: Mon, 30 Sep 2024 16:49:29 +1000 Subject: [PATCH] Add option to turn on logging to IndexedDB + delete logs + download --- Cargo.lock | 142 +++++++++-- Cargo.toml | 5 +- src/logging.rs | 544 +++++++++++++++++++++++++++++++++++++++++ src/lua_engine.rs | 13 + src/lua_engine/muds.rs | 170 ++++++++++++- src/main.rs | 4 + src/websocket.rs | 31 +++ 7 files changed, 889 insertions(+), 20 deletions(-) create mode 100644 src/logging.rs diff --git a/Cargo.lock b/Cargo.lock index b85d377..7a23121 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.73", +] + [[package]] name = "addr2line" version = "0.22.0" @@ -148,6 +160,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.73", +] + [[package]] name = "either" version = "1.13.0" @@ -160,6 +184,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fancy_constructor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.73", +] + [[package]] name = "fnv" version = "1.0.7" @@ -701,6 +737,23 @@ dependencies = [ "syn 2.0.73", ] +[[package]] +name = "indexed_db_futures" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "indexmap" version = "2.3.0" @@ -728,9 +781,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -747,6 +800,53 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.73", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.73", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.73", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1299,6 +1399,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -1333,19 +1443,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -1358,9 +1469,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -1370,9 +1481,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1380,9 +1491,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -1393,9 +1504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" @@ -1424,18 +1535,21 @@ dependencies = [ "console_error_panic_hook", "gc-arena", "im", + "indexed_db_futures", "itertools", "minicrossterm", "nom", "piccolo", "regex", "serde", + "serde-wasm-bindgen 0.6.5", "serde_json", "strip-ansi-escapes", "thiserror", "unicode-segmentation", "unicode-width", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", "yew", ] diff --git a/Cargo.toml b/Cargo.toml index 6a1899c..a8c4a64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ piccolo = { git = "https://github.com/kyren/piccolo.git", rev = "fcbaabc92429217 unicode-segmentation = "1.11.0" unicode-width = "0.1.13" wasm-bindgen = "0.2.92" -web-sys = { version = "0.3.69", features = ["ResizeObserver", "DomRect", "CssStyleDeclaration"] } +web-sys = { version = "0.3.69", features = ["ResizeObserver", "DomRect", "CssStyleDeclaration", "HtmlAnchorElement"] } yew = { version = "0.21.0", features = ["csr"] } minicrossterm = { git = "https://git.blastmud.org/blasthavers/minicrossterm.git", rev = "0c8c6d4f0cf445adf7bb957811081a1b710bd933" } thiserror = "1.0.63" @@ -24,3 +24,6 @@ serde_json = "1.0.127" gc-arena = { git = "https://github.com/kyren/gc-arena.git", rev = "5a7534b883b703f23cfb8c3cfdf033460aa77ea9" } regex = "1.10.6" strip-ansi-escapes = "0.2.0" +indexed_db_futures = "0.5.0" +wasm-bindgen-futures = "0.4.43" +serde-wasm-bindgen = "0.6.5" diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..7d671f9 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,544 @@ +use std::{ + collections::BTreeMap, + mem::replace, + ops::{Deref, DerefMut}, + sync::Arc, +}; + +use indexed_db_futures::{ + idb_object_store::IdbObjectStoreParameters, + request::{IdbOpenDbRequestLike, OpenDbRequest}, + IdbDatabase, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::{ + js_sys::{Array, Uint8Array}, + spawn_local, +}; +use web_sys::{ + console, js_sys::Date, Blob, BlobPropertyBag, DomException, HtmlAnchorElement, IdbKeyRange, + IdbTransactionMode, Url, +}; + +use crate::{echo_to_term_frame, GlobalMemoCell, TermFrame}; + +#[derive(Clone, Debug)] +pub enum QueuedAction { + LogEvent(LogEvent), + ReportOnLogStreams(TermFrame), + DeleteLogs(DeleteLogs), + DownloadLogs(DownloadLogs), +} + +#[derive(Clone, Debug)] +pub struct DeleteLogs { + pub reply_to: TermFrame, + pub stream: String, + pub min_date: String, + pub max_date: String, +} + +#[derive(Clone, Debug)] +pub struct DownloadLogs { + pub reply_to: TermFrame, + pub stream: String, + pub min_date: String, + pub max_date: String, +} + +#[derive(Clone, Debug)] +pub struct LogEvent { + pub log_stream: String, + pub timestamp: Date, + pub message: String, +} + +#[derive(Serialize, Deserialize)] +pub struct StoredLogEvent { + pub stream: String, + pub timestamp: String, + pub message: String, +} + +impl From<&LogEvent> for StoredLogEvent { + fn from(value: &LogEvent) -> Self { + Self { + stream: value.log_stream.clone(), + timestamp: value + .timestamp + .to_iso_string() + .as_string() + .expect("timestamp to_iso_string wasn't string"), + message: value.message.clone(), + } + } +} + +pub enum LoggingEngine { + Uninitialised, + Initialising { backlog: Vec }, + Unavailable, + Ready { db: Arc }, +} + +impl Default for LoggingEngine { + fn default() -> Self { + Self::Uninitialised + } +} + +async fn async_init_logging() -> Result { + let mut db_req: OpenDbRequest = IdbDatabase::open_u32("logging", 1)?; + db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + if evt + .db() + .object_store_names() + .find(|n| n == "logs") + .is_none() + { + let store = evt.db().create_object_store_with_params( + "logs", + ::default().auto_increment(true), + )?; + store.create_index( + "by_stream_timestamp", + &IdbKeyPath::str_sequence(&["stream", "timestamp"]), + )?; + } + Ok(()) + })); + let db: IdbDatabase = db_req.await?; + + Ok(LoggingEngine::Ready { db: db.into() }) +} + +fn logging_broken_message(global: &GlobalMemoCell, frame: &TermFrame) { + let _ = echo_to_term_frame(global, frame, "Couldn't enable logging. This sometimes happens if your browser doesn't enable indexed storage, or is in a mode (e.g. private browsing) where such storage is not permitted.\r\n"); +} + +fn init_logging(global: &GlobalMemoCell) { + let global: GlobalMemoCell = global.clone(); + spawn_local(async move { + match async_init_logging().await { + Ok(new_engine) => { + let old_engine = replace(global.log_engine.borrow_mut().deref_mut(), new_engine); + if let LoggingEngine::Initialising { backlog } = old_engine { + for action in backlog.into_iter() { + match action { + QueuedAction::LogEvent(ev) => queue_immediate_log(&global, ev), + QueuedAction::ReportOnLogStreams(frame) => { + immediate_report_log_frame(&global, frame); + } + QueuedAction::DeleteLogs(act) => { + immediate_delete_logs(&global, act); + } + QueuedAction::DownloadLogs(act) => { + immediate_download_logs(&global, act); + } + } + } + } + } + Err(_) => { + if let LoggingEngine::Initialising { .. } = global.log_engine.borrow().deref() { + logging_broken_message(&global, &TermFrame(1)); + } + } + } + }) +} + +fn queue_immediate_log_refutable(db: &IdbDatabase, event: &LogEvent) -> Result<(), String> { + let trans = db + .transaction_on_one_with_mode("logs", IdbTransactionMode::Readwrite) + .map_err(|e| e.message())?; + let store = trans.object_store("logs").map_err(|e| e.message())?; + store + .put_val( + &serde_wasm_bindgen::to_value::(&event.into()) + .map_err(|_| "Can't serialise event")?, + ) + .map_err(|e| e.message())?; + Ok(()) +} + +fn queue_immediate_log(global: &GlobalMemoCell, event: LogEvent) { + if let LoggingEngine::Ready { db } = global.log_engine.borrow().deref() { + match queue_immediate_log_refutable(db, &event) { + Ok(()) => {} + Err(e) => { + console::log_2( + &JsValue::from_str("Error writing event to IndexedDb"), + &JsValue::from(e), + ); + } + } + } +} + +async fn immediate_report_log_frame_refutable( + global: GlobalMemoCell, + db: &IdbDatabase, + frame: TermFrame, +) -> Result<(), DomException> { + let trans = db.transaction_on_one("logs")?; + let store = trans.object_store("logs")?; + let curs = store.open_cursor()?.await?; + let curs = match curs { + None => { + let _ = echo_to_term_frame(&global, &frame, "No logs are currently stored.\r\n"); + return Ok(()); + } + Some(curs) => curs, + }; + let mut event_stats = BTreeMap::::new(); + loop { + if let Ok(ev) = serde_wasm_bindgen::from_value::(curs.value()) { + event_stats + .entry(ev.stream) + .and_modify(|v| *v += 1) + .or_insert(1); + }; + if !curs.continue_cursor()?.await? { + break; + } + } + let msg = event_stats + .iter() + .map(|(k, v)| format!("Log stream {} has {} log records.\r\n", k, v)) + .join(""); + let _ = echo_to_term_frame(&global, &frame, &msg); + Ok(()) +} + +async fn immediate_report_log_frame_async( + global: GlobalMemoCell, + db: Arc, + frame: TermFrame, +) { + match immediate_report_log_frame_refutable(global, &db, frame).await { + Ok(()) => {} + Err(e) => { + console::log_2( + &JsValue::from_str("Error reading log stats from IndexedDb"), + &JsValue::from(e), + ); + } + } +} + +fn immediate_report_log_frame(global: &GlobalMemoCell, frame: TermFrame) { + let log_engine = global.log_engine.borrow(); + if let LoggingEngine::Ready { db } = log_engine.deref() { + spawn_local(immediate_report_log_frame_async( + global.clone(), + db.clone(), + frame, + )); + } +} + +async fn immediate_delete_logs_refutable( + global: GlobalMemoCell, + db: &IdbDatabase, + act: DeleteLogs, +) -> Result<(), DomException> { + let trans = db.transaction_on_one_with_mode("logs", IdbTransactionMode::Readwrite)?; + let store = trans.object_store("logs")?; + let idx = store.index("by_stream_timestamp")?; + let range = IdbKeyRange::bound( + &JsValue::from( + [ + JsValue::from_str(&act.stream), + JsValue::from_str(&act.min_date), + ] + .iter() + .collect::(), + ), + &JsValue::from( + [ + JsValue::from_str(&act.stream), + JsValue::from_str(&act.max_date), + ] + .iter() + .collect::(), + ), + )?; + let cur = match idx.open_key_cursor_with_range(&range)?.await? { + None => { + let _ = echo_to_term_frame( + &global, + &act.reply_to, + "No logs matched; no logs deleted.\r\n", + ); + return Ok(()); + } + Some(cur) => cur, + }; + let mut records: u64 = 0; + loop { + if let Some(pk) = cur.primary_key() { + store.delete(&pk)?; + records += 1; + } + if !cur.continue_cursor()?.await? { + break; + } + } + let _ = echo_to_term_frame( + &global, + &act.reply_to, + &format!("{} logs matched and were deleted.\r\n", records), + ); + Ok(()) +} + +async fn immediate_delete_logs_async( + global: GlobalMemoCell, + db: Arc, + act: DeleteLogs, +) { + match immediate_delete_logs_refutable(global, &db, act).await { + Ok(()) => {} + Err(e) => { + console::log_2( + &JsValue::from_str("Error deleting logs from IndexedDb"), + &JsValue::from(e), + ); + } + } +} + +fn immediate_delete_logs(global: &GlobalMemoCell, act: DeleteLogs) { + let log_engine = global.log_engine.borrow(); + if let LoggingEngine::Ready { db } = log_engine.deref() { + spawn_local(immediate_delete_logs_async( + global.clone(), + db.clone(), + act.clone(), + )); + } +} + +async fn immediate_download_logs_refutable( + global: GlobalMemoCell, + db: &IdbDatabase, + act: DownloadLogs, +) -> Result<(), DomException> { + let trans = db.transaction_on_one_with_mode("logs", IdbTransactionMode::Readwrite)?; + let store = trans.object_store("logs")?; + let idx = store.index("by_stream_timestamp")?; + let range = IdbKeyRange::bound( + &JsValue::from( + [ + JsValue::from_str(&act.stream), + JsValue::from_str(&act.min_date), + ] + .iter() + .collect::(), + ), + &JsValue::from( + [ + JsValue::from_str(&act.stream), + JsValue::from_str(&act.max_date), + ] + .iter() + .collect::(), + ), + )?; + let cur = match idx.open_key_cursor_with_range(&range)?.await? { + None => { + let _ = echo_to_term_frame( + &global, + &act.reply_to, + "No logs matched; no logs downloaded.\r\n", + ); + return Ok(()); + } + Some(cur) => cur, + }; + let mut buf: String = String::new(); + loop { + if let Some(k) = cur.primary_key() { + if let Some(Ok(rec)) = store + .get(&k)? + .await? + .map(serde_wasm_bindgen::from_value::) + { + buf.push_str(&rec.message); + } + } + if !cur.continue_cursor()?.await? { + break; + } + } + let buf_bytes = buf.as_bytes(); + let buf_array = Uint8Array::new_with_length(buf_bytes.len() as u32); + buf_array.copy_from(buf_bytes); + let mut blobprops: BlobPropertyBag = Default::default(); + blobprops.type_("text/plain"); + let blob = Blob::new_with_u8_array_sequence_and_options( + &[&buf_array].into_iter().collect::(), + &blobprops, + )?; + console::log_2(&buf_array, &blob); + let url = Url::create_object_url_with_blob(&blob)?; + if let Some((doc, body)) = web_sys::window() + .and_then(|w| w.document()) + .and_then(|d| d.body().map(|b| (d, b))) + { + let el_anchor = JsCast::unchecked_into::(doc.create_element("a")?); + body.append_child(&el_anchor)?; + el_anchor.set_href(&url); + el_anchor.set_download(&format!("{}-logs.txt", act.stream)); + el_anchor.click(); + body.remove_child(&el_anchor)?; + } + // Url::revoke_object_url(&url)?; + Ok(()) +} + +async fn immediate_download_logs_async( + global: GlobalMemoCell, + db: Arc, + act: DownloadLogs, +) { + match immediate_download_logs_refutable(global, &db, act).await { + Ok(()) => {} + Err(e) => { + console::log_2( + &JsValue::from_str("Error downloading logs from IndexedDb"), + &JsValue::from(e), + ); + } + } +} + +fn immediate_download_logs(global: &GlobalMemoCell, act: DownloadLogs) { + let log_engine = global.log_engine.borrow(); + if let LoggingEngine::Ready { db } = log_engine.deref() { + spawn_local(immediate_download_logs_async( + global.clone(), + db.clone(), + act.clone(), + )); + } +} + +pub fn log(global: &GlobalMemoCell, event: &LogEvent) { + let mut engine_borrow = global.log_engine.borrow_mut(); + match engine_borrow.deref_mut() { + LoggingEngine::Uninitialised => { + *engine_borrow = LoggingEngine::Initialising { + backlog: vec![QueuedAction::LogEvent(event.clone())], + }; + drop(engine_borrow); + init_logging(global); + } + LoggingEngine::Initialising { ref mut backlog } => { + backlog.push(QueuedAction::LogEvent(event.clone())); + } + LoggingEngine::Unavailable => { + // Assume the user has already been informed it failed, so do nothing. + } + LoggingEngine::Ready { .. } => { + drop(engine_borrow); + queue_immediate_log(global, event.clone()); + } + } +} + +pub fn start_listing_logs(global: &GlobalMemoCell, send_to: &TermFrame) { + let mut engine_borrow = global.log_engine.borrow_mut(); + match engine_borrow.deref_mut() { + LoggingEngine::Uninitialised => { + *engine_borrow = LoggingEngine::Initialising { + backlog: vec![QueuedAction::ReportOnLogStreams(send_to.clone())], + }; + drop(engine_borrow); + init_logging(global); + } + LoggingEngine::Initialising { ref mut backlog } => { + backlog.push(QueuedAction::ReportOnLogStreams(send_to.clone())); + } + LoggingEngine::Unavailable => { + // Assume the user has already been informed it failed, so do nothing. + } + LoggingEngine::Ready { .. } => { + drop(engine_borrow); + immediate_report_log_frame(global, send_to.clone()); + } + } +} + +pub fn start_deleting_logs( + global: &GlobalMemoCell, + reply_to: &TermFrame, + stream: &str, + min_date: &str, + max_date: &str, +) { + let mut engine_borrow = global.log_engine.borrow_mut(); + let act = DeleteLogs { + reply_to: reply_to.clone(), + stream: stream.to_owned(), + min_date: min_date.to_owned(), + max_date: max_date.to_owned(), + }; + match engine_borrow.deref_mut() { + LoggingEngine::Uninitialised => { + *engine_borrow = LoggingEngine::Initialising { + backlog: vec![QueuedAction::DeleteLogs(act)], + }; + drop(engine_borrow); + init_logging(global); + } + LoggingEngine::Initialising { ref mut backlog } => { + backlog.push(QueuedAction::DeleteLogs(act)); + } + LoggingEngine::Unavailable => { + // Assume the user has already been informed it failed, so do nothing. + } + LoggingEngine::Ready { .. } => { + drop(engine_borrow); + immediate_delete_logs(global, act); + } + } +} + +pub fn start_downloading_logs( + global: &GlobalMemoCell, + reply_to: &TermFrame, + stream: &str, + min_date: &str, + max_date: &str, +) { + let mut engine_borrow = global.log_engine.borrow_mut(); + let act = DownloadLogs { + reply_to: reply_to.clone(), + stream: stream.to_owned(), + min_date: min_date.to_owned(), + max_date: max_date.to_owned(), + }; + match engine_borrow.deref_mut() { + LoggingEngine::Uninitialised => { + *engine_borrow = LoggingEngine::Initialising { + backlog: vec![QueuedAction::DownloadLogs(act)], + }; + drop(engine_borrow); + init_logging(global); + } + LoggingEngine::Initialising { ref mut backlog } => { + backlog.push(QueuedAction::DownloadLogs(act)); + } + LoggingEngine::Unavailable => { + // Assume the user has already been informed it failed, so do nothing. + } + LoggingEngine::Ready { .. } => { + drop(engine_borrow); + immediate_download_logs(global, act); + } + } +} diff --git a/src/lua_engine.rs b/src/lua_engine.rs index d28dcf7..a876a7c 100644 --- a/src/lua_engine.rs +++ b/src/lua_engine.rs @@ -147,6 +147,15 @@ pub fn install_lua_globals( ) .map_err(|_| Error::msg("Can't add command"))?; }; + ($sym: ident, $name: literal) => { + cmd_table + .set( + ctx, + ctx.intern_static($name.as_bytes()), + $sym(ctx, &global_memo, &global_layout), + ) + .map_err(|_| Error::msg("Can't add command"))?; + }; } macro_rules! register_stateless_command { ($sym: ident) => { @@ -171,11 +180,15 @@ pub fn install_lua_globals( register_command!(close_mud); register_command!(connect_mud); register_stateless_command!(create_match_table); + register_command!(cmd_delete_logs, "deletelogs"); register_command!(delete_mud); + register_command!(cmd_download_logs, "downloadlogs"); register_command!(echo); register_command!(echo_frame); register_command!(echo_frame_raw); register_command!(hsplit); + register_command!(cmd_list_logs, "listlogs"); + register_command!(mud_log, "log"); register_command!(panel_merge); register_command!(sendmud_raw); register_stateless_command!(mud_trigger, "untrigger"); diff --git a/src/lua_engine/muds.rs b/src/lua_engine/muds.rs index 62601f2..5a6a47b 100644 --- a/src/lua_engine/muds.rs +++ b/src/lua_engine/muds.rs @@ -11,6 +11,7 @@ use yew::UseStateSetter; use crate::{ command_handler::execute_queue, id_intern::{intern_id, unintern_id}, + logging::{start_deleting_logs, start_downloading_logs, start_listing_logs}, match_table::{create_match_table, match_table_add, match_table_remove}, telnet::{parse_telnet_buf, TelnetOutput}, websocket::{connect_websocket, send_message_to_mud, WebSocketId}, @@ -148,7 +149,7 @@ pub(super) fn connect_mud<'gc>( } break v; }; - let name: Value<'gc> = ctx.intern(&name.as_bytes()).into(); + let name: Value<'gc> = ctx.intern(name.as_bytes()).into(); let muds: Table = ctx.get_global("muds")?; if !muds.get_value(ctx, name).is_nil() { @@ -392,10 +393,6 @@ pub(super) fn mudoutput_line<'gc>( .unwrap_or_else(|| Ok(ctx.get_global::("frames")?.get(ctx, 1_i64)?))? .get(ctx, "frame")?; - console::log_1(&JsValue::from_str(&format!( - "Line to match: {:?}", - line.as_bytes() - ))); let seq = async_sequence(&ctx, |locals, mut seq| { let frameroutes: Vec = frameroutes .iter() @@ -635,3 +632,166 @@ pub(super) fn mud_untrigger(ctx: Context<'_>) -> Callback<'_> { Ok(piccolo::CallbackReturn::Sequence(seq)) }) } + +pub(super) fn mud_log<'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 name: String = stack.from_front(ctx)?; + + let muds: Table = ctx.get_global("muds")?; + let mud_value: Value = muds.get(ctx, name)?; + if mud_value.is_nil() { + Err(Error::msg( + "Attempt to delete MUD connection that wasn't found", + ))? + } + + let log_dest: String = stack.from_back(ctx)?; + let log_dest = if log_dest == "none" || log_dest == "off" { + None + } else { + Some(log_dest) + }; + + let socket_id = try_unwrap_socketid(ctx, &mud_value)?; + match global_memo.ws_registry.borrow_mut().get_mut(&socket_id) { + None => Err(Error::msg("That MUD connection doesn't exist."))?, + Some(v) => { + v.log_dest = log_dest; + } + } + + Ok(CallbackReturn::Return) + }) +} + +pub(super) fn cmd_list_logs<'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, _stack| { + let frame = try_unwrap_frame( + ctx, + &ctx.get_global::
("info")? + .get::<&str, Value>(ctx, "current_frame")?, + )?; + start_listing_logs(&global_memo, &frame); + Ok(CallbackReturn::Return) + }) +} + +pub(super) fn cmd_delete_logs<'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 frame = try_unwrap_frame( + ctx, + &ctx.get_global::
("info")? + .get::<&str, Value>(ctx, "current_frame")?, + )?; + let stream = String::from_utf8( + piccolo::String::from_value( + ctx, + stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("No stream name argument given"))?, + )? + .as_bytes() + .to_vec(), + )?; + let (min_date, max_date) = match stack.len() { + 2 => { + let min_date = String::from_utf8( + piccolo::String::from_value(ctx, stack.pop_front().unwrap())? + .as_bytes() + .to_vec(), + )?; + let max_date = String::from_utf8( + piccolo::String::from_value(ctx, stack.pop_front().unwrap())? + .as_bytes() + .to_vec(), + )?; + (min_date, max_date) + } + 1 => { + let max_date = String::from_utf8( + piccolo::String::from_value(ctx, stack.pop_front().unwrap())? + .as_bytes() + .to_vec(), + )?; + ("0000-00-00".to_owned(), max_date) + } + 0 => ("0000-00-00".to_owned(), "9999-12-31".to_owned()), + _ => Err(anyhow::Error::msg( + "At most three arguments expected: stream min-date max-date", + ))?, + }; + + start_deleting_logs(&global_memo, &frame, &stream, &min_date, &max_date); + Ok(CallbackReturn::Return) + }) +} + +pub(super) fn cmd_download_logs<'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 frame = try_unwrap_frame( + ctx, + &ctx.get_global::
("info")? + .get::<&str, Value>(ctx, "current_frame")?, + )?; + let stream = String::from_utf8( + piccolo::String::from_value( + ctx, + stack + .pop_front() + .ok_or_else(|| anyhow::Error::msg("No stream name argument given"))?, + )? + .as_bytes() + .to_vec(), + )?; + let (min_date, max_date) = match stack.len() { + 2 => { + let min_date = String::from_utf8( + piccolo::String::from_value(ctx, stack.pop_front().unwrap())? + .as_bytes() + .to_vec(), + )?; + let max_date = String::from_utf8( + piccolo::String::from_value(ctx, stack.pop_front().unwrap())? + .as_bytes() + .to_vec(), + )?; + (min_date, max_date) + } + 1 => { + let max_date = String::from_utf8( + piccolo::String::from_value(ctx, stack.pop_front().unwrap())? + .as_bytes() + .to_vec(), + )?; + ("0000-00-00".to_owned(), max_date) + } + 0 => ("0000-00-00".to_owned(), "9999-12-31".to_owned()), + _ => Err(anyhow::Error::msg( + "At most three arguments expected: stream min-date max-date", + ))?, + }; + + start_downloading_logs(&global_memo, &frame, &stream, &min_date, &max_date); + Ok(CallbackReturn::Return) + }) +} diff --git a/src/main.rs b/src/main.rs index 099e7a3..890c26c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc; +use logging::LoggingEngine; use parsing::ParsedCommand; use term_split::TermSplit; use yew::prelude::*; @@ -9,6 +10,7 @@ use yew::prelude::*; pub mod command_handler; pub mod id_intern; pub mod lineengine; +pub mod logging; pub mod lua_engine; pub mod match_table; pub mod parsing; @@ -29,6 +31,7 @@ pub struct GlobalMemoState { lua_engine: RefCell, ws_registry: RefCell, command_queue: RefCell>, + log_engine: 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 @@ -72,6 +75,7 @@ fn app() -> Html { command_queue: VecDeque::new().into(), lua_engine: LuaState::setup().expect("Can create interpreter").into(), layout: RefCell::new((*global_layout).clone()), + log_engine: RefCell::new(Default::default()), }); use_memo((), |_| { install_lua_globals(&global_memo, global_layout.setter()) diff --git a/src/websocket.rs b/src/websocket.rs index 4ec44d5..ad8334d 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use gc_arena::Collect; use serde::Deserialize; use wasm_bindgen::{closure::Closure, JsCast, JsValue}; +use wasm_bindgen_futures::js_sys::Date; use web_sys::{ console, js_sys::{ArrayBuffer, JsString, Uint8Array}, @@ -10,6 +11,7 @@ use web_sys::{ }; use crate::{ + logging::{self, LogEvent}, lua_engine::muds::{handle_websocket_close_log_err, handle_websocket_output_log_err}, GlobalMemoCell, }; @@ -23,6 +25,7 @@ pub struct WebSocketData { pub closed: bool, pub url: String, pub retained_closures: Option<(Closure, Closure)>, + pub log_dest: Option, } pub type RegisteredWebSockets = BTreeMap; @@ -52,6 +55,7 @@ pub fn connect_websocket( closed: false, url: url.to_owned(), retained_closures: None, + log_dest: None, }; data.connection.set_binary_type(BinaryType::Arraybuffer); @@ -63,6 +67,23 @@ pub fn connect_websocket( let data_closure: Closure = Closure::new(move |ev: MessageEvent| { let data = ev.data(); if data.has_type::() { + let log_dest = data_globals + .ws_registry + .borrow() + .get(&data_new_id) + .and_then(|d| d.log_dest.clone()); + if let Some(log_dest) = &log_dest { + let str_rendering = + String::from_utf8_lossy(&Uint8Array::new(&data).to_vec()).to_string(); + logging::log( + &data_globals, + &LogEvent { + log_stream: log_dest.clone(), + timestamp: Date::new_0(), + message: format!("R {}", &str_rendering), + }, + ); + } handle_websocket_output_log_err( &data_new_id, &data_globals, @@ -126,6 +147,16 @@ pub fn send_message_to_mud( 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) => { + if let Some(log_dest) = &sock_data.log_dest { + logging::log( + global, + &LogEvent { + log_stream: log_dest.clone(), + timestamp: Date::new_0(), + message: format!("W {}", String::from_utf8_lossy(msg)), + }, + ); + } sock_data.connection.send_with_u8_array(msg).map_err(|e| { e.dyn_into::() .map(|e| anyhow::Error::msg(e.message()))