diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 1398559f3..09639cab5 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -5489,3 +5489,72 @@ test('Paste should not work unless an input is focused', async ({ ) ).toContain(pasteContent) }) + +test('Core dump from keyboard commands success', async ({ page }) => { + // This test can run long if it takes a little too long to load + // the engine, plus coredump takes bit to process. + test.setTimeout(150000) + const u = await getUtils(page) + await page.addInitScript(async () => { + ;(window as any).playwrightSkipFilePicker = true + localStorage.setItem( + 'persistCode', + `const topAng = 25 +const bottomAng = 35 +const baseLen = 3.5 +const baseHeight = 1 +const totalHeightHalf = 2 +const armThick = 0.5 +const totalLen = 9.5 +const part001 = startSketchOn('-XZ') + |> startProfileAt([0, 0], %) + |> yLine(baseHeight, %) + |> xLine(baseLen, %) + |> angledLineToY({ + angle: topAng, + to: totalHeightHalf, + }, %, 'seg04') + |> xLineTo(totalLen, %, 'seg03') + |> yLine(-armThick, %, 'seg01') + |> angledLineThatIntersects({ + angle: HALF_TURN, + offset: -armThick, + intersectTag: 'seg04' + }, %) + |> angledLineToY([segAng('seg04', %) + 180, ZERO], %) + |> angledLineToY({ + angle: -bottomAng, + to: -totalHeightHalf - armThick, + }, %, 'seg02') + |> xLineTo(segEndX('seg03', %) + 0, %) + |> yLine(-segLen('seg01', %), %) + |> angledLineThatIntersects({ + angle: HALF_TURN, + offset: -armThick, + intersectTag: 'seg02' + }, %) + |> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %) + |> xLineTo(ZERO, %) + |> close(%) + |> extrude(4, %)` + ) + }) + await page.setViewportSize({ width: 1200, height: 500 }) + await page.goto('/') + await u.waitForAuthSkipAppStart() + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup') + await page.keyboard.press('Meta+Shift+.') + // after invoking coredump, a loading toast will appear + await expect(page.getByText('Starting core dump')).toBeVisible() + // Allow time for core dump processing + await page.waitForTimeout(1000) + await expect(page.getByText('Core dump completed successfully')).toBeVisible() + const popup = await popupPromise + console.log(await popup.title()) + // GitHub popup will go to unlogged in page. Can't look for "New Issue" here. + await expect(popup).toHaveTitle(/GitHub /) +}) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b66fd1e5d..e5b832b1c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6181,6 +6181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" dependencies = [ "chrono", + "serde_json", "thiserror", "ts-rs-macros", "url", diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index df7c5cced..ee33a4021 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -121,7 +121,24 @@ export const ModelingMachineProvider = ({ htmlRef, token ) - useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true)) + useHotkeyWrapper(['meta + shift + .'], () => { + console.warn('CoreDump: Initializing core dump') + toast.promise( + coreDump(coreDumpManager, true), + { + loading: 'Starting core dump...', + success: 'Core dump completed successfully', + error: 'Error while exporting core dump', + }, + { + success: { + // Note: this extended duration is especially important for Playwright e2e testing + // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations + duration: 6000, + }, + } + ) + }) // Settings machine setup // const retrievedSettings = useRef( diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 8aa05b04c..a66746b12 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -25,7 +25,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program' import type { Token } from '../wasm-lib/kcl/bindings/Token' import { Coords2d } from './std/sketch' import { fileSystemManager } from 'lang/std/fileSystemManager' -import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo' +import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo' import { CoreDumpManager } from 'lib/coredump' import openWindow from 'lib/openWindow' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' @@ -335,14 +335,27 @@ export function programMemoryInit(): ProgramMemory { export async function coreDump( coreDumpManager: CoreDumpManager, openGithubIssue: boolean = false -): Promise { +): Promise { try { - const dump: AppInfo = await coredump(coreDumpManager) + const dump: CoreDumpInfo = await coredump(coreDumpManager) + /* NOTE: this console output of the coredump should include the field + `github_issue_url` which is not in the uploaded coredump file. + `github_issue_url` is added after the file is uploaded + and is only needed for the openWindow operation which creates + a new GitHub issue for the user. + */ if (openGithubIssue && dump.github_issue_url) { openWindow(dump.github_issue_url) + } else { + console.error( + 'github_issue_url undefined. Unable to create GitHub issue for coredump.' + ) } + console.log('CoreDump: final coredump', dump) + console.log('CoreDump: final coredump JSON', JSON.stringify(dump)) return dump } catch (e: any) { + console.error('CoreDump: error', e) throw new Error(`Error getting core dump: ${e}`) } } diff --git a/src/lib/coredump.ts b/src/lib/coredump.ts index fbf6e263d..93b1cdfdb 100644 --- a/src/lib/coredump.ts +++ b/src/lib/coredump.ts @@ -13,8 +13,15 @@ import screenshot from 'lib/screenshot' import React from 'react' import { VITE_KC_API_BASE_URL } from 'env' -// This is a class for getting all the values from the JS world to pass to the Rust world -// for a core dump. +/** + * CoreDumpManager module + * - for getting all the values from the JS world to pass to the Rust world for a core dump. + * @module lib/coredump + * @class + */ +// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts +// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows +// them to so the toast handler in ModelingMachineProvider can show the user an error message toast export class CoreDumpManager { engineCommandManager: EngineCommandManager htmlRef: React.RefObject | null @@ -144,6 +151,293 @@ export class CoreDumpManager { }) } + // Currently just a placeholder to begin loading singleton and xstate data into + getClientState(): Promise { + /** + * Deep clone a JavaScript Object + * - NOTE: this function throws on parse errors from things like circular references + * - It is also synchronous and could be more performant + * - There is a whole rabbit hole to explore here if you like. + * - This works for our use case. + * @param {object} obj - The object to clone. + */ + const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj)) + + /** + * Check if a function is private method + */ + const isPrivateMethod = (key: string) => { + return key.length && key[0] === '_' + } + + // Turn off verbose logging by default + const verboseLogging = false + + /** + * Toggle verbose debug logging of step-by-step client state coredump data + */ + const debugLog = verboseLogging ? console.log : () => {} + + console.warn('CoreDump: Gathering client state') + + // Initialize the clientState object + let clientState = { + // singletons + engine_command_manager: { + artifact_map: {}, + command_logs: [], + engine_connection: { state: { type: '' } }, + default_planes: {}, + scene_command_artifacts: {}, + }, + kcl_manager: { + ast: {}, + kcl_errors: [], + }, + scene_infra: {}, + scene_entities_manager: {}, + editor_manager: {}, + // xstate + auth_machine: {}, + command_bar_machine: {}, + file_machine: {}, + home_machine: {}, + modeling_machine: {}, + settings_machine: {}, + } + debugLog('CoreDump: initialized clientState', clientState) + debugLog('CoreDump: globalThis.window', globalThis.window) + + try { + // Singletons + + // engine_command_manager + debugLog('CoreDump: engineCommandManager', this.engineCommandManager) + + // artifact map - this.engineCommandManager.artifactMap + if (this.engineCommandManager?.artifactMap) { + debugLog( + 'CoreDump: Engine Command Manager artifact map', + this.engineCommandManager.artifactMap + ) + clientState.engine_command_manager.artifact_map = deepClone( + this.engineCommandManager.artifactMap + ) + } + + // command logs - this.engineCommandManager.commandLogs + if (this.engineCommandManager?.commandLogs) { + debugLog( + 'CoreDump: Engine Command Manager command logs', + this.engineCommandManager.commandLogs + ) + clientState.engine_command_manager.command_logs = deepClone( + this.engineCommandManager.commandLogs + ) + } + + // default planes - this.engineCommandManager.defaultPlanes + if (this.engineCommandManager?.defaultPlanes) { + debugLog( + 'CoreDump: Engine Command Manager default planes', + this.engineCommandManager.defaultPlanes + ) + clientState.engine_command_manager.default_planes = deepClone( + this.engineCommandManager.defaultPlanes + ) + } + + // engine connection state + if (this.engineCommandManager?.engineConnection?.state) { + debugLog( + 'CoreDump: Engine Command Manager engine connection state', + this.engineCommandManager.engineConnection.state + ) + clientState.engine_command_manager.engine_connection.state = + this.engineCommandManager.engineConnection.state + } + + // in sequence - this.engineCommandManager.inSequence + if (this.engineCommandManager?.inSequence) { + debugLog( + 'CoreDump: Engine Command Manager in sequence', + this.engineCommandManager.inSequence + ) + ;(clientState.engine_command_manager as any).in_sequence = + this.engineCommandManager.inSequence + } + + // out sequence - this.engineCommandManager.outSequence + if (this.engineCommandManager?.outSequence) { + debugLog( + 'CoreDump: Engine Command Manager out sequence', + this.engineCommandManager.outSequence + ) + ;(clientState.engine_command_manager as any).out_sequence = + this.engineCommandManager.outSequence + } + + // scene command artifacts - this.engineCommandManager.sceneCommandArtifacts + if (this.engineCommandManager?.sceneCommandArtifacts) { + debugLog( + 'CoreDump: Engine Command Manager scene command artifacts', + this.engineCommandManager.sceneCommandArtifacts + ) + clientState.engine_command_manager.scene_command_artifacts = deepClone( + this.engineCommandManager.sceneCommandArtifacts + ) + } + + // KCL Manager - globalThis?.window?.kclManager + const kclManager = (globalThis?.window as any)?.kclManager + debugLog('CoreDump: kclManager', kclManager) + + if (kclManager) { + // KCL Manager AST + debugLog('CoreDump: KCL Manager AST', kclManager?.ast) + if (kclManager?.ast) { + clientState.kcl_manager.ast = deepClone(kclManager.ast) + } + + // KCL Errors + debugLog('CoreDump: KCL Errors', kclManager?.kclErrors) + if (kclManager?.kclErrors) { + clientState.kcl_manager.kcl_errors = deepClone(kclManager.kclErrors) + } + + // KCL isExecuting + debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting) + if (kclManager?.isExecuting) { + ;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting + } + + // KCL logs + debugLog('CoreDump: KCL logs', kclManager?.logs) + if (kclManager?.logs) { + ;(clientState.kcl_manager as any).logs = deepClone(kclManager.logs) + } + + // KCL programMemory + debugLog('CoreDump: KCL programMemory', kclManager?.programMemory) + if (kclManager?.programMemory) { + ;(clientState.kcl_manager as any).programMemory = deepClone( + kclManager.programMemory + ) + } + + // KCL wasmInitFailed + debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed) + if (kclManager?.wasmInitFailed) { + ;(clientState.kcl_manager as any).wasmInitFailed = + kclManager.wasmInitFailed + } + } + + // Scene Infra - globalThis?.window?.sceneInfra + const sceneInfra = (globalThis?.window as any)?.sceneInfra + debugLog('CoreDump: Scene Infra', sceneInfra) + + if (sceneInfra) { + const sceneInfraSkipKeys = ['camControls'] + const sceneInfraKeys = Object.keys(sceneInfra) + .sort() + .filter((entry) => { + return ( + typeof sceneInfra[entry] !== 'function' && + !sceneInfraSkipKeys.includes(entry) + ) + }) + + debugLog('CoreDump: Scene Infra keys', sceneInfraKeys) + sceneInfraKeys.forEach((key: string) => { + debugLog('CoreDump: Scene Infra', key, sceneInfra[key]) + try { + ;(clientState.scene_infra as any)[key] = sceneInfra[key] + } catch (error) { + console.error( + 'CoreDump: unable to parse Scene Infra ' + key + ' data due to ', + error + ) + } + }) + } + + // Scene Entities Manager - globalThis?.window?.sceneEntitiesManager + const sceneEntitiesManager = (globalThis?.window as any) + ?.sceneEntitiesManager + debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager) + + if (sceneEntitiesManager) { + // Scene Entities Manager active segments + debugLog( + 'CoreDump: Scene Entities Manager active segments', + sceneEntitiesManager?.activeSegments + ) + if (sceneEntitiesManager?.activeSegments) { + ;(clientState.scene_entities_manager as any).activeSegments = + deepClone(sceneEntitiesManager.activeSegments) + } + } + + // Editor Manager - globalThis?.window?.editorManager + const editorManager = (globalThis?.window as any)?.editorManager + debugLog('CoreDump: editorManager', editorManager) + + if (editorManager) { + const editorManagerSkipKeys = ['camControls'] + const editorManagerKeys = Object.keys(editorManager) + .sort() + .filter((entry) => { + return ( + typeof editorManager[entry] !== 'function' && + !isPrivateMethod(entry) && + !editorManagerSkipKeys.includes(entry) + ) + }) + + debugLog('CoreDump: Editor Manager keys', editorManagerKeys) + editorManagerKeys.forEach((key: string) => { + debugLog('CoreDump: Editor Manager', key, editorManager[key]) + try { + ;(clientState.editor_manager as any)[key] = deepClone( + editorManager[key] + ) + } catch (error) { + console.error( + 'CoreDump: unable to parse Editor Manager ' + + key + + ' data due to ', + error + ) + } + }) + } + + // enableMousePositionLogs - Not coredumped + // See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998 + debugLog( + 'CoreDump: enableMousePositionLogs [not coredumped]', + (globalThis?.window as any)?.enableMousePositionLogs + ) + + // XState Machines + debugLog( + 'CoreDump: xstate services', + (globalThis?.window as any)?.__xstate__?.services + ) + + debugLog('CoreDump: final clientState', clientState) + + const clientStateJson = JSON.stringify(clientState) + debugLog('CoreDump: final clientState JSON', clientStateJson) + + return Promise.resolve(clientStateJson) + } catch (error) { + console.error('CoreDump: unable to return data due to ', error) + return Promise.reject(JSON.stringify(error)) + } + } + // Return a data URL (png format) of the screenshot of the current page. screenshot(): Promise { return screenshot(this.htmlRef) diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index abdbe2608..ed511ae4b 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -3272,6 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" dependencies = [ "chrono", + "serde_json", "thiserror", "ts-rs-macros", "url", diff --git a/src/wasm-lib/kcl/Cargo.toml b/src/wasm-lib/kcl/Cargo.toml index 81dfa3d1d..a038b3432 100644 --- a/src/wasm-lib/kcl/Cargo.toml +++ b/src/wasm-lib/kcl/Cargo.toml @@ -37,7 +37,7 @@ serde_json = "1.0.116" sha2 = "0.10.8" thiserror = "1.0.61" toml = "0.8.14" -ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] } +ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] } url = { version = "2.5.2", features = ["serde"] } uuid = { version = "1.8.0", features = ["v4", "js", "serde"] } validator = { version = "0.18.1", features = ["derive"] } diff --git a/src/wasm-lib/kcl/src/coredump/local.rs b/src/wasm-lib/kcl/src/coredump/local.rs index 5f08fcdb1..220a4f9de 100644 --- a/src/wasm-lib/kcl/src/coredump/local.rs +++ b/src/wasm-lib/kcl/src/coredump/local.rs @@ -3,6 +3,7 @@ use anyhow::Result; use crate::coredump::CoreDump; +use serde_json::Value as JValue; #[derive(Debug, Clone)] pub struct CoreDumper {} @@ -55,6 +56,10 @@ impl CoreDump for CoreDumper { Ok(crate::coredump::WebrtcStats::default()) } + async fn get_client_state(&self) -> Result { + Ok(JValue::default()) + } + async fn screenshot(&self) -> Result { // Take a screenshot of the engine. todo!() diff --git a/src/wasm-lib/kcl/src/coredump/mod.rs b/src/wasm-lib/kcl/src/coredump/mod.rs index 0e412fcc6..8fd4d3c72 100644 --- a/src/wasm-lib/kcl/src/coredump/mod.rs +++ b/src/wasm-lib/kcl/src/coredump/mod.rs @@ -7,8 +7,13 @@ pub mod wasm; use anyhow::Result; use base64::Engine; +use kittycad::Client; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +/// "Value" would be OK. This is imported as "JValue" throughout the rest of this crate. +use serde_json::Value as JValue; +use std::path::Path; +use uuid::Uuid; #[async_trait::async_trait(?Send)] pub trait CoreDump: Clone { @@ -27,25 +32,24 @@ pub trait CoreDump: Clone { async fn get_webrtc_stats(&self) -> Result; + async fn get_client_state(&self) -> Result; + /// Return a screenshot of the app. async fn screenshot(&self) -> Result; /// Get a screenshot of the app and upload it to public cloud storage. - async fn upload_screenshot(&self) -> Result { + async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result { let screenshot = self.screenshot().await?; let cleaned = screenshot.trim_start_matches("data:image/png;base64,"); - // Create the zoo client. - let mut zoo = kittycad::Client::new(self.token()?); - zoo.set_base_url(&self.base_api_url()?); // Base64 decode the screenshot. let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?; // Upload the screenshot. - let links = zoo + let links = zoo_client .meta() .create_debug_uploads(vec![kittycad::types::multipart::Attachment { name: "".to_string(), - filename: Some("modeling-app/core-dump-screenshot.png".to_string()), + filename: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#)), content_type: Some("image/png".to_string()), data, }]) @@ -60,12 +64,19 @@ pub trait CoreDump: Clone { } /// Dump the app info. - async fn dump(&self) -> Result { + async fn dump(&self) -> Result { + // Create the zoo client. + let mut zoo_client = kittycad::Client::new(self.token()?); + zoo_client.set_base_url(&self.base_api_url()?); + + let coredump_id = uuid::Uuid::new_v4(); + let client_state = self.get_client_state().await?; let webrtc_stats = self.get_webrtc_stats().await?; let os = self.os().await?; - let screenshot_url = self.upload_screenshot().await?; + let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?; - let mut app_info = AppInfo { + let mut core_dump_info = CoreDumpInfo { + id: coredump_id, version: self.version()?, git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()), timestamp: chrono::Utc::now(), @@ -74,18 +85,44 @@ pub trait CoreDump: Clone { webrtc_stats, github_issue_url: None, pool: self.pool()?, + client_state, }; - app_info.set_github_issue_url(&screenshot_url)?; - Ok(app_info) + // pretty-printed JSON byte vector of the coredump. + let data = serde_json::to_vec_pretty(&core_dump_info)?; + + // Upload the coredump. + let links = zoo_client + .meta() + .create_debug_uploads(vec![kittycad::types::multipart::Attachment { + name: "".to_string(), + filename: Some(format!(r#"modeling-app/coredump-{}.json"#, coredump_id)), + content_type: Some("application/json".to_string()), + data, + }]) + .await + .map_err(|e| anyhow::anyhow!(e.to_string()))?; + + if links.is_empty() { + anyhow::bail!("Failed to upload coredump"); + } + + let coredump_url = &links[0]; + + core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?; + + Ok(core_dump_info) } } /// The app info structure. +/// The Core Dump Info structure. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] #[serde(rename_all = "snake_case")] -pub struct AppInfo { +pub struct CoreDumpInfo { + /// The unique id for the core dump - this helps correlate uploaded files with coredump data. + pub id: Uuid, /// The version of the app. pub version: String, /// The git revision of the app. @@ -95,45 +132,44 @@ pub struct AppInfo { pub timestamp: chrono::DateTime, /// If the app is running in tauri or the browser. pub tauri: bool, - /// The os info. pub os: OsInfo, - /// The webrtc stats. pub webrtc_stats: WebrtcStats, - /// A GitHub issue url to report the core dump. - /// This gets prepoulated with all the core dump info. #[serde(skip_serializing_if = "Option::is_none")] pub github_issue_url: Option, - /// Engine pool the client is connected to. pub pool: String, + /// The client state (singletons and xstate). + pub client_state: JValue, } -impl AppInfo { +impl CoreDumpInfo { /// Set the github issue url. - pub fn set_github_issue_url(&mut self, screenshot_url: &str) -> Result<()> { + pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> { + let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap(); let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" }; let labels = ["coredump", "bug", tauri_or_browser_label]; let body = format!( - r#"[Insert a description of the issue here] + r#"[Add a title above and insert a description of the issue here] -![Screenshot]({}) +![Screenshot]({screenshot_url})
Core Dump -```json -{} -``` +[{coredump_filename}]({coredump_url}) + +Reference ID: {coredump_id}
-"#, - screenshot_url, - serde_json::to_string_pretty(&self)? +"# ); let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect(); + // Note that `github_issue_url` is not included in the coredump file. + // It has already been encoded and uploaded at this point. + // The `github_issue_url` is used in openWindow in wasm.ts. self.github_issue_url = Some(format!( r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#, "KittyCAD", diff --git a/src/wasm-lib/kcl/src/coredump/wasm.rs b/src/wasm-lib/kcl/src/coredump/wasm.rs index 976255346..31ccc9100 100644 --- a/src/wasm-lib/kcl/src/coredump/wasm.rs +++ b/src/wasm-lib/kcl/src/coredump/wasm.rs @@ -4,6 +4,7 @@ use anyhow::Result; use wasm_bindgen::prelude::wasm_bindgen; use crate::{coredump::CoreDump, wasm::JsFuture}; +use serde_json::Value as JValue; #[wasm_bindgen(module = "/../../lib/coredump.ts")] extern "C" { @@ -31,6 +32,9 @@ extern "C" { #[wasm_bindgen(method, js_name = getWebrtcStats, catch)] fn get_webrtc_stats(this: &CoreDumpManager) -> Result; + #[wasm_bindgen(method, js_name = getClientState, catch)] + fn get_client_state(this: &CoreDumpManager) -> Result; + #[wasm_bindgen(method, js_name = screenshot, catch)] fn screenshot(this: &CoreDumpManager) -> Result; } @@ -123,6 +127,27 @@ impl CoreDump for CoreDumper { Ok(stats) } + async fn get_client_state(&self) -> Result { + let promise = self + .manager + .get_client_state() + .map_err(|e| anyhow::anyhow!("Failed to get promise from get client state: {:?}", e))?; + + let value = JsFuture::from(promise) + .await + .map_err(|e| anyhow::anyhow!("Failed to get response from client state: {:?}", e))?; + + // Parse the value as a string. + let s = value + .as_string() + .ok_or_else(|| anyhow::anyhow!("Failed to get string from response from client stat: `{:?}`", value))?; + + let client_state: JValue = + serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse client state: {:?}", e))?; + + Ok(client_state) + } + async fn screenshot(&self) -> Result { let promise = self .manager diff --git a/src/wasm-lib/tests/cordump/inputs/coredump.fixture.json b/src/wasm-lib/tests/cordump/inputs/coredump.fixture.json new file mode 100644 index 000000000..43d9e9bb6 --- /dev/null +++ b/src/wasm-lib/tests/cordump/inputs/coredump.fixture.json @@ -0,0 +1,316 @@ +{ + "version": "0.20.1", + "git_rev": "3a05211d306ca045ace2e7bf10b7f8138e1daad5", + "timestamp": "2024-05-07T20:06:34.655Z", + "tauri": false, + "os": { + "platform": "Mac OS", + "version": "10.15.7", + "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" + }, + "webrtc_stats": { + "packets_lost": 0, + "frames_received": 672, + "frame_width": 1440.0, + "frame_height": 712.0, + "frame_rate": 58.0, + "key_frames_decoded": 7, + "frames_dropped": 77, + "pause_count": 0, + "total_pauses_duration": 0.0, + "freeze_count": 12, + "total_freezes_duration": 3.057, + "pli_count": 6, + "jitter": 0.011 + }, + "pool": "", + "client_state": { + "engine_command_manager": { + "artifact_map": { + "ac7a8c52-7437-42e6-ae2a-25c54d0b4a16": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "83a2e866-d8e1-47d9-afae-d55a64cd5b40": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "plane_set_color", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "c92d7e84-a03e-456e-aad0-3e302ae0ffb6": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "a9abb9b8-54b1-4042-8ab3-e67c7ddb4cb3": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "plane_set_color", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "bafb884d-ee93-48f5-a667-91d1bdb8178a": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "6b7f539b-c4ca-4e62-a971-147e1abaca7b": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "plane_set_color", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "6ddaaaa3-b080-4cb3-b08e-8fe277312ccc": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "f1ef28b0-49b3-45f8-852d-772648149785": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "7c532be8-f07d-456b-8643-53808df86823": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "cf38f826-6d15-45f4-9c64-a6e9e4909e75": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "d69e6248-9e0f-4bc8-bc6e-d995931e8886": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "plane_set_color", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "cdb44075-ac6d-4626-b321-26d022f5f7f0": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "44f81391-0f2d-49f3-92b9-7dfc8446d571": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "plane_set_color", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "7b893ecf-8887-49c3-b978-392813d5f625": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "61247d28-04ba-4768-85b0-fbc582151dbd": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "plane_set_color", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "1318f5af-76a9-4161-9f6a-d410831fd098": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "908d264c-5989-4030-b2f4-5dfb52fda71a": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + }, + "0de4c253-ee21-4385-93bd-b93264f7eb60": { + "type": "result", + "range": [0, 0], + "pathToNode": [], + "commandType": "make_plane", + "data": { "type": "empty" }, + "raw": { + "success": true, + "request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c", + "resp": { + "type": "modeling", + "data": { "modeling_response": { "type": "empty" } } + } + } + } + }, + "engine_connection": { + "state": { + "type": "connection-established" + } + } + }, + "kcl_manager": {}, + "scene_infra": {}, + "auth_machine": {}, + "command_bar_machine": {}, + "file_machine": {}, + "home_machine": {}, + "modeling_machine": {}, + "settings_machine": {} + } +}