Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/rokucommunity/roku-debug
Browse files Browse the repository at this point in the history
…into logging-updates
  • Loading branch information
TwitchBronBron committed Apr 21, 2022
2 parents 7a59f2a + a08e84f commit 8c3da57
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 2 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ module.exports = {
'@typescript-eslint/no-unused-vars-experimental': 'off',
'@typescript-eslint/dot-notation': 'off',
'github/array-foreach': 'off',
'camelcase': 'off',
'new-cap': 'off',
'no-shadow': 'off',
'func-names': 'off'
Expand Down
56 changes: 55 additions & 1 deletion src/debugProtocol/Debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { SmartBuffer } from 'smart-buffer';
import { logger } from '../logging';
import { ERROR_CODES, UPDATE_TYPES } from '..';
import { ExecuteResponseV3 } from './responses/ExecuteResponseV3';
import { ListBreakpointsResponse } from './responses/ListBreakpointsResponse';
import { AddBreakpointsResponse } from './responses/AddBreakpointsResponse';
import { RemoveBreakpointsResponse } from './responses/RemoveBreakpointsResponse';

export class Debugger {

Expand Down Expand Up @@ -144,7 +147,6 @@ export class Debugger {
});

this.controllerClient.on('data', (buffer) => {
this.logger
if (this.unhandledData) {
this.unhandledData = Buffer.concat([this.unhandledData, buffer]);
} else {
Expand Down Expand Up @@ -279,6 +281,36 @@ export class Debugger {
}
}

public async addBreakpoints(breakpoints: BreakpointSpec[]): Promise<AddBreakpointsResponse> {
if (breakpoints?.length > 0) {
let buffer = new SmartBuffer();
buffer.writeUInt32LE(breakpoints.length); // num_breakpoints - The number of breakpoints in the breakpoints array.
breakpoints.forEach((breakpoint) => {
buffer.writeStringNT(breakpoint.filePath); // file_path - The path of the source file where the breakpoint is to be inserted.
buffer.writeUInt32LE(breakpoint.lineNumber); // line_number - The line number in the channel application code where the breakpoint is to be executed.
buffer.writeUInt32LE(breakpoint.hitCount ?? 0); // ignore_count - The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint.
});
return this.makeRequest<AddBreakpointsResponse>(buffer, COMMANDS.ADD_BREAKPOINTS);
}
return new AddBreakpointsResponse(null);
}

public async listBreakpoints(): Promise<ListBreakpointsResponse> {
return this.makeRequest<ListBreakpointsResponse>(new SmartBuffer({ size: 12 }), COMMANDS.LIST_BREAKPOINTS);
}

public async removeBreakpoints(breakpointIds: number[]): Promise<RemoveBreakpointsResponse> {
if (breakpointIds?.length > 0) {
let buffer = new SmartBuffer();
buffer.writeUInt32LE(breakpointIds.length); // num_breakpoints - The number of breakpoints in the breakpoints array.
breakpointIds.forEach((breakpointId) => {
buffer.writeUInt32LE(breakpointId); // breakpoint_ids - An array of breakpoint IDs representing the breakpoints to be removed.
});
return this.makeRequest<RemoveBreakpointsResponse>(buffer, COMMANDS.REMOVE_BREAKPOINTS);
}
return new RemoveBreakpointsResponse(null);
}

private async makeRequest<T>(buffer: SmartBuffer, command: COMMANDS, extraData?) {
this.totalRequests++;
let requestId = this.totalRequests;
Expand Down Expand Up @@ -361,6 +393,12 @@ export class Debugger {
return true;
case COMMANDS.EXECUTE:
return this.checkResponse(new ExecuteResponseV3(slicedBuffer), buffer, packetLength);
case COMMANDS.ADD_BREAKPOINTS:
return this.checkResponse(new AddBreakpointsResponse(slicedBuffer), buffer, packetLength);
case COMMANDS.LIST_BREAKPOINTS:
return this.checkResponse(new ListBreakpointsResponse(slicedBuffer), buffer, packetLength);
case COMMANDS.REMOVE_BREAKPOINTS:
return this.checkResponse(new RemoveBreakpointsResponse(slicedBuffer), buffer, packetLength);
case COMMANDS.VARIABLES:
return this.checkResponse(new VariableResponse(slicedBuffer), buffer, packetLength);
case COMMANDS.STACKTRACE:
Expand Down Expand Up @@ -553,6 +591,22 @@ export interface ProtocolVersionDetails {
errorCode: PROTOCOL_ERROR_CODES;
}

export interface BreakpointSpec {
/**
* The path of the source file where the breakpoint is to be inserted.
*/
filePath: string;
/**
* The (1-based) line number in the channel application code where the breakpoint is to be executed.
*/
lineNumber: number;
/**
* The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint.
*/
hitCount?: number;
}


export interface ConstructorOptions {
/**
* The host/ip address of the Roku
Expand Down
4 changes: 4 additions & 0 deletions src/debugProtocol/responses/AddBreakpointsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ListBreakpointsResponse } from './ListBreakpointsResponse';

//There's currently no difference between this response and the ListBreakpoints response
export class AddBreakpointsResponse extends ListBreakpointsResponse { }
133 changes: 133 additions & 0 deletions src/debugProtocol/responses/ListBreakpointsResponse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createListBreakpointsResponse, getRandomBuffer } from './responseCreationHelpers.spec';
import { expect } from 'chai';
import { ListBreakpointsResponse } from './ListBreakpointsResponse';
import { ERROR_CODES } from '../Constants';

describe('ListBreakpointsResponse', () => {
let response: ListBreakpointsResponse;
beforeEach(() => {
response = undefined;
});
it('handles empty buffer', () => {
response = new ListBreakpointsResponse(null);
//Great, it didn't explode!
expect(response.success).to.be.false;
});

it('handles undersized buffers', () => {
response = new ListBreakpointsResponse(
getRandomBuffer(0)
);
expect(response.success).to.be.false;

response = new ListBreakpointsResponse(
getRandomBuffer(1)
);
expect(response.success).to.be.false;

response = new ListBreakpointsResponse(
getRandomBuffer(11)
);
expect(response.success).to.be.false;
});

it('gracefully handles mismatched breakpoint count', () => {
const bp1 = {
breakpointId: 1,
errorCode: ERROR_CODES.OK,
hitCount: 0,
success: true
};
response = new ListBreakpointsResponse(
createListBreakpointsResponse({
requestId: 1,
num_breakpoints: 2,
breakpoints: [bp1]
}).toBuffer()
);
expect(response.success).to.be.false;
expect(response.breakpoints).to.eql([bp1]);
});

it('handles malformed breakpoint data', () => {
const bp1 = {
breakpointId: 1,
errorCode: ERROR_CODES.OK,
hitCount: 2,
success: true
};
response = new ListBreakpointsResponse(
createListBreakpointsResponse({
requestId: 1,
num_breakpoints: 2,
breakpoints: [
bp1,
{
//missing all other bp properties
breakpointId: 1
}
]
}).toBuffer()
);
expect(response.success).to.be.false;
expect(response.breakpoints).to.eql([bp1]);
});

it('handles malformed breakpoint data', () => {
const bp1 = {
breakpointId: 0,
errorCode: ERROR_CODES.OK,
success: true
};
response = new ListBreakpointsResponse(
createListBreakpointsResponse({
requestId: 1,
num_breakpoints: 2,
breakpoints: [bp1]
}).toBuffer()
);
expect(response.success).to.be.false;
//hitcount should not be set when bpId is zero
expect(response.breakpoints[0].hitCount).to.be.undefined;
//the breakpoint should not be verified if bpId === 0
expect(response.breakpoints[0].isVerified).to.be.false;
});

it('reads breakpoint data properly', () => {
const bp1 = {
breakpointId: 1,
errorCode: ERROR_CODES.OK,
hitCount: 0,
success: true
};
response = new ListBreakpointsResponse(
createListBreakpointsResponse({
requestId: 1,
breakpoints: [bp1]
}).toBuffer()
);
expect(response.success).to.be.true;
expect(response.breakpoints).to.eql([bp1]);
expect(response.breakpoints[0].isVerified).to.be.true;
});

it('reads breakpoint data properly', () => {
const bp1 = {
breakpointId: 1,
errorCode: ERROR_CODES.NOT_STOPPED,
hitCount: 0,
success: true
};
response = new ListBreakpointsResponse(
createListBreakpointsResponse({
requestId: 1,
breakpoints: [bp1]
}).toBuffer()
);
expect(
response.breakpoints[0].errorText
).to.eql(
ERROR_CODES[ERROR_CODES.NOT_STOPPED]
);
});
});
72 changes: 72 additions & 0 deletions src/debugProtocol/responses/ListBreakpointsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { SmartBuffer } from 'smart-buffer';
import { ERROR_CODES } from '../Constants';

export class ListBreakpointsResponse {

constructor(buffer: Buffer) {
// The minimum size of a request
if (buffer?.byteLength >= 12) {
try {
let bufferReader = SmartBuffer.fromBuffer(buffer);
this.requestId = bufferReader.readUInt32LE(); // request_id

// Any request id less then one is an update and we should not process it here
if (this.requestId > 0) {
this.errorCode = ERROR_CODES[bufferReader.readUInt32LE()];
this.numBreakpoints = bufferReader.readUInt32LE(); // num_breakpoints - The number of breakpoints in the breakpoints array.

// build the list of BreakpointInfo
for (let i = 0; i < this.numBreakpoints; i++) {
let breakpointInfo = new BreakpointInfo(bufferReader);
// All the necessary data was present, so keep this item
this.breakpoints.push(breakpointInfo);
}

this.readOffset = bufferReader.readOffset;
this.success = (this.breakpoints.length === this.numBreakpoints);
}
} catch (error) {
// Could not parse
}
}
}
public success = false;
public readOffset = 0;

// response fields
public requestId = -1;
public numBreakpoints: number;
public breakpoints = [] as BreakpointInfo[];
public data = -1;
public errorCode: string;
}

export class BreakpointInfo {
constructor(bufferReader: SmartBuffer) {
// breakpoint_id - The ID assigned to the breakpoint. An ID greater than 0 indicates an active breakpoint. An ID of 0 denotes that the breakpoint has an error.
this.breakpointId = bufferReader.readUInt32LE();
// error_code - Indicates whether the breakpoint was successfully returned.
this.errorCode = bufferReader.readUInt32LE();

if (this.breakpointId > 0) {
// This argument is only present if the breakpoint_id is valid.
// ignore_count - Current state, decreases as breakpoint is executed.
this.hitCount = bufferReader.readUInt32LE();
}
this.success = true;
}

public get isVerified() {
return this.breakpointId > 0;
}
public success = false;
public breakpointId: number;
public errorCode: number;
/**
* The textual description of the error code
*/
public get errorText() {
return ERROR_CODES[this.errorCode];
}
public hitCount: number;
}
4 changes: 4 additions & 0 deletions src/debugProtocol/responses/RemoveBreakpointsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ListBreakpointsResponse } from './ListBreakpointsResponse';

//There's currently no difference between this response and the ListBreakpoints response
export class RemoveBreakpointsResponse extends ListBreakpointsResponse { }
50 changes: 49 additions & 1 deletion src/debugProtocol/responses/responseCreationHelpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SmartBuffer } from 'smart-buffer';
import type { ERROR_CODES, UPDATE_TYPES } from '../Constants';

import type { BreakpointInfo } from './ListBreakpointsResponse';

interface Handshake {
magic: string;
Expand Down Expand Up @@ -96,3 +96,51 @@ export function createProtocolEventV3(protocolEvent: ProtocolEvent, extraBufferD
function addPacketLength(buffer: SmartBuffer): SmartBuffer {
return buffer.insertUInt32LE(buffer.length + 4, 0); // packet_length - The size of the packet to be sent.
}

/**
* Create a buffer for `ListBreakpointsResponse`
*/
export function createListBreakpointsResponse(params: { requestId?: number; errorCode?: number; num_breakpoints?: number; breakpoints?: Partial<BreakpointInfo>[]; extraBufferData?: Buffer }): SmartBuffer {
let buffer = new SmartBuffer();

writeIfSet(params.requestId, x => buffer.writeUInt32LE(x));
writeIfSet(params.errorCode, x => buffer.writeUInt32LE(x));

buffer.writeUInt32LE(params.num_breakpoints ?? params.breakpoints?.length ?? 0); // num_breakpoints
for (const breakpoint of params?.breakpoints ?? []) {
writeIfSet(breakpoint.breakpointId, x => buffer.writeUInt32LE(x));
writeIfSet(breakpoint.errorCode, x => buffer.writeUInt32LE(x));
writeIfSet(breakpoint.hitCount, x => buffer.writeUInt32LE(x));
}

// write any extra data for testing
writeIfSet(params.extraBufferData, x => buffer.writeBuffer(x));

return addPacketLength(buffer);
}

/**
* If the value is undefined or null, skip the callback.
* All other values will cause the callback to be called
*/
function writeIfSet<T, R>(value: T, writer: (x: T) => R, defaultValue?: T) {
if (
//if we have a value
(value !== undefined && value !== null) ||
//we don't have a value, but we have a default value
(defaultValue !== undefined && defaultValue !== null)
) {
return writer(value);
}
}

/**
* Build a buffer of `byteCount` size and fill it with random data
*/
export function getRandomBuffer(byteCount: number) {
const result = new SmartBuffer();
for (let i = 0; i < byteCount; i++) {
result.writeUInt32LE(i);
}
return result.toBuffer();
}

0 comments on commit 8c3da57

Please sign in to comment.