import type { Protocol } from 'devtools-protocol'; import type { DebuggerContext, DebuggerNamespace, HeapProfilerNamespace, ProfilerNamespace, RuntimeNamespace, TargetNamespace, } from './types.mts'; import { getParsedEvent } from './internal-utils.mts'; import { InspectorContext } from './context.mts'; import { Call, NormalCompletion, ObjectValue, ParseScript, runJobQueue, ScriptRecord, surroundingAgent, ThrowCompletion, skipDebugger, Value, type FunctionObject, ParseModule, SourceTextModuleRecord, ValueOfNormalCompletion, JSStringValue, evalQ, Assert, kInternal, captureStack, isEvaluator, getBreakpointCandidateNodes, parseNodeToBreakpointLocation, performDevtoolsEval, isFunctionObject, ModuleRecord, GetModuleNamespace, } from '#self'; export const Debugger: DebuggerNamespace = { enable(_req, context) { context.onDebuggerConnect(); return { debuggerId: 'debugger.0' }; }, disable(_req, context) { context.onDebuggerDisconnect(); }, getScriptSource({ scriptId }) { const source = surroundingAgent.parsedSources.get(scriptId); if (!source) { throw new Error('Not found'); } return { scriptSource: source.ECMAScriptCode.sourceText }; }, setAsyncCallStackDepth() { }, setBlackboxPatterns() { }, setBlackboxExecutionContexts() { }, // #region breakpoints getPossibleBreakpoints({ start, end, restrictToFunction }) { return { locations: [...getBreakpointCandidateNodes(start, end, restrictToFunction)] .map((node) => parseNodeToBreakpointLocation(start.scriptId, node)), }; }, removeBreakpoint({ breakpointId }) { surroundingAgent?.removeBreakpoint(breakpointId); }, setBreakpoint(req) { return surroundingAgent.addBreakpointByLocation(req); }, setBreakpointByUrl(req) { return surroundingAgent.addBreakpointByUrl(req); }, setBreakpointOnFunctionCall(req, context) { const f = context.context.getObject(req.objectId); if (!f || !isFunctionObject(f)) return { breakpointId: null! }; return surroundingAgent.addBreakpointOnFunctionCall(f, req.condition); }, setInstrumentationBreakpoint(req) { return surroundingAgent.addInstrumentationBreakpoint(req); }, setBreakpointsActive({ active }) { surroundingAgent.breakpointsEnabled = active; }, setPauseOnExceptions({ state }) { if (surroundingAgent) { surroundingAgent.pauseOnExceptions = state === 'none' ? undefined : state; } }, // #endregion stepInto(_, { sendEvent }) { sendEvent['Debugger.resumed'](); surroundingAgent.resumeEvaluate({ pauseAt: 'step-in' }); }, resume(_, { sendEvent }) { sendEvent['Debugger.resumed'](); surroundingAgent.resumeEvaluate(); }, stepOver(_req, { sendEvent }) { sendEvent['Debugger.resumed'](); surroundingAgent.resumeEvaluate({ pauseAt: 'step-over' }); }, stepOut(_req, { sendEvent }) { sendEvent['Debugger.resumed'](); surroundingAgent.resumeEvaluate({ pauseAt: 'step-out' }); }, evaluateOnCallFrame(req, context) { return evaluate({ ...req, uniqueContextId: context.context.getRealm(undefined)!.descriptor.uniqueId, evalMode: context.context.evaluateMode, }, context); }, engine262_setEvaluateMode({ mode }, { context }) { if (mode === 'module' || mode === 'script' || mode === 'console') { context.evaluateMode = mode; } }, engine262_setFeatures() { throw new Error('Method should not be implemented here.'); }, }; export const Profiler: ProfilerNamespace = { enable() { }, }; export const Runtime: RuntimeNamespace = { discardConsoleEntries() { }, enable() {}, compileScript(options, { context, sendEvent }) { let parsed!: ScriptRecord | SourceTextModuleRecord | ObjectValue[]; let realm = context.getRealm(options.executionContextId); if (!realm && !options.persistScript) { realm = context.getAnyRealm(); } if (!realm) { return unsupportedError; } realm.realm.scope(() => { if (context.evaluateMode === 'module') { parsed = ParseModule(options.expression, realm.realm, { specifier: options.sourceURL, doNotTrackScriptId: !options.persistScript }); } else { parsed = ParseScript(options.expression, realm.realm, { specifier: options.sourceURL, doNotTrackScriptId: !options.persistScript, [kInternal]: { allowAllPrivateNames: true, allowAwait: true } }); } }); if (!parsed) { throw new Error('No parsed result'); } if (Array.isArray(parsed)) { const e = context.createExceptionDetails(ThrowCompletion(parsed[0]), false); // Note: it has to be this message to trigger devtools' line wrap. e.exception!.description = 'SyntaxError: Unexpected end of input'; return { exceptionDetails: e }; } if (options.persistScript) { if (realm?.descriptor.id === undefined) { throw new Error('No realm id found'); } const event = getParsedEvent(parsed, parsed.HostDefined!.scriptId!, realm.descriptor.id); sendEvent['Debugger.scriptParsed'](event); return { scriptId: event.scriptId }; } return {}; }, callFunctionOn(options, { context }): Protocol.Runtime.CallFunctionOnResponse { const realmDesc = context.getRealm(options.uniqueContextId || options.executionContextId) || context.getAnyRealm(); if (!realmDesc) { throw new Error('No realm found'); } const { Value: F } = realmDesc.realm.evaluateScriptSkipDebugger(`(${options.functionDeclaration})`, { doNotTrackScriptId: true }) as NormalCompletion; const thisValue = options.objectId ? context.getObject(options.objectId)! : Value.undefined; const args = options.arguments?.map((a) => { // TODO: revisit if ('value' in a) { return Value(a.value); } if (a.objectId) { return context.getObject(a.objectId)!; } if ('unserializableValue' in a) { throw new RangeError(); } return Value.undefined; }); return realmDesc.realm.scope((): Protocol.Runtime.CallFunctionOnResponse => { const completion = evalQ((Q, X): Protocol.Runtime.CallFunctionOnResponse => { const r = Q(skipDebugger(Call(F, thisValue, args || []))); if (options.returnByValue) { const value = X(Call(realmDesc.realm.Intrinsics['%JSON.stringify%'], Value.undefined, [r])); if (value instanceof JSStringValue) { const valueRealized = JSON.parse(value.stringValue()); return { result: { type: typeof value, value: valueRealized } }; } } return context.createEvaluationResult(r); }); if (completion instanceof ThrowCompletion) { return { result: { type: 'undefined' }, exceptionDetails: context.createExceptionDetails(completion, false) }; } return completion.Value; }); }, evaluate(options, context) { return evaluate({ ...options, evalMode: context.context.evaluateMode, uniqueContextId: options.uniqueContextId!, }, context); }, getExceptionDetails(req, { context }) { const object = context.getObject(req.errorObjectId)!; if (object instanceof ObjectValue) { return { exceptionDetails: context.createExceptionDetails(ThrowCompletion(object), false), }; } return { exceptionDetails: { text: 'unsupported', lineNumber: 0, columnNumber: 0, exceptionId: 0, }, }; }, getHeapUsage() { return { usedSize: 0, totalSize: 0, backingStorageSize: 0, embedderHeapUsedSize: 0, }; }, getIsolateId() { return { id: 'isolate.0' }; }, getProperties(options, { context }) { return context.getProperties(options); }, globalLexicalScopeNames({ executionContextId }, { context }) { const global = context.getRealm(executionContextId)?.realm.GlobalObject; if (!global) { return { names: [] }; } const keys = skipDebugger(global.OwnPropertyKeys()); if (keys instanceof ThrowCompletion) { return { names: [] }; } return { names: ValueOfNormalCompletion(keys).map((k) => (k instanceof JSStringValue ? k.stringValue() : null!)).filter(Boolean) }; }, releaseObject(req, { context }) { context.releaseObject(req.objectId); }, releaseObjectGroup({ objectGroup }, { context }) { context.releaseObjectGroup(objectGroup); }, runIfWaitingForDebugger() { }, }; export const HeapProfiler: HeapProfilerNamespace = { enable() { }, collectGarbage() { }, }; export const Target: TargetNamespace = { setDiscoverTargets() { }, // @ts-expect-error no doc setRemoteLocations() { }, }; const unsupportedError: Protocol.Runtime.EvaluateResponse = { result: { type: 'undefined' }, exceptionDetails: { text: 'unsupported', lineNumber: 0, columnNumber: 0, exceptionId: 0, }, }; function evaluate(options: { uniqueContextId: string, expression: string, evalMode: InspectorContext['evaluateMode'], throwOnSideEffect?: boolean, awaitPromise?: boolean, callFrameId?: string, }, inspectorContext: DebuggerContext): Protocol.Runtime.EvaluateResponse | Promise { const { context } = inspectorContext; const isPreview = options.throwOnSideEffect; if (options.awaitPromise) { return unsupportedError; } const realm = context.getRealm(options.uniqueContextId); if (!realm) { return unsupportedError; } const isCallOnFrame = typeof options.callFrameId === 'string'; let callOnFramePoppedLevel = 0; const oldExecutionStack = [...surroundingAgent.executionContextStack]; if (isCallOnFrame) { const frame = surroundingAgent.executionContextStack[options.callFrameId as `${number}`]; if (!frame) { inspectorContext.sendEvent['Runtime.exceptionThrown']({ timestamp: Date.now(), exceptionDetails: { columnNumber: 0, exceptionId: 0, lineNumber: 0, text: `Execution context not found for callFrameId ${options.callFrameId}`, }, }); return unsupportedError; } for (const currentFrame of [...surroundingAgent.executionContextStack].reverse()) { if (currentFrame === frame) { break; } callOnFramePoppedLevel += 1; surroundingAgent.executionContextStack.pop(currentFrame); } } const promise = new Promise((resolve) => { let toBeEvaluated; if (isPreview || options.evalMode === 'console' || isCallOnFrame) { toBeEvaluated = performDevtoolsEval(options.expression, realm.realm, false, !!(isPreview || isCallOnFrame)); } else { let parsed!: ScriptRecord | SourceTextModuleRecord | ObjectValue[]; const realm = context.getRealm(options.uniqueContextId); realm?.realm.scope(() => { if (options.evalMode === 'module') { parsed = ParseModule(options.expression, realm.realm); } else { parsed = ParseScript(options.expression, realm.realm); } }); if (Array.isArray(parsed)) { const e = context.createExceptionDetails(ThrowCompletion(parsed[0]), false); resolve({ exceptionDetails: e, result: { type: 'undefined' } }); return; } toBeEvaluated = parsed; } const noDebuggerEvaluate = () => { if (!isEvaluator(toBeEvaluated)) { throw new Assert.Error('Unexpected'); } resolve(context.createEvaluationResult(skipDebugger(toBeEvaluated))); }; if (isPreview) { surroundingAgent.debugger_scopePreview(noDebuggerEvaluate); return; } if (isCallOnFrame) { noDebuggerEvaluate(); return; } if (toBeEvaluated instanceof ModuleRecord) { realm.realm.evaluateModule(toBeEvaluated, undefined, (completion) => { if (completion instanceof ThrowCompletion) { resolve(context.createEvaluationResult(completion)); } else { resolve(context.createEvaluationResult(NormalCompletion(GetModuleNamespace(toBeEvaluated, 'evaluation')))); } runJobQueue(); }); } else if (toBeEvaluated instanceof ScriptRecord) { let completion; realm.realm.evaluateScript(toBeEvaluated, {}, (c) => { completion = c; resolve(context.createEvaluationResult(completion)); }); if (!completion) surroundingAgent.resumeEvaluate(); runJobQueue(); } else { let completion; surroundingAgent.evaluate(toBeEvaluated, (c) => { completion = c; resolve(context.createEvaluationResult(c)); }); if (!completion) surroundingAgent.resumeEvaluate(); runJobQueue(); } }); promise.then(() => { if (callOnFramePoppedLevel) { Assert(oldExecutionStack.length - callOnFramePoppedLevel === surroundingAgent.executionContextStack.length); for (const [newIndex, newStack] of surroundingAgent.executionContextStack.entries()) { Assert(newStack === oldExecutionStack[newIndex]); } surroundingAgent.executionContextStack.length = 0; for (const stack of oldExecutionStack) { surroundingAgent.executionContextStack.push(stack); } } }, (err): Protocol.Runtime.EvaluateResponse => { const expr = surroundingAgent.runningExecutionContext?.callSite.lastNode?.sourceText; const frame = InspectorContext.callSiteToCallFrame(captureStack().stack); // @ts-expect-error // eslint-disable-next-line no-console, @typescript-eslint/no-explicit-any declare const console: any; if (typeof console === 'object') console.error(err); inspectorContext.sendEvent['Runtime.exceptionThrown']({ timestamp: Date.now(), exceptionDetails: { stackTrace: frame.length ? { callFrames: frame } : undefined, text: `engine262 error when evaluating the following node:\n\n ${expr}\n\n${err.constructor.name}: ${err.message}\n${err.stack.slice(err.stack.indexOf(err.message) + err.message.length).split('\n').map((line: string) => ` ${line}`).join('\n')}\n\nFrom now on, the engine262 VM state is broken, please press the reload button.`, columnNumber: frame[0]?.columnNumber, lineNumber: frame[0]?.lineNumber, scriptId: frame[0]?.scriptId, url: frame[0]?.url, exceptionId: 0, }, }); return { result: { type: 'undefined' }, }; }); return promise; }