import { Plugin } from 'tools/dap/plugin.js'; class InspectorManager extends Plugin { constructor(jsDebugger, dapHandler) { super('InspectorManager', 'manager'); this.variableReferences = []; this.logger.debug('Setting up InspectorManager'); jsDebugger.on('onDebuggerDetached', () => { this.logger.debug('Debugger attached'); this.variableReferences = []; }, this.name); jsDebugger.on('onContinue', () => { this.logger.debug('Continuing execution'); this.variableReferences = []; }, this.name); dapHandler.registerCommand('scopes', (req) => { this.logger.debug('Handling scopes command'); if (!jsDebugger.currentFrame) { this.logger.error('No current frame available'); return dapHandler.errorResponse(req, 'No current frame available'); } if (!jsDebugger.currentFrame.onStack || jsDebugger.currentFrame.terminated) { this.logger.error('Current frame is not on stack or is terminated'); return dapHandler.errorResponse(req, 'Current frame is not on stack or is terminated'); } const scopes = []; let frameId = req.arguments.frameId; let frame = jsDebugger.currentFrame; while (frameId > 1) { frameId--; frame = frame.onStack && !frame.terminated ? frame.older : null; if (!frame) { this.logger.error(`Invalid frameId: ${req.arguments.frameId}`); return dapHandler.errorResponse(req, 'Invalid frameId'); } } return dapHandler.successResponse(req, { 'scopes': this.createScopeAndVariableReferences(frame, req.arguments.frameId) }); }); dapHandler.registerCommand('variables', (req) => { this.logger.debug('Handling variables command'); if (!jsDebugger.currentFrame) { this.logger.error('No current frame available'); return dapHandler.errorResponse(req, 'No current frame available'); } const variables = this.expandVariableReference(req.arguments.variablesReference, jsDebugger.currentFrame); return dapHandler.successResponse(req, { 'variables': variables }); }); dapHandler.registerCommand('evaluate', (req) => { this.logger.debug('Handling evaluate command'); if (!jsDebugger.currentFrame) { this.logger.error('No current frame available'); return dapHandler.errorResponse(req, 'No current frame available'); } const expression = req.arguments.expression; if (!expression) { this.logger.error('No expression provided for evaluation'); return dapHandler.errorResponse(req, 'No expression provided for evaluation'); } try { const result = jsDebugger.currentFrame.eval(expression); if (result.return instanceof Debugger.Object) { const described = this.describeJSObjectVariable(result.return, `eval.${expression}`, { "frameId": req.arguments.frameId }, expression); described.result = described.value; return dapHandler.successResponse(req, described); } else if (result.return !== undefined) return dapHandler.successResponse(req, { 'result': JSON.stringify(result.return) || "null", }); return dapHandler.successResponse(req, { 'result': JSON.stringify(result) || "null", 'variablesReference': 0 }); } catch(error) { this.logger.error(`Error evaluating expression: ${error.message}`); return dapHandler.errorResponse(req, `Error evaluating expression: ${error.message}`); } }); } createOrUpdateVariableReference(name, data) { let index = this.variableReferences.findIndex((ref) => ref.name === name); if (index === -1) { this.variableReferences.push({ name, data }); index = this.variableReferences.length - 1; } else this.variableReferences[index].data = data; return index + 1; } createScopeAndVariableReferences(frame, frameId) { if (!frame || !frame.onStack || frame.terminated) { this.logger.error(`Invalid frame: ${frameId}`); return []; } const scopes = []; let environment = frame.environment; let depth = 0; while (environment !== null) { this.logger.debug(`Creating scope and variable references for environment: ${environment.type}, depth: ${depth}, frameId: ${frameId}, frameType: ${frame.type}`); const labelParts = []; const scope = {}; scope.variablesReference = 0; if (environment.optimizedOut) labelParts.push('(Optimized Out)'); switch (environment.type) { case 'object': labelParts.push('Global'); break; case 'width': labelParts.push('With Scope'); break; case 'declarative': if (environment.scopeKind === 'function') labelParts.push('Closure'); else if (environment.scopeKind === 'function lexical') labelParts.push('Function Block'); else if (environment.scopeKind) labelParts.push(environment.scopeKind.charAt(0).toUpperCase() + environment.scopeKind.slice(1)); else labelParts.push('Declarative'); break; default: labelParts.push(environment.type || "Unknown"); } if (environment.calleeScript) labelParts.push(`(Script: ${environment.calleeScript.displayName || environment.calleeScript.name || 'unknown'})`); scope.name = labelParts.join(' '); const names = (typeof environment.names === "function") ? environment.names() : []; if (depth === 0 && frame.this instanceof Debugger.Object) names.unshift('this'); scope.variablesReference = this.createOrUpdateVariableReference(`${frameId}.scope.${depth}`, { "values": names, "frameId": frameId, }); scope.expensive = false; scopes.push(scope); environment = environment.parent; depth++; } return scopes; } describeJSObjectCallable(jsObject, varName) { if (!jsObject || !(jsObject instanceof Debugger.Object) || !jsObject.callable) { this.logger.error('Invalid JS Object for callable description'); return { 'name': varName || '(unknown)', 'value': 'undefined', 'variablesReference': 0 }; } const variable = { 'name': varName, 'variablesReference': 0, 'presentationHint': { 'kind': 'method', } }; const funcName = jsObject.displayName || jsObject.name || 'anonymous'; let funcType = 'Function'; if (jsObject.isArrowFunction) funcType = 'Arrow Function'; else if (jsObject.isClassConstructor) funcType = 'Class Constructor'; else if (jsObject.isGeneratorFunction) funcType = 'Generator Function'; else if (jsObject.isAsyncFunction) funcType = 'Async Function'; else if (jsObject.isBoundFunction) funcType = 'Bound Function'; const paramNames = (jsObject.parameterNames || []).join(', '); variable.type = funcType; variable.value = `[${funcType}] ${funcName}(${paramNames})`; return variable; } describeJSObjectVariable(jsObject, varRefName, varReference, varName) { if (!jsObject || !(jsObject instanceof Debugger.Object)) { this.logger.error('Invalid JS Object for description'); return { 'name': varName || '(unknown)', 'value': 'undefined', 'variablesReference': 0 }; } if (jsObject.callable) return this.describeJSObjectCallable(jsObject, varName); const variable = { 'name': varName, 'variablesReference': 0, }; switch (jsObject.class) { case 'Array': { const length = jsObject.getOwnPropertyDescriptor("length")?.value || 0; variable.value = `[Array] (${length})`; break; } case 'Date': { const str = jsObject.unsafeDereference().toString(); variable.value = `[Date] ${str}`; break; } case 'RegExp': { const str = jsObject.unsafeDereference().toString(); variable.value = `[RegExp] ${str}`; break; } case 'Map': case 'Set': variable.value = `[${jsObject.class}] (${jsObject.getOwnPropertyNames().length})`; break; case 'String': case 'Number': case 'Boolean': { const val = jsObject.unsafeDereference().valueOf(); variable.value = `[${jsObject.class}] ${val}`; break; } default: { let label = jsObject.displayName || jsObject.class; if (jsObject.isProxy) label = `[Proxy ${label}]`; else if (jsObject.isPromise) label = `[Promise ${label}]`; else if (!jsObject.unsafeDereference) label = `[Opaque ${label}]`; else if (jsObject.proto) { const proto = jsObject.proto.displayName || jsObject.proto.class || 'unknown'; label = `[${label} (Prototype: ${proto})]`; } else label = `[${label}]`; variable.value = label; } } const objectProperties = jsObject.getOwnPropertyNames(); if (objectProperties.length === 0) return variable; variable.variablesReference = this.createOrUpdateVariableReference(varRefName, { 'values': objectProperties, 'jsObject': jsObject, 'frameId': varReference.frameId }); return variable; } expandVariableReference(variableReferenceIndex, frame) { if (variableReferenceIndex === 0 || this.variableReferences.length < variableReferenceIndex - 1) { this.logger.warn(`Invalid variable reference index: ${variableReferenceIndex}`); return []; } const varReferenceRef = this.variableReferences[variableReferenceIndex - 1]; const varReference = varReferenceRef.data; const variables = []; this.logger.debug(`Expanding variable reference ${variableReferenceIndex}: ${uneval(varReferenceRef)}`); let frameId = varReference.frameId; while (frameId > 1) { frame = frame.onStack && !frame.terminated ? frame.older : null; if (!frame) { this.logger.error(`Invalid frameId: ${req.arguments.frameId}`); return dapHandler.errorResponse(req, 'Invalid frameId'); } frameId--; } for (const varName of varReference.values) { this.logger.debug(`Expanding variable: ${varName}`); let value; if (varReference.jsObject) value = varReference.jsObject.getOwnPropertyDescriptor(varName)?.value; else if (varName === 'this' && frame.this instanceof Debugger.Object) value = frame.this; else { const valueEnv = frame.environment.find(varName); if (valueEnv === null) { this.logger.warn(`Could not find variable ${varName}`); continue; } if (valueEnv.type === 'object' || valueEnv.type === 'with') value = valueEnv.object.getOwnPropertyDescriptor(varName)?.value; else value = valueEnv.getVariable(varName); } if (value instanceof Debugger.Object) { variables.push(this.describeJSObjectVariable(value, `${varReferenceRef.name}.${varName}`, varReference, varName)); continue; } variables.push({ 'name': varName, 'variablesReference': 0, 'value': JSON.stringify(value) || "null" }); } return variables; } } export default InspectorManager;