diff --git a/src/index.tsx b/src/index.tsx index 12d1c361a..907961a59 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,8 +10,11 @@ import { AppStreamProvider } from 'AppState' import { ToastUpdate } from 'components/ToastUpdate' import { markOnce } from 'lib/performance' import { AUTO_UPDATER_TOAST_ID } from 'lib/constants' +import { initializeWindowExceptionHandler } from 'lib/exceptions' markOnce('code/willAuth') +initializeWindowExceptionHandler() + // uncomment for xstate inspector // import { DEV } from 'env' // import { inspect } from '@xstate/inspect' diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 87b33bf64..b106cd83e 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -390,6 +390,24 @@ export class KclManager { this._cancelTokens.delete(currentExecutionId) markOnce('code/endExecuteAst') } + + /** + * This cleanup function is external and internal to the KclSingleton class. + * Since the WASM runtime can panic and the error cannot be caught in executeAst + * we need a global exception handler in exceptions.ts + * This file will interface with this cleanup as if it caught the original error + * to properly restore the TS application state. + */ + executeAstCleanUp() { + this.isExecuting = false + this.executeIsStale = null + this.engineCommandManager.addCommandLog({ + type: 'execution-done', + data: null, + }) + markOnce('code/endExecuteAst') + } + // NOTE: this always updates the code state and editor. // DO NOT CALL THIS from codemirror ever. async executeAstMock( diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 84c4dd527..cc59b1904 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -1,4 +1,5 @@ -import init, { +import { + init, parse_wasm, recast_wasm, execute, @@ -16,7 +17,9 @@ import init, { default_project_settings, base64_decode, clear_scene_and_bust_cache, -} from '../wasm-lib/pkg/wasm_lib' + reloadModule, +} from 'lib/wasm_lib_wrapper' + import { KCLError } from './errors' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { EngineCommandManager } from './std/engineConnection' @@ -144,6 +147,7 @@ export const wasmUrl = () => { // Initialise the wasm module. const initialise = async () => { try { + await reloadModule() const fullUrl = wasmUrl() const input = await fetch(fullUrl) const buffer = await input.arrayBuffer() diff --git a/src/lib/exceptions.ts b/src/lib/exceptions.ts new file mode 100644 index 000000000..d72075221 --- /dev/null +++ b/src/lib/exceptions.ts @@ -0,0 +1,51 @@ +import { kclManager } from 'lib/singletons' +import { reloadModule, getModule } from 'lib/wasm_lib_wrapper' +import toast from 'react-hot-toast' +import { reportRejection } from './trap' + +let initialized = false + +/** + * WASM/Rust runtime can panic and the original try/catch/finally blocks will not trigger + * on the await promise. The interface will killed. This means we need to catch the error at + * the global/DOM level. This will have to interface with whatever controlflow that needs to be picked up + * within the error branch in the typescript to cover the application state. + */ +export const initializeWindowExceptionHandler = () => { + if (window && !initialized) { + window.addEventListener('error', (event) => { + void (async () => { + if (matchImportExportErrorCrash(event.message)) { + // do global singleton cleanup + kclManager.executeAstCleanUp() + toast.error( + 'You have hit a KCL execution bug! Put your KCL code in a github issue to help us resolve this bug.' + ) + try { + await reloadModule() + await getModule().default() + } catch (e) { + console.error('Failed to initialize wasm_lib') + console.error(e) + } + } + })().catch(reportRejection) + }) + // Make sure we only initialize this event listener once + initialized = true + } else { + console.error( + `Failed to initialize, window: ${window}, initialized:${initialized}` + ) + } +} + +/** + * Specifically match a substring of the message error to detect an import export runtime issue + * when the WASM runtime panics + */ +const matchImportExportErrorCrash = (message: string): boolean => { + // called `Result::unwrap_throw()` on an `Err` value + const substringError = '`Result::unwrap_throw()` on an `Err` value' + return message.indexOf(substringError) !== -1 ? true : false +} diff --git a/src/lib/wasm_lib_wrapper.ts b/src/lib/wasm_lib_wrapper.ts new file mode 100644 index 000000000..1fe1b8bf7 --- /dev/null +++ b/src/lib/wasm_lib_wrapper.ts @@ -0,0 +1,108 @@ +/** + * This wrapper file is to enable reloading of the wasm_lib.js file. + * When the wasm instance bricks there is no API or interface to restart, + * restore, or re init the WebAssembly instance. The entire application would need + * to restart. + * A way to bypass this is by reloading the entire .js file so the global wasm variable + * gets reinitialized and we do not use that old reference + */ + +import { + parse_wasm as ParseWasm, + recast_wasm as RecastWasm, + execute as Execute, + kcl_lint as KclLint, + modify_ast_for_sketch_wasm as ModifyAstForSketch, + is_points_ccw as IsPointsCcw, + get_tangential_arc_to_info as GetTangentialArcToInfo, + program_memory_init as ProgramMemoryInit, + make_default_planes as MakeDefaultPlanes, + coredump as CoreDump, + toml_stringify as TomlStringify, + default_app_settings as DefaultAppSettings, + parse_app_settings as ParseAppSettings, + parse_project_settings as ParseProjectSettings, + default_project_settings as DefaultProjectSettings, + base64_decode as Base64Decode, + clear_scene_and_bust_cache as ClearSceneAndBustCache, +} from '../wasm-lib/pkg/wasm_lib' + +type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib') + +// Stores the result of the import of the wasm_lib file +let data: ModuleType + +// Imports the .js file again which will clear the old import +// This allows us to reinitialize the wasm instance +export async function reloadModule() { + data = await import(`../wasm-lib/pkg/wasm_lib`) +} + +export function getModule(): ModuleType { + return data +} + +export async function init(module_or_path: any) { + return await getModule().default(module_or_path) +} +export const parse_wasm: typeof ParseWasm = (...args) => { + return getModule().parse_wasm(...args) +} +export const recast_wasm: typeof RecastWasm = (...args) => { + return getModule().recast_wasm(...args) +} +export const execute: typeof Execute = (...args) => { + return getModule().execute(...args) +} +export const kcl_lint: typeof KclLint = (...args) => { + return getModule().kcl_lint(...args) +} +export const modify_ast_for_sketch_wasm: typeof ModifyAstForSketch = ( + ...args +) => { + return getModule().modify_ast_for_sketch_wasm(...args) +} +export const is_points_ccw: typeof IsPointsCcw = (...args) => { + return getModule().is_points_ccw(...args) +} +export const get_tangential_arc_to_info: typeof GetTangentialArcToInfo = ( + ...args +) => { + return getModule().get_tangential_arc_to_info(...args) +} +export const program_memory_init: typeof ProgramMemoryInit = (...args) => { + return getModule().program_memory_init(...args) +} +export const make_default_planes: typeof MakeDefaultPlanes = (...args) => { + return getModule().make_default_planes(...args) +} +export const coredump: typeof CoreDump = (...args) => { + return getModule().coredump(...args) +} +export const toml_stringify: typeof TomlStringify = (...args) => { + return getModule().toml_stringify(...args) +} +export const default_app_settings: typeof DefaultAppSettings = (...args) => { + return getModule().default_app_settings(...args) +} +export const parse_app_settings: typeof ParseAppSettings = (...args) => { + return getModule().parse_app_settings(...args) +} +export const parse_project_settings: typeof ParseProjectSettings = ( + ...args +) => { + return getModule().parse_project_settings(...args) +} +export const default_project_settings: typeof DefaultProjectSettings = ( + ...args +) => { + return getModule().default_project_settings(...args) +} +export const base64_decode: typeof Base64Decode = (...args) => { + return getModule().base64_decode(...args) +} +export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = ( + ...args +) => { + return getModule().clear_scene_and_bust_cache(...args) +}