Skip to content

Commit

Permalink
Fix bug with telnet and ecp mismatch
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Jun 29, 2023
1 parent 0cb6065 commit bfaef31
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 74 deletions.
11 changes: 7 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"replace-in-file": "^6.3.2",
"replace-last": "^1.2.6",
"roku-deploy": "^3.10.2",
"semver": "^7.3.5",
"semver": "^7.5.3",
"serialize-error": "^8.1.0",
"smart-buffer": "^4.2.0",
"source-map": "^0.7.4",
Expand Down
6 changes: 6 additions & 0 deletions src/LaunchConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,10 @@ export interface ComponentLibraryConfiguration {
* This is an absolute path to the TrackerTask.xml file to be injected into the component library during a debug session.
*/
raleTrackerTaskFileLocation: string;
/**
* If true, turn on ECP tracking, unless unsupported, then turn on 8080 tracking.
* If false, turn off both.
* TODO - this isn't actually implemented yet
*/
rendezvousTracking: boolean;
}
117 changes: 112 additions & 5 deletions src/RendezvousTracker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const sinon = createSandbox();
import { assert, expect } from 'chai';
import type { RendezvousHistory } from './RendezvousTracker';
import { RendezvousTracker } from './RendezvousTracker';
import { SceneGraphDebugCommandController } from './SceneGraphDebugCommandController';

