Nadro/4857/wasm panic catching errors (#4901)
* chore: skeleton code to initialize and detect the global WASM panic * chore: implementing a reimport method to fix the wasm instance being bricked * fix: cleaning up tsc/lint * fix: renaming file to be more accurate * fix: added toast message * fix: types... * fix: typed the functions with arg spreads
This commit is contained in:
		@ -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'
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								src/lib/exceptions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/lib/exceptions.ts
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								src/lib/wasm_lib_wrapper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/lib/wasm_lib_wrapper.ts
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user