Add option to turn on logging to IndexedDB + delete logs + download

This commit is contained in:
Condorra 2024-09-30 16:49:29 +10:00
parent a8c279e13e
commit 7fddf30657
7 changed files with 889 additions and 20 deletions

142
Cargo.lock generated
View File

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

View File

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

544
src/logging.rs Normal file
View File

@ -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<QueuedAction> },
Unavailable,
Ready { db: Arc<IdbDatabase> },
}
impl Default for LoggingEngine {
fn default() -> Self {
Self::Uninitialised
}
}
async fn async_init_logging() -> Result<LoggingEngine, DomException> {
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",
<IdbObjectStoreParameters as Default>::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::<StoredLogEvent>(&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::<String, u64>::new();
loop {
if let Ok(ev) = serde_wasm_bindgen::from_value::<StoredLogEvent>(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<IdbDatabase>,
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::<Array>(),
),
&JsValue::from(
[
JsValue::from_str(&act.stream),
JsValue::from_str(&act.max_date),
]
.iter()
.collect::<Array>(),
),
)?;
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<IdbDatabase>,
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::<Array>(),
),
&JsValue::from(
[
JsValue::from_str(&act.stream),
JsValue::from_str(&act.max_date),
]
.iter()
.collect::<Array>(),
),
)?;
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::<StoredLogEvent>)
{
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::<Array>(),
&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::<HtmlAnchorElement>(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<IdbDatabase>,
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);
}
}
}

View File

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

View File

@ -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::<Table>("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<StashedTable> = 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<GlobalLayoutCell>,
) -> 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<GlobalLayoutCell>,
) -> 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::<Table>("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<GlobalLayoutCell>,
) -> 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::<Table>("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<GlobalLayoutCell>,
) -> 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::<Table>("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)
})
}

View File

@ -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<LuaState>,
ws_registry: RefCell<RegisteredWebSockets>,
command_queue: RefCell<VecDeque<(TermFrame, ParsedCommand)>>,
log_engine: RefCell<LoggingEngine>,
// 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())

View File

@ -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<dyn FnMut(MessageEvent)>, Closure<dyn FnMut()>)>,
pub log_dest: Option<String>,
}
pub type RegisteredWebSockets = BTreeMap<WebSocketId, WebSocketData>;
@ -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<dyn FnMut(MessageEvent)> = Closure::new(move |ev: MessageEvent| {
let data = ev.data();
if data.has_type::<ArrayBuffer>() {
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::<DomException>()
.map(|e| anyhow::Error::msg(e.message()))