Skip to content

Commit

Permalink
Add children roSGNode containers (#192)
Browse files Browse the repository at this point in the history
* Add children to the children of roSGNode containers. Make the evaluation of temparory variables a helper function

* Fix lint issues

* Move lint fixes

* Rename evalutTemporaryVariables to evaluateExpressionToTempVar

* Add the [[count]] value to the variables debug panel

* Fix unit test

* Change counts type to "number" and make it greyed out with the "virtual" presentationHint

* Allow name to be a string or number when creating containers

* Set the [[count]] to zero if there are no elements and the type is "Array"

* Move logic for adding new variables into a helper file / function

* Return the container object

* Change the gate keeping logic to a nested if block instead of an early out if check

* Use $ instead of [[ ]]

* Add clarity to a test why a `2` showed up

* Fix some lint errors in loops

* Rename `insertCustomVariablesHelpers` and make it async

* Make `createEvaluateContainer` `name` only accept a string

* Rename custom variable utils file

* Revert integer thing.

* Add unit tests for createEvaulateContainer with different children types

* Add clarifying comment

---------

Co-authored-by: Bronley Plumb <[email protected]>
  • Loading branch information
Christian-Holbrook and TwitchBronBron authored Dec 3, 2024
1 parent 41e5157 commit cc32b3c
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 32 deletions.
61 changes: 60 additions & 1 deletion src/adapters/DebugProtocolAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]');
});

});
16 changes: 9 additions & 7 deletions src/adapters/DebugProtocolAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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++,
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -658,15 +660,15 @@ 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') {
evaluateName = `${parentEvaluateName}[${name}]`;
}

let container: EvaluateContainer = {
name: name ?? '',
name: name?.toString() ?? '',
evaluateName: evaluateName ?? '',
type: variableType ?? '',
value: value ?? null,
Expand All @@ -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);
Expand Down Expand Up @@ -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 = <Thread>{
// NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
Expand Down Expand Up @@ -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') {
Expand Down
40 changes: 40 additions & 0 deletions src/adapters/customVariableUtils.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
if (semver.satisfies(adapter?.activeProtocolVersion, '<3.3.0')) {
if (container?.value?.startsWith('roSGNode')) {
let nodeChildren = <EvaluateContainer>{
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 = <EvaluateContainer>{
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();
}
55 changes: 31 additions & 24 deletions src/debugSession/BrightScriptDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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') {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit cc32b3c

Please sign in to comment.