diff --git a/src/adapters/DebugProtocolAdapter.spec.ts b/src/adapters/DebugProtocolAdapter.spec.ts index 38ba6506..2f430966 100644 --- a/src/adapters/DebugProtocolAdapter.spec.ts +++ b/src/adapters/DebugProtocolAdapter.spec.ts @@ -542,7 +542,9 @@ describe('DebugProtocolAdapter', function() { container?.children.map(x => x.evaluateName) ).to.eql([ 'person["name"]', - 'person["age"]' + 'person["age"]', + //For arrays or objects with children we add a $count property for the number of items or children + '2' ]); //the top level object should be an AA expect(container.type).to.eql(VariableType.AssociativeArray); @@ -559,4 +561,61 @@ describe('DebugProtocolAdapter', function() { expect(container.children[1].children).to.eql([]); }); }); + + it('creates evaluate container with keyType string', () => { + let container = adapter['createEvaluateContainer']( + { + isConst: false, + isContainer: true, + refCount: 1, + type: VariableType.AssociativeArray, + value: undefined, + childCount: 1, + keyType: VariableType.String, + name: 'm', + children: [{ + isConst: false, + isContainer: true, + refCount: 1, + type: VariableType.AssociativeArray, + value: undefined, + childCount: 0, + keyType: VariableType.String, + name: 'child' + }] + }, + 'm', + undefined + ); + expect(container.children[0].evaluateName).to.eql('m["child"]'); + }); + + it('creates evaluate container with keyType integer', () => { + let container = adapter['createEvaluateContainer']( + { + isConst: false, + isContainer: true, + refCount: 1, + type: VariableType.AssociativeArray, + value: undefined, + childCount: 1, + keyType: VariableType.Integer, + name: 'm', + children: [{ + isConst: false, + isContainer: true, + refCount: 1, + type: VariableType.AssociativeArray, + value: undefined, + childCount: 0, + keyType: VariableType.Integer, + name: 'child' + }] + }, + 'm', + undefined + ); + expect(container.children[0].evaluateName).to.eql('m[0]'); + }); + }); diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 8bc0b39d..c9617ca4 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -21,6 +21,7 @@ import { VariableType } from '../debugProtocol/events/responses/VariablesRespons import type { TelnetAdapter } from './TelnetAdapter'; import type { DeviceInfo } from 'roku-deploy'; import type { ThreadsResponse } from '../debugProtocol/events/responses/ThreadsResponse'; +import { insertCustomVariables } from './customVariableUtils'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. @@ -515,7 +516,7 @@ export class DebugProtocolAdapter { let thread = await this.getThreadByThreadId(threadIndex); let frames: StackFrame[] = []; let stackTraceData = await this.client.getStackTrace(threadIndex); - for (let i = 0; i < stackTraceData?.data?.entries?.length ?? 0; i++) { + for (let i = 0; i < (stackTraceData?.data?.entries?.length ?? 0); i++) { let frameData = stackTraceData.data.entries[i]; let stackFrame: StackFrame = { frameId: this.nextFrameId++, @@ -598,6 +599,7 @@ export class DebugProtocolAdapter { //this is the top-level container, so there are no parent keys to this entry undefined ); + await insertCustomVariables(this, expression, container); return container; } } @@ -636,7 +638,7 @@ export class DebugProtocolAdapter { * @param name the name of this variable. For example, `alpha.beta.charlie`, this value would be `charlie`. For local vars, this is the root variable name (i.e. `alpha`) * @param parentEvaluateName the string used to derive the parent, _excluding_ this variable's name (i.e. `alpha.beta` or `alpha[0]`) */ - private createEvaluateContainer(variable: Variable, name: string, parentEvaluateName: string) { + private createEvaluateContainer(variable: Variable, name: string | number, parentEvaluateName: string) { let value; let variableType = variable.type; if (variable.value === null) { @@ -658,7 +660,7 @@ export class DebugProtocolAdapter { //build full evaluate name for this var. (i.e. `alpha["beta"]` + ["charlie"]` === `alpha["beta"]["charlie"]`) let evaluateName: string; if (!parentEvaluateName?.trim()) { - evaluateName = name; + evaluateName = name?.toString(); } else if (typeof name === 'string') { evaluateName = `${parentEvaluateName}["${name}"]`; } else if (typeof name === 'number') { @@ -666,7 +668,7 @@ export class DebugProtocolAdapter { } let container: EvaluateContainer = { - name: name ?? '', + name: name?.toString() ?? '', evaluateName: evaluateName ?? '', type: variableType ?? '', value: value ?? null, @@ -685,7 +687,7 @@ export class DebugProtocolAdapter { const childVariable = variable.children[i]; const childContainer = this.createEvaluateContainer( childVariable, - container.keyType === KeyType.integer ? i.toString() : childVariable.name, + container.keyType === KeyType.integer ? i : childVariable.name, container.evaluateName ); container.children.push(childContainer); @@ -736,7 +738,7 @@ export class DebugProtocolAdapter { return []; } - for (let i = 0; i < threadsResponse.data?.threads?.length ?? 0; i++) { + for (let i = 0; i < (threadsResponse.data?.threads?.length ?? 0); i++) { let threadInfo = threadsResponse.data.threads[i]; let thread = { // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary. @@ -902,7 +904,7 @@ export class DebugProtocolAdapter { //if the response was successful, and we have the correct number of breakpoints in the response if (response.data.errorCode === ErrorCode.OK && response?.data?.breakpoints?.length === breakpoints.length) { - for (let i = 0; i < response?.data?.breakpoints?.length ?? 0; i++) { + for (let i = 0; i < (response?.data?.breakpoints?.length ?? 0); i++) { const deviceBreakpoint = response.data.breakpoints[i]; if (typeof deviceBreakpoint?.id === 'number') { diff --git a/src/adapters/customVariableUtils.ts b/src/adapters/customVariableUtils.ts new file mode 100644 index 00000000..1899e180 --- /dev/null +++ b/src/adapters/customVariableUtils.ts @@ -0,0 +1,40 @@ +import * as semver from 'semver'; +import { KeyType } from './DebugProtocolAdapter'; +import type { DebugProtocolAdapter, EvaluateContainer } from './DebugProtocolAdapter'; + +/** + * Insert custom variables into the `EvaluateContainer`. Most of these are for compatibility with older versions of the BrightScript debug protocol, + * but occasionally can be for adding new functionality for properties that don't exist in the debug protocol. Some of these will run `evaluate` commands + * to look up the data for the custom variables. + */ +export async function insertCustomVariables(adapter: DebugProtocolAdapter, expression: string, container: EvaluateContainer): Promise { + if (semver.satisfies(adapter?.activeProtocolVersion, '<3.3.0')) { + if (container?.value?.startsWith('roSGNode')) { + let nodeChildren = { + name: '$children', + type: 'roArray', + highLevelType: 'array', + keyType: KeyType.integer, + presentationHint: 'virtual', + evaluateName: `${expression}.getChildren(-1, 0)`, + children: [] + }; + container.children.push(nodeChildren); + } + if (container.elementCount > 0 || container.type === 'Array') { + let nodeCount = { + name: '$count', + evaluateName: container.elementCount.toString(), + type: 'number', + highLevelType: undefined, + keyType: undefined, + presentationHint: 'virtual', + value: container.elementCount.toString(), + elementCount: undefined, + children: [] + }; + container.children.push(nodeCount); + } + } + await Promise.resolve(); +} diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 3b4c1666..eb131b00 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -1108,7 +1108,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { const vars = await (this.rokuAdapter as TelnetAdapter).getScopeVariables(); for (const varName of vars) { - let result = await this.rokuAdapter.getVariable(varName, -1); + let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: varName, frameId: -1 }, util.getVariablePath(varName)); + let result = await this.rokuAdapter.getVariable(evalArgs.expression, -1); let tempVar = this.getVariableFromResult(result, -1); childVariables.push(tempVar); } @@ -1126,7 +1127,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { logger.log('variable', v); //query for child vars if we haven't done it yet. if (v.childVariables.length === 0) { - let result = await this.rokuAdapter.getVariable(v.evaluateName, v.frameId); + let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: v.evaluateName, frameId: v.frameId }, util.getVariablePath(v.evaluateName)); + let result = await this.rokuAdapter.getVariable(evalArgs.expression, v.frameId); let tempVar = this.getVariableFromResult(result, v.frameId); tempVar.frameId = v.frameId; v.childVariables = tempVar.childVariables; @@ -1207,41 +1209,26 @@ export class BrightScriptDebugSession extends BaseDebugSession { //is at debugger prompt } else { - let variablePath = util.getVariablePath(args.expression); - if (!variablePath && util.isAssignableExpression(args.expression)) { - let varIndex = this.getNextVarIndex(args.frameId); - let arrayVarName = this.tempVarPrefix + 'eval'; - if (varIndex === 0) { - const response = await this.rokuAdapter.evaluate(`${arrayVarName} = []`, args.frameId); - console.log(response); - } - let statement = `${arrayVarName}[${varIndex}] = ${args.expression}`; - args.expression = `${arrayVarName}[${varIndex}]`; - let commandResults = await this.rokuAdapter.evaluate(statement, args.frameId); - if (commandResults.type === 'error') { - throw new Error(commandResults.message); - } - variablePath = [arrayVarName, varIndex.toString()]; - } + let { evalArgs, variablePath } = await this.evaluateExpressionToTempVar(args, util.getVariablePath(args.expression)); //if we found a variable path (e.g. ['a', 'b', 'c']) then do a variable lookup because it's faster and more widely supported than `evaluate` if (variablePath) { - let refId = this.getEvaluateRefId(args.expression, args.frameId); + let refId = this.getEvaluateRefId(evalArgs.expression, evalArgs.frameId); let v: AugmentedVariable; //if we already looked this item up, return it if (this.variables[refId]) { v = this.variables[refId]; } else { - let result = await this.rokuAdapter.getVariable(args.expression, args.frameId); + let result = await this.rokuAdapter.getVariable(evalArgs.expression, evalArgs.frameId); if (!result) { throw new Error('Error: unable to evaluate expression'); } - v = this.getVariableFromResult(result, args.frameId); + v = this.getVariableFromResult(result, evalArgs.frameId); //TODO - testing something, remove later // eslint-disable-next-line camelcase v.request_seq = response.request_seq; - v.frameId = args.frameId; + v.frameId = evalArgs.frameId; } response.body = { result: v.value, @@ -1253,13 +1240,13 @@ export class BrightScriptDebugSession extends BaseDebugSession { //run an `evaluate` call } else { - let commandResults = await this.rokuAdapter.evaluate(args.expression, args.frameId); + let commandResults = await this.rokuAdapter.evaluate(evalArgs.expression, evalArgs.frameId); commandResults.message = util.trimDebugPrompt(commandResults.message); if (args.context !== 'watch') { //clear variable cache since this action could have side-effects this.clearState(); - this.sendInvalidatedEvent(null, args.frameId); + this.sendInvalidatedEvent(null, evalArgs.frameId); } //if the adapter captured output (probably only telnet), print it to the vscode debug console if (typeof commandResults.message === 'string') { @@ -1290,6 +1277,26 @@ export class BrightScriptDebugSession extends BaseDebugSession { deferred.resolve(); } + private async evaluateExpressionToTempVar(args: DebugProtocol.EvaluateArguments, variablePath: string[]): Promise< { evalArgs: DebugProtocol.EvaluateArguments; variablePath: string[] } > { + let returnVal = { evalArgs: args, variablePath }; + if (!variablePath && util.isAssignableExpression(args.expression)) { + let varIndex = this.getNextVarIndex(args.frameId); + let arrayVarName = this.tempVarPrefix + 'eval'; + if (varIndex === 0) { + const response = await this.rokuAdapter.evaluate(`${arrayVarName} = []`, args.frameId); + console.log(response); + } + let statement = `${arrayVarName}[${varIndex}] = ${args.expression}`; + returnVal.evalArgs.expression = `${arrayVarName}[${varIndex}]`; + let commandResults = await this.rokuAdapter.evaluate(statement, args.frameId); + if (commandResults.type === 'error') { + throw new Error(commandResults.message); + } + returnVal.variablePath = [arrayVarName, varIndex.toString()]; + } + return returnVal; + } + /** * Called when the host stops debugging * @param response