Nadro/3686/file swapping while executing (#3703)
* chore: Implemented a executeAst interrupt to stop processing a KCL program * fix: added a catch since this promise was not being caught * fix: fmt formatting, need to fix some tsc errors next. * fix: fixing tsc errors * fix: cleaning up comment * fix: only rejecting pending modeling commands * fix: adding constant for rejection message, adding rejection in WASM send command * fix: tsc, lint, fmt checks * fix circ dependency --------- Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
@ -4,6 +4,7 @@ import { KCLError, kclErrorsToDiagnostics } from './errors'
|
|||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EngineCommandManager } from './std/engineConnection'
|
import { EngineCommandManager } from './std/engineConnection'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -122,6 +123,7 @@ export class KclManager {
|
|||||||
get isExecuting() {
|
get isExecuting() {
|
||||||
return this._isExecuting
|
return this._isExecuting
|
||||||
}
|
}
|
||||||
|
|
||||||
set isExecuting(isExecuting) {
|
set isExecuting(isExecuting) {
|
||||||
this._isExecuting = isExecuting
|
this._isExecuting = isExecuting
|
||||||
// If we have finished executing, but the execute is stale, we should
|
// If we have finished executing, but the execute is stale, we should
|
||||||
@ -232,6 +234,12 @@ export class KclManager {
|
|||||||
async executeAst(args: ExecuteArgs = {}): Promise<void> {
|
async executeAst(args: ExecuteArgs = {}): Promise<void> {
|
||||||
if (this.isExecuting) {
|
if (this.isExecuting) {
|
||||||
this.executeIsStale = args
|
this.executeIsStale = args
|
||||||
|
|
||||||
|
// The previous execteAst will be rejected and cleaned up. The execution will be marked as stale.
|
||||||
|
// A new executeAst will start.
|
||||||
|
this.engineCommandManager.rejectAllModelingCommands(
|
||||||
|
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE
|
||||||
|
)
|
||||||
// Exit early if we are already executing.
|
// Exit early if we are already executing.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -245,16 +253,18 @@ export class KclManager {
|
|||||||
// Make sure we clear before starting again. End session will do this.
|
// Make sure we clear before starting again. End session will do this.
|
||||||
this.engineCommandManager?.endSession()
|
this.engineCommandManager?.endSession()
|
||||||
await this.ensureWasmInit()
|
await this.ensureWasmInit()
|
||||||
const { logs, errors, programMemory } = await executeAst({
|
const { logs, errors, programMemory, isInterrupted } = await executeAst({
|
||||||
ast,
|
ast,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Program was not interrupted, setup the scene
|
||||||
|
// Do not send send scene commands if the program was interrupted, go to clean up
|
||||||
|
if (!isInterrupted) {
|
||||||
this.lints = await lintAst({ ast: ast })
|
this.lints = await lintAst({ ast: ast })
|
||||||
|
|
||||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||||
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
||||||
await this.engineCommandManager.waitForAllCommands()
|
|
||||||
|
|
||||||
if (args.zoomToFit) {
|
if (args.zoomToFit) {
|
||||||
let zoomObjectId: string | undefined = ''
|
let zoomObjectId: string | undefined = ''
|
||||||
@ -275,6 +285,7 @@ export class KclManager {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.isExecuting = false
|
this.isExecuting = false
|
||||||
|
|
||||||
@ -284,7 +295,8 @@ export class KclManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.logs = logs
|
this.logs = logs
|
||||||
this.addKclErrors(errors)
|
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
||||||
|
this.addKclErrors(isInterrupted ? [] : errors)
|
||||||
this.programMemory = programMemory
|
this.programMemory = programMemory
|
||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
@ -292,6 +304,7 @@ export class KclManager {
|
|||||||
type: 'execution-done',
|
type: 'execution-done',
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._cancelTokens.delete(currentExecutionId)
|
this._cancelTokens.delete(currentExecutionId)
|
||||||
}
|
}
|
||||||
// NOTE: this always updates the code state and editor.
|
// NOTE: this always updates the code state and editor.
|
||||||
|
@ -54,10 +54,12 @@ export async function executeAst({
|
|||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
useFakeExecutor?: boolean
|
useFakeExecutor?: boolean
|
||||||
programMemoryOverride?: ProgramMemory
|
programMemoryOverride?: ProgramMemory
|
||||||
|
isInterrupted?: boolean
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
logs: string[]
|
logs: string[]
|
||||||
errors: KCLError[]
|
errors: KCLError[]
|
||||||
programMemory: ProgramMemory
|
programMemory: ProgramMemory
|
||||||
|
isInterrupted: boolean
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
if (!useFakeExecutor) {
|
if (!useFakeExecutor) {
|
||||||
@ -73,13 +75,23 @@ export async function executeAst({
|
|||||||
logs: [],
|
logs: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
programMemory,
|
programMemory,
|
||||||
|
isInterrupted: false,
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
let isInterrupted = false
|
||||||
if (e instanceof KCLError) {
|
if (e instanceof KCLError) {
|
||||||
|
// Detect if it is a force interrupt error which is not a KCL processing error.
|
||||||
|
if (
|
||||||
|
e.msg ===
|
||||||
|
'Failed to wait for promise from engine: JsValue("Force interrupt, executionIsStale, new AST requested")'
|
||||||
|
) {
|
||||||
|
isInterrupted = true
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
errors: [e],
|
errors: [e],
|
||||||
logs: [],
|
logs: [],
|
||||||
programMemory: ProgramMemory.empty(),
|
programMemory: ProgramMemory.empty(),
|
||||||
|
isInterrupted,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@ -87,6 +99,7 @@ export async function executeAst({
|
|||||||
logs: [e],
|
logs: [e],
|
||||||
errors: [],
|
errors: [],
|
||||||
programMemory: ProgramMemory.empty(),
|
programMemory: ProgramMemory.empty(),
|
||||||
|
isInterrupted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import { exportMake } from 'lib/exportMake'
|
import { exportMake } from 'lib/exportMake'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
||||||
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
|
import { KclManager } from 'lang/KclSingleton'
|
||||||
|
|
||||||
// TODO(paultag): This ought to be tweakable.
|
// TODO(paultag): This ought to be tweakable.
|
||||||
const pingIntervalMs = 5_000
|
const pingIntervalMs = 5_000
|
||||||
@ -1279,6 +1281,7 @@ interface PendingMessage {
|
|||||||
resolve: (data: [Models['WebSocketResponse_type']]) => void
|
resolve: (data: [Models['WebSocketResponse_type']]) => void
|
||||||
reject: (reason: string) => void
|
reject: (reason: string) => void
|
||||||
promise: Promise<[Models['WebSocketResponse_type']]>
|
promise: Promise<[Models['WebSocketResponse_type']]>
|
||||||
|
isSceneCommand: boolean
|
||||||
}
|
}
|
||||||
export class EngineCommandManager extends EventTarget {
|
export class EngineCommandManager extends EventTarget {
|
||||||
/**
|
/**
|
||||||
@ -1379,6 +1382,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
}: CustomEvent<NewTrackArgs>) => {}
|
}: CustomEvent<NewTrackArgs>) => {}
|
||||||
modelingSend: ReturnType<typeof useModelingContext>['send'] =
|
modelingSend: ReturnType<typeof useModelingContext>['send'] =
|
||||||
(() => {}) as any
|
(() => {}) as any
|
||||||
|
kclManager: null | KclManager = null
|
||||||
|
|
||||||
set exportIntent(intent: ExportIntent | null) {
|
set exportIntent(intent: ExportIntent | null) {
|
||||||
this._exportIntent = intent
|
this._exportIntent = intent
|
||||||
@ -1932,11 +1936,21 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
;(cmd as any).sequence = this.outSequence++
|
;(cmd as any).sequence = this.outSequence++
|
||||||
}
|
}
|
||||||
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
||||||
return this.sendCommand(command.cmd_id, {
|
return this.sendCommand(
|
||||||
|
command.cmd_id,
|
||||||
|
{
|
||||||
command,
|
command,
|
||||||
idToRangeMap: {},
|
idToRangeMap: {},
|
||||||
range: [0, 0],
|
range: [0, 0],
|
||||||
}).then(([a]) => a)
|
},
|
||||||
|
true // isSceneCommand
|
||||||
|
)
|
||||||
|
.then(([a]) => a)
|
||||||
|
.catch((e) => {
|
||||||
|
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
|
||||||
|
/*noop*/
|
||||||
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* A wrapper around the sendCommand where all inputs are JSON strings
|
* A wrapper around the sendCommand where all inputs are JSON strings
|
||||||
@ -1963,6 +1977,12 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
const idToRangeMap: { [key: string]: SourceRange } =
|
const idToRangeMap: { [key: string]: SourceRange } =
|
||||||
JSON.parse(idToRangeStr)
|
JSON.parse(idToRangeStr)
|
||||||
|
|
||||||
|
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
|
||||||
|
// Used in conjunction with rejectAllModelingCommands
|
||||||
|
if (this?.kclManager?.executeIsStale) {
|
||||||
|
return Promise.reject(EXECUTE_AST_INTERRUPT_ERROR_MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await this.sendCommand(id, {
|
const resp = await this.sendCommand(id, {
|
||||||
command,
|
command,
|
||||||
range,
|
range,
|
||||||
@ -1980,7 +2000,8 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
command: PendingMessage['command']
|
command: PendingMessage['command']
|
||||||
range: PendingMessage['range']
|
range: PendingMessage['range']
|
||||||
idToRangeMap: PendingMessage['idToRangeMap']
|
idToRangeMap: PendingMessage['idToRangeMap']
|
||||||
}
|
},
|
||||||
|
isSceneCommand = false
|
||||||
): Promise<[Models['WebSocketResponse_type']]> {
|
): Promise<[Models['WebSocketResponse_type']]> {
|
||||||
const { promise, resolve, reject } = promiseFactory<any>()
|
const { promise, resolve, reject } = promiseFactory<any>()
|
||||||
this.pendingCommands[id] = {
|
this.pendingCommands[id] = {
|
||||||
@ -1990,7 +2011,9 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
command: message.command,
|
command: message.command,
|
||||||
range: message.range,
|
range: message.range,
|
||||||
idToRangeMap: message.idToRangeMap,
|
idToRangeMap: message.idToRangeMap,
|
||||||
|
isSceneCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.command.type === 'modeling_cmd_req') {
|
if (message.command.type === 'modeling_cmd_req') {
|
||||||
this.orderedCommands.push({
|
this.orderedCommands.push({
|
||||||
command: message.command,
|
command: message.command,
|
||||||
@ -2037,6 +2060,19 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
this.deferredArtifactPopulated(null)
|
this.deferredArtifactPopulated(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject all of the modeling pendingCommands created from sendModelingCommandFromWasm
|
||||||
|
* This interrupts the runtime of executeAst. Stops the AST processing and stops sending commands
|
||||||
|
* to the engine
|
||||||
|
*/
|
||||||
|
rejectAllModelingCommands(rejectionMessage: string) {
|
||||||
|
Object.values(this.pendingCommands).forEach(
|
||||||
|
({ reject, isSceneCommand }) =>
|
||||||
|
!isSceneCommand && reject(rejectionMessage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async initPlanes() {
|
async initPlanes() {
|
||||||
if (this.planesInitialized()) return
|
if (this.planesInitialized()) return
|
||||||
const planes = await this.makeDefaultPlanes()
|
const planes = await this.makeDefaultPlanes()
|
||||||
|
@ -67,3 +67,8 @@ export const COOKIE_NAME = '__Secure-next-auth.session-token'
|
|||||||
|
|
||||||
/** localStorage key to determine if we're in Playwright tests */
|
/** localStorage key to determine if we're in Playwright tests */
|
||||||
export const PLAYWRIGHT_KEY = 'playwright'
|
export const PLAYWRIGHT_KEY = 'playwright'
|
||||||
|
|
||||||
|
/** Custom error message to match when rejectAllModelCommands is called
|
||||||
|
* allows us to match if the execution of executeAst was interrupted */
|
||||||
|
export const EXECUTE_AST_INTERRUPT_ERROR_MESSAGE =
|
||||||
|
'Force interrupt, executionIsStale, new AST requested'
|
||||||
|
@ -17,6 +17,7 @@ window.tearDown = engineCommandManager.tearDown
|
|||||||
// This needs to be after codeManager is created.
|
// This needs to be after codeManager is created.
|
||||||
export const kclManager = new KclManager(engineCommandManager)
|
export const kclManager = new KclManager(engineCommandManager)
|
||||||
kclManager.isFirstRender = true
|
kclManager.isFirstRender = true
|
||||||
|
engineCommandManager.kclManager = kclManager
|
||||||
|
|
||||||
engineCommandManager.getAstCb = () => kclManager.ast
|
engineCommandManager.getAstCb = () => kclManager.ast
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user