From 150434de9d064e7c75fb37a9e5c762e69ffcd942 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 30 Jun 2023 16:15:51 -0400 Subject: [PATCH] Drive usage based on launch config setting instead of device. Move rendezvous events out of adapters. --- src/LaunchConfiguration.ts | 13 ++-- src/RendezvousTracker.spec.ts | 29 +++++-- src/RendezvousTracker.ts | 77 +++++++++++++------ src/adapters/DebugProtocolAdapter.ts | 6 -- src/adapters/TelnetAdapter.ts | 7 -- .../BrightScriptDebugSession.spec.ts | 29 ++++++- src/debugSession/BrightScriptDebugSession.ts | 46 ++++++++--- 7 files changed, 145 insertions(+), 62 deletions(-) diff --git a/src/LaunchConfiguration.ts b/src/LaunchConfiguration.ts index c5489a54..8c775b51 100644 --- a/src/LaunchConfiguration.ts +++ b/src/LaunchConfiguration.ts @@ -253,6 +253,13 @@ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArgument * Show variables that are prefixed with a special prefix designated to be hidden */ showHiddenVariables: boolean; + + /** + * If true: turn on ECP rendezvous tracking, or turn on 8080 rendezvous tracking if ECP unsupported + * If false, turn off both. + * @default true + */ + rendezvousTracking: boolean; } export interface ComponentLibraryConfiguration { @@ -287,10 +294,4 @@ 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; } diff --git a/src/RendezvousTracker.spec.ts b/src/RendezvousTracker.spec.ts index 2e936a73..1399186d 100644 --- a/src/RendezvousTracker.spec.ts +++ b/src/RendezvousTracker.spec.ts @@ -266,22 +266,27 @@ describe('BrightScriptFileUtils ', () => { }); - afterEach(() => { + afterEach(async () => { sinon.restore(); rendezvousTrackerMock.restore(); - rendezvousTracker?.destroy(); + + //prevent hitting the network during teardown + rendezvousTracker.toggleEcpRendezvousTracking = () => Promise.resolve() as any; + rendezvousTracker['runSGLogrendezvousCommand'] = () => Promise.resolve() as any; + + await rendezvousTracker?.destroy(); }); describe('isEcpRendezvousTrackingSupported ', () => { it('works', () => { rendezvousTracker['deviceInfo']['software-version'] = '11.0.0'; - expect(rendezvousTracker.isEcpRendezvousTrackingSupported).to.be.false; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.false; rendezvousTracker['deviceInfo']['software-version'] = '11.5.0'; - expect(rendezvousTracker.isEcpRendezvousTrackingSupported).to.be.true; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.true; rendezvousTracker['deviceInfo']['software-version'] = '12.0.1'; - expect(rendezvousTracker.isEcpRendezvousTrackingSupported).to.be.true; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.true; }); }); @@ -369,26 +374,29 @@ describe('BrightScriptFileUtils ', () => { }); it('does not activate if telnet and ecp are both off', async () => { + sinon.stub(rendezvousTracker as any, 'runSGLogrendezvousCommand').returns(Promise.resolve('')); sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(false)); sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(false)); expect( - await rendezvousTracker.activateEcpTracking() + await rendezvousTracker.activate() ).to.be.false; }); it('activates if telnet is enabled but ecp is disabled', async () => { + sinon.stub(rendezvousTracker as any, 'runSGLogrendezvousCommand').returns(Promise.resolve('')); sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(false)); sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(true)); expect( - await rendezvousTracker.activateEcpTracking() + await rendezvousTracker.activate() ).to.be.true; }); it('activates if telnet is disabled but ecp is enabled', async () => { + sinon.stub(rendezvousTracker as any, 'runSGLogrendezvousCommand').returns(Promise.resolve('')); sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(true)); sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(false)); expect( - await rendezvousTracker.activateEcpTracking() + await rendezvousTracker.activate() ).to.be.true; }); }); @@ -396,6 +404,8 @@ describe('BrightScriptFileUtils ', () => { describe('processLog ', () => { it('filters out all rendezvous log lines', async () => { rendezvousTrackerMock.expects('emit').withArgs('rendezvous').once(); + rendezvousTracker['trackingSource'] = 'telnet'; + let expected = `channel: Start\nStarting data processing\nData processing completed\n`; assert.equal(await rendezvousTracker.processLog(logString), expected); assert.deepEqual(rendezvousTracker.getRendezvousHistory, expectedHistory); @@ -405,6 +415,8 @@ describe('BrightScriptFileUtils ', () => { it('does not filter out rendezvous log lines', async () => { rendezvousTrackerMock.expects('emit').withArgs('rendezvous').once(); rendezvousTracker.setConsoleOutput('full'); + rendezvousTracker['trackingSource'] = 'telnet'; + assert.equal(await rendezvousTracker.processLog(logString), logString); assert.deepEqual(rendezvousTracker.getRendezvousHistory, expectedHistory); rendezvousTrackerMock.verify(); @@ -434,6 +446,7 @@ describe('BrightScriptFileUtils ', () => { it('to reset the history data', async () => { rendezvousTrackerMock.expects('emit').withArgs('rendezvous').twice(); let expected = `channel: Start\nStarting data processing\nData processing completed\n`; + rendezvousTracker['trackingSource'] = 'telnet'; assert.equal(await rendezvousTracker.processLog(logString), expected); assert.deepEqual(rendezvousTracker.getRendezvousHistory, expectedHistory); diff --git a/src/RendezvousTracker.ts b/src/RendezvousTracker.ts index 5760967a..da76caa3 100644 --- a/src/RendezvousTracker.ts +++ b/src/RendezvousTracker.ts @@ -27,12 +27,17 @@ export class RendezvousTracker { private filterOutLogs: boolean; private rendezvousBlocks: RendezvousBlocks; private rendezvousHistory: RendezvousHistory; - private ecpTrackingEnabled = false; + + /** + * Where should the rendezvous data be tracked from? If ecp, then the ecp ping data will be reported. If telnet, then any + * rendezvous data from telnet will reported. If 'off', then no data will be reported + */ + private trackingSource: 'telnet' | 'ecp' | 'off' = 'off'; /** * Determine if the current Roku device supports the ECP rendezvous tracking feature */ - public get isEcpRendezvousTrackingSupported() { + public get doesHostSupportEcpRendezvousTracking() { return semver.gte(this.deviceInfo['software-version'] as string, '11.5.0'); } @@ -118,13 +123,18 @@ export class RendezvousTracker { * 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); + return (await this.runSGLogrendezvousCommand('status'))?.trim()?.toLowerCase() === 'on'; + } + + /** + * Run a SceneGraph logendezvous 8080 command and get the text output + */ + private async runSGLogrendezvousCommand(command: 'status' | 'on' | 'off'): Promise { + let sgDebugCommandController = new SceneGraphDebugCommandController(this.deviceInfo.host as string); try { - let logRendezvousResponse = await sgDebugCommandController.logrendezvous('status'); - return logRendezvousResponse.result.rawResponse?.trim()?.toLowerCase() === 'on'; + return (await sgDebugCommandController.logrendezvous(command)).result.rawResponse; } catch (error) { - this.logger.warn('An error occurred getting logRendezvous'); + this.logger.warn(`An error occurred running SG command "${command}"`, error); } finally { await sgDebugCommandController.end(); } @@ -138,27 +148,32 @@ export class RendezvousTracker { return ecpData.trackingEnabled; } - public async activateEcpTracking(): Promise { - //ECP tracking not supported, return early - if (!this.isEcpRendezvousTrackingSupported) { - return; - } - - let isTelnetRendezvousTrackingEnabled = false; - this.ecpTrackingEnabled = await this.getIsEcpRendezvousTrackingEnabled(); - isTelnetRendezvousTrackingEnabled = await this.getIsTelnetRendezvousTrackingEnabled(); - - if (this.ecpTrackingEnabled || isTelnetRendezvousTrackingEnabled) { + public async activate(): Promise { + //if ECP tracking is supported, turn that on + if (this.doesHostSupportEcpRendezvousTracking) { // 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; + const isEcpTrackingEnabled = untrack && track && await this.getIsEcpRendezvousTrackingEnabled(); + if (isEcpTrackingEnabled) { + this.trackingSource = 'ecp'; + this.startEcpPingTimer(); + + //disable telnet rendezvous tracking since ECP is working + try { + await this.runSGLogrendezvousCommand('off'); + } catch { } + return true; + } } - if (this.ecpTrackingEnabled) { - this.logger.log('ecp rendezvous logging is enabled'); - this.startEcpPingTimer(); + + //ECP tracking is not supported (or had an issue). Try enabling telnet rendezvous tracking (that only works with run_as_process=0, but worth a try...) + await this.runSGLogrendezvousCommand('on'); + if (await this.getIsTelnetRendezvousTrackingEnabled()) { + this.trackingSource = 'telnet'; + return true; } - return this.ecpTrackingEnabled; + return false; } /** @@ -229,7 +244,7 @@ export class RendezvousTracker { let match = /\[sg\.node\.(BLOCK|UNBLOCK)\s{0,}\] Rendezvous\[(\d+)\](?:\s\w+\n|\s\w{2}\s(.*)\((\d+)\)|[\s\w]+(\d+\.\d+)+|\s\w+)/g.exec(line); // see the following for an explanation for this regex: https://regex101.com/r/In0t7d/6 if (match) { - if (!this.ecpTrackingEnabled) { + if (this.trackingSource === 'telnet') { let [, type, id, fileName, lineNumber, duration] = match; if (type === 'BLOCK') { // detected the start of a rendezvous event @@ -400,8 +415,20 @@ export class RendezvousTracker { /** * Destroy/tear down this class */ - public destroy() { + public async destroy() { + this.emitter?.removeAllListeners(); this.stopEcpPingTimer(); + //turn off ECP rendezvous tracking + if (this.doesHostSupportEcpRendezvousTracking) { + await this.toggleEcpRendezvousTracking('untrack'); + } + + //turn off telnet rendezvous tracking + try { + await this.runSGLogrendezvousCommand('off'); + } catch (e) { + this.logger.error('Failed to disable logrendezvous over 8080', e); + } } } diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index be8c27fd..ec55ee86 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -38,11 +38,6 @@ export class DebugProtocolAdapter { this.chanperfTracker.on('chanperf', (output) => { this.emit('chanperf', output); }); - - // watch for rendezvous events - this.rendezvousTracker.on('rendezvous', (output) => { - this.emit('rendezvous', output); - }); } private logger = logger.createLogger(`[${DebugProtocolAdapter.name}]`); @@ -91,7 +86,6 @@ export class DebugProtocolAdapter { public on(eventName: 'connected', handler: (params: boolean) => void); public on(eventname: 'console-output', handler: (output: string) => void); // TODO: might be able to remove this at some point. public on(eventname: 'protocol-version', handler: (output: ProtocolVersionDetails) => void); - public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); public on(eventName: 'suspend', handler: () => void); public on(eventName: 'start', handler: () => void); diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 4f38450e..be40ba27 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -35,16 +35,10 @@ export class TelnetAdapter { this.chanperfTracker = new ChanperfTracker(); this.compileErrorProcessor = new CompileErrorProcessor(); - // watch for chanperf events this.chanperfTracker.on('chanperf', (output) => { this.emit('chanperf', output); }); - - // watch for rendezvous events - this.rendezvousTracker.on('rendezvous', (output) => { - this.emit('rendezvous', output); - }); } public logger = logger.createLogger(`[${TelnetAdapter.name}]`); @@ -83,7 +77,6 @@ export class TelnetAdapter { public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: 'connected', handler: (params: boolean) => void); public on(eventname: 'console-output', handler: (output: string) => void); - public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); public on(eventName: 'suspend', handler: () => void); public on(eventName: 'start', handler: () => void); diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index 86456f83..fee554b2 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -17,7 +17,7 @@ import { util as bscUtil, standardizePath as s } from 'brighterscript'; import { DefaultFiles } from 'roku-deploy'; import type { AddProjectParams, ComponentLibraryConstructorParams } from '../managers/ProjectManager'; import { ComponentLibraryProject, Project } from '../managers/ProjectManager'; -import * as dateFormat from 'dateformat'; +import { RendezvousTracker } from '../RendezvousTracker'; const sinon = sinonActual.createSandbox(); const tempDir = s`${__dirname}/../../.tmp`; @@ -548,6 +548,33 @@ describe('BrightScriptDebugSession', () => { }); }); + describe('initRendezvousTracking', () => { + it('clears history when disabled', async () => { + const stub = sinon.stub(session, 'sendEvent'); + const activateStub = sinon.stub(RendezvousTracker.prototype, 'activate'); + const clearHistoryStub = sinon.stub(RendezvousTracker.prototype, 'clearHistory'); + + session['launchConfiguration'].rendezvousTracking = false; + + await session['initRendezvousTracking'](); + expect(clearHistoryStub.called).to.be.true; + expect(activateStub.called).to.be.false; + }); + + it('activates when not disabled', async () => { + const stub = sinon.stub(session, 'sendEvent'); + const activateStub = sinon.stub(RendezvousTracker.prototype, 'activate'); + const clearHistoryStub = sinon.stub(RendezvousTracker.prototype, 'clearHistory'); + + session['launchConfiguration'].rendezvousTracking = undefined; + + await session['initRendezvousTracking'](); + expect(clearHistoryStub.called).to.be.true; + expect(activateStub.called).to.be.true; + + }); + }); + describe('setBreakPointsRequest', () => { let response; let args: DebugProtocol.SetBreakpointsArguments; diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 0c7e0fd4..b82879d2 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -289,10 +289,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { util.log(`Connecting to Roku via ${this.enableDebugProtocol ? 'the BrightScript debug protocol' : 'telnet'} at ${this.launchConfiguration.host}`); - this.rendezvousTracker = new RendezvousTracker(this.deviceInfo); - - // start ECP rendezvous tracking (if possible) - await this.rendezvousTracker.activateEcpTracking(); + await this.initRendezvousTracking(); this.createRokuAdapter(this.launchConfiguration.host, this.rendezvousTracker); if (!this.enableDebugProtocol) { @@ -333,11 +330,6 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.sendEvent(new ChanperfEvent(output)); }); - // Send rendezvous events to the extension - this.rokuAdapter.on('rendezvous', (output) => { - this.sendEvent(new RendezvousEvent(output)); - }); - //listen for a closed connection (shut down when received) this.rokuAdapter.on('close', (reason = '') => { if (reason === 'compileErrors') { @@ -429,6 +421,42 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } + /** + * Activate rendezvous tracking (IF enabled in the LaunchConfig) + */ + public async initRendezvousTracking() { + const timeout = 5000; + let initCompleted = false; + await Promise.race([ + util.sleep(timeout), + this._initRendezvousTracking().finally(() => { + initCompleted = true; + }) + ]); + + if (initCompleted === false) { + this.showPopupMessage(`Rendezvous tracking timed out after ${timeout}ms. Consider setting "rendezvousTracking": false in launch.json`, 'warn'); + } + } + + private async _initRendezvousTracking() { + this.rendezvousTracker = new RendezvousTracker(this.deviceInfo); + + // Send rendezvous events to the debug protocol client + this.rendezvousTracker.on('rendezvous', (output) => { + this.sendEvent(new RendezvousEvent(output)); + }); + + //clear the history so the user doesn't have leftover rendezvous data from a previous session + this.rendezvousTracker.clearHistory(); + + //if rendezvous tracking is enabled, then enable it on the device + if (this.launchConfiguration.rendezvousTracking !== false) { + // start ECP rendezvous tracking (if possible) + await this.rendezvousTracker.activate(); + } + } + /** * Anytime a roku adapter emits diagnostics, this method is called to handle it. */