describe('BrightScriptFileUtils ', () => {
let rendezvousTracker: RendezvousTracker;
Expand Down Expand Up @@ -268,24 +269,130 @@ describe('BrightScriptFileUtils ', () => {
afterEach(() => {
sinon.restore();
rendezvousTrackerMock.restore();
rendezvousTracker?.destroy();
});

describe('hasMinVersion ', () => {
describe('isEcpRendezvousTrackingSupported ', () => {
it('works', () => {
expect(rendezvousTracker.hasMinVersion('11.0.0')).to.equal(false);
expect(rendezvousTracker.hasMinVersion('11.5.0')).to.equal(true);
expect(rendezvousTracker.hasMinVersion('12.0.1')).to.equal(true);
rendezvousTracker['deviceInfo']['software-version'] = '11.0.0';
expect(rendezvousTracker.isEcpRendezvousTrackingSupported).to.be.false;

rendezvousTracker['deviceInfo']['software-version'] = '11.5.0';
expect(rendezvousTracker.isEcpRendezvousTrackingSupported).to.be.true;

rendezvousTracker['deviceInfo']['software-version'] = '12.0.1';
expect(rendezvousTracker.isEcpRendezvousTrackingSupported).to.be.true;
});
});

describe('on', () => {
it('supports unsubscribing', () => {
const spy = sinon.spy();
const disconnect = rendezvousTracker.on('rendezvous', spy);
rendezvousTracker['emit']('rendezvous', {});
rendezvousTracker['emit']('rendezvous', {});
expect(spy.callCount).to.eql(2);
disconnect();
expect(spy.callCount).to.eql(2);
//disconnect again to fix code coverage
delete rendezvousTracker['emitter'];
disconnect();
});
});

describe('getIsTelnetRendezvousTrackingEnabled', () => {
async function doTest(rawResponse: string, expectedValue: boolean) {
const stub = sinon.stub(SceneGraphDebugCommandController.prototype, 'logrendezvous').returns(Promise.resolve({
result: {
rawResponse: 'on\n'
}
} as any));
expect(
await rendezvousTracker.getIsTelnetRendezvousTrackingEnabled()
).to.be.true;
stub.restore();
}

it('handles various responses', async () => {
await doTest('on', true);
await doTest('on\n', true);
await doTest('on \n', true);
await doTest('off', false);
await doTest('off\n', false);
await doTest('off \n', false);
});

it('does not crash on missing response', async () => {
await doTest(undefined, true);
});

it('logs an error', async () => {
const stub = sinon.stub(rendezvousTracker['logger'], 'warn');
sinon.stub(
SceneGraphDebugCommandController.prototype, 'logrendezvous'
).returns(
Promise.reject(new Error('crash'))
);
await rendezvousTracker.getIsTelnetRendezvousTrackingEnabled();
expect(stub.called).to.be.true;
});
});

describe('startEcpPingTimer', () => {
it('only sets the timer once', () => {
rendezvousTracker.startEcpPingTimer();
const ecpPingTimer = rendezvousTracker['ecpPingTimer'];
rendezvousTracker.startEcpPingTimer();
//the timer reference shouldn't have changed
expect(ecpPingTimer).to.eql(rendezvousTracker['ecpPingTimer']);
//stop the timer
rendezvousTracker.stopEcpPingTimer();
expect(rendezvousTracker['ecpPingTimer']).to.be.undefined;
//stopping while stopped is a noop
rendezvousTracker.stopEcpPingTimer();
});
});

describe('pingEcpRendezvous ', () => {
it('works', async() => {
it('works', async () => {
sinon.stub(rendezvousTracker, 'getEcpRendezvous').returns(Promise.resolve({ 'trackingEnabled': true, 'items': [{ 'id': '1403', 'startTime': '97771301', 'endTime': '97771319', 'lineNumber': '11', 'file': 'pkg:/components/Tasks/GetSubReddit.brs' }, { 'id': '1404', 'startTime': '97771322', 'endTime': '97771322', 'lineNumber': '15', 'file': 'pkg:/components/Tasks/GetSubReddit.brs' }] }));
await rendezvousTracker.pingEcpRendezvous();
expect(rendezvousTracker['rendezvousHistory']).to.eql({ 'hitCount': 2, 'occurrences': { 'pkg:/components/Tasks/GetSubReddit.brs': { 'occurrences': { '11': { 'clientLineNumber': 11, 'clientPath': '/components/Tasks/GetSubReddit.brs', 'hitCount': 1, 'totalTime': 0.018, 'type': 'lineInfo' }, '15': { 'clientLineNumber': 15, 'clientPath': '/components/Tasks/GetSubReddit.brs', 'hitCount': 1, 'totalTime': 0, 'type': 'lineInfo' } }, 'hitCount': 2, 'totalTime': 0.018, 'type': 'fileInfo', 'zeroCostHitCount': 1 } }, 'totalTime': 0.018, 'type': 'historyInfo', 'zeroCostHitCount': 1 });
});
});

describe('activateEcpTracking', () => {
beforeEach(() => {
sinon.stub(rendezvousTracker, 'pingEcpRendezvous').returns(Promise.resolve());
sinon.stub(rendezvousTracker, 'startEcpPingTimer').callsFake(() => { });
sinon.stub(rendezvousTracker, 'toggleEcpRendezvousTracking').returns(Promise.resolve(true));
});

it('does not activate if telnet and ecp are both off', async () => {
sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(false));
sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(false));
expect(
await rendezvousTracker.activateEcpTracking()
).to.be.false;
});

it('activates if telnet is enabled but ecp is disabled', async () => {
sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(false));
sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(true));
expect(
await rendezvousTracker.activateEcpTracking()
).to.be.true;
});

it('activates if telnet is disabled but ecp is enabled', async () => {
sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(true));
sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(false));
expect(
await rendezvousTracker.activateEcpTracking()
).to.be.true;
});
});

describe('processLog ', () => {
it('filters out all rendezvous log lines', async () => {
rendezvousTrackerMock.expects('emit').withArgs('rendezvous').once();
Expand Down
148 changes: 90 additions & 58 deletions src/RendezvousTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import * as path from 'path';
import * as replaceLast from 'replace-last';
import type { SourceLocation } from './managers/LocationManager';
import { logger } from './logging';
import axios from 'axios';
import { SceneGraphDebugCommandController } from './SceneGraphDebugCommandController';
import * as xml2js from 'xml2js';
import * as request from 'request';
import { util } from './util';
import * as semver from 'semver';

const minVersion = '11.5.0';
const telnetRendezvousString = 'on\n';

export class RendezvousTracker {
Expand All @@ -29,6 +29,13 @@ export class RendezvousTracker {
private rendezvousHistory: RendezvousHistory;
private ecpTrackingEnabled = false;

/**
* Determine if the current Roku device supports the ECP rendezvous tracking feature
*/
public get isEcpRendezvousTrackingSupported() {
return semver.gte(this.deviceInfo['software-version'] as string, '11.5.0');
}

public logger = logger.createLogger(`[${RendezvousTracker.name}]`);
public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void);
public on(eventName: string, handler: (payload: any) => void) {
Expand Down Expand Up @@ -76,10 +83,21 @@ export class RendezvousTracker {
this.emit('rendezvous', this.rendezvousHistory);
}

private ecpPingTimer: NodeJS.Timer;

public startEcpPingTimer(): void {
setInterval(() => {
void this.pingEcpRendezvous();
}, 1000);
if (!this.ecpPingTimer) {
this.ecpPingTimer = setInterval(() => {
void this.pingEcpRendezvous();
}, 1000);
}
}

public stopEcpPingTimer() {
if (this.ecpPingTimer) {
clearInterval(this.ecpPingTimer);
this.ecpPingTimer = undefined;
}
}

public async pingEcpRendezvous(): Promise<void> {
Expand All @@ -96,55 +114,60 @@ export class RendezvousTracker {
this.emit('rendezvous', this.rendezvousHistory);
}

public async checkForEcpTracking(): Promise<void> {
let currVersion = <string> this.deviceInfo['software-version'];
let host = <string> this.deviceInfo.host;
let telnetRendezvousTracking = false;
if (this.hasMinVersion(currVersion)) {
let ecpData = await this.getEcpRendezvous();
this.ecpTrackingEnabled = ecpData.trackingEnabled;
if (!this.ecpTrackingEnabled) {
let connection = new SceneGraphDebugCommandController(host);
try {
let logRendezvousResponse = await connection.logrendezvous('status');
telnetRendezvousTracking = logRendezvousResponse.result.rawResponse.endsWith(telnetRendezvousString);
} catch (error) {
this.logger.warn('An error occurred getting logRendezvous');
}
await connection.end();
}
if (telnetRendezvousTracking || this.ecpTrackingEnabled) {
// Toggle ECP tracking off and on to clear the log and then continue tracking
let untrack = await this.toggleEcpTracking('untrack');
let track = await this.toggleEcpTracking('track');
this.ecpTrackingEnabled = untrack && track;
}
if (this.ecpTrackingEnabled) {
this.logger.log('ecp rendezvous logging is enabled');
this.startEcpPingTimer();
}
/**
* Determine if rendezvous tracking is enabled via the 8080 telnet command
*/
public async getIsTelnetRendezvousTrackingEnabled() {
let host = this.deviceInfo.host as string;
let sgDebugCommandController = new SceneGraphDebugCommandController(host);
try {
let logRendezvousResponse = await sgDebugCommandController.logrendezvous('status');
return logRendezvousResponse.result.rawResponse?.trim()?.toLowerCase() === 'on';
} catch (error) {
this.logger.warn('An error occurred getting logRendezvous');
} finally {
await sgDebugCommandController.end();
}
}

public hasMinVersion(currVersion: string): boolean {
const currVersionArr = currVersion.split('.').map((n) => parseInt(n));
const minimumVersionArr = minVersion.split('.').map((n) => parseInt(n));
/**
* Determine if rendezvous tracking is enabled via the ECP command
*/
public async getIsEcpRendezvousTrackingEnabled() {
let ecpData = await this.getEcpRendezvous();
return ecpData.trackingEnabled;
}

public async activateEcpTracking(): Promise<boolean> {
//ECP tracking not supported, return early
if (!this.isEcpRendezvousTrackingSupported) {
return;
}

// Compare major, minor, and patch versions, loop if versions are equal
for (let i = 0; i < 3; i++) {
if (currVersionArr[i] < minimumVersionArr[i]) {
return false;
} else if (currVersionArr[i] > minimumVersionArr[i]) {
return true;
}
let isTelnetRendezvousTrackingEnabled = false;
this.ecpTrackingEnabled = await this.getIsEcpRendezvousTrackingEnabled();
isTelnetRendezvousTrackingEnabled = await this.getIsTelnetRendezvousTrackingEnabled();

if (this.ecpTrackingEnabled || isTelnetRendezvousTrackingEnabled) {
// Toggle ECP tracking off and on to clear the log and then continue tracking
let untrack = await this.toggleEcpRendezvousTracking('untrack');
let track = await this.toggleEcpRendezvousTracking('track');
this.ecpTrackingEnabled = untrack && track;
}
if (this.ecpTrackingEnabled) {
this.logger.log('ecp rendezvous logging is enabled');
this.startEcpPingTimer();
}
return true;
return this.ecpTrackingEnabled;
}

/**
* Get the response from an ECP sgrendezvous request from the Roku
*/
public async getEcpRendezvous(): Promise<EcpRendezvousData> {
// Send rendezvous query to ECP
const rendezvousQuery = await axios.get(`http://${this.deviceInfo.host}:${this.deviceInfo.remotePort}/query/sgrendezvous`);
let rendezvousQueryData = rendezvousQuery.data;
const rendezvousQuery = await util.httpGet(`http://${this.deviceInfo.host}:${this.deviceInfo.remotePort}/query/sgrendezvous`);
let rendezvousQueryData = rendezvousQuery.body;
let ecpData: EcpRendezvousData = {
trackingEnabled: false,
items: []
Expand Down Expand Up @@ -174,19 +197,21 @@ export class RendezvousTracker {
return ecpData;
}

public async toggleEcpTracking(toggle: string): Promise<boolean> {
// Send rendezvous query to ECP
const url = `http://${this.deviceInfo.host}:${this.deviceInfo.remotePort}/sgrendezvous/${toggle}`;
const data = '';
let results: boolean = await new Promise((resolve, reject) => {
request.post(url, { body: data }, (err, resp, body) => {
if (err) {
return reject(err);
}
resolve(true);
});
});
return results;
/**
* Enable/Disable ECP Rendezvous tracking on the Roku device
* @returns true if successful, false if there was an issue setting the value
*/
public async toggleEcpRendezvousTracking(toggle: 'track' | 'untrack'): Promise<boolean> {
try {
const response = await util.httpPost(
`http://${this.deviceInfo.host}:${this.deviceInfo.remotePort}/sgrendezvous/${toggle}`,
//not sure if we need this, but it works...so probably better to just leave it here
{ body: '' }
);
return true;
} catch (e) {
return false;
}
}

/**
Expand Down Expand Up @@ -371,6 +396,13 @@ export class RendezvousTracker {
private getTime(duration?: string): number {
return duration ? parseFloat(duration) : 0.000;
}

/**
* Destroy/tear down this class
*/
public destroy() {
this.stopEcpPingTimer();
}
}

export interface RendezvousHistory {
Expand Down
Loading

0 comments on commit bfaef31

Please sign in to comment.