diff --git a/src/RokuECP.spec.ts b/src/RokuECP.spec.ts
new file mode 100644
index 00000000..c285c3d5
--- /dev/null
+++ b/src/RokuECP.spec.ts
@@ -0,0 +1,493 @@
+import { expect } from 'chai';
+import { createSandbox } from 'sinon';
+import { describe } from 'mocha';
+import type { EcpAppStateData, EcpRegistryData } from './RokuECP';
+import { AppState, EcpStatus, rokuECP } from './RokuECP';
+import { util } from './util';
+import { expectThrowsAsync } from './testHelpers.spec';
+
+const sinon = createSandbox();
+
+
+describe('RokuECP', () => {
+
+ beforeEach(() => {
+ sinon.restore();
+ });
+
+ describe('doRequest', () => {
+ it('correctly builds url without leading /', async () => {
+ let options = {
+ host: '1.1.1.1',
+ remotePort: 8080
+ };
+
+ let stub = sinon.stub(util as any, 'httpGet').resolves({
+ body: '',
+ statusCode: 200
+ });
+
+ await rokuECP['doRequest']('query/my-route', options);
+ expect(stub.getCall(0).args).to.eql([`http://1.1.1.1:8080/query/my-route`, undefined]);
+ });
+
+ it('correctly builds url with leading /', async () => {
+ let options = {
+ host: '1.1.1.1',
+ remotePort: 8080
+ };
+
+ let stub = sinon.stub(util as any, 'httpGet').resolves({
+ body: '',
+ statusCode: 200
+ });
+
+ await rokuECP['doRequest']('/query/my-route', options);
+ expect(stub.getCall(0).args).to.eql([`http://1.1.1.1:8080/query/my-route`, undefined]);
+ });
+
+ it('passes request options if populated', async () => {
+ let options = {
+ host: '1.1.1.1',
+ remotePort: 8080,
+ requestOptions: {
+ timeout: 1000
+ }
+ };
+
+ let stub = sinon.stub(util as any, 'httpGet').resolves({
+ body: '',
+ statusCode: 200
+ });
+
+ await rokuECP['doRequest']('/query/my-route', options);
+ expect(stub.getCall(0).args).to.eql([`http://1.1.1.1:8080/query/my-route`, options.requestOptions]);
+ });
+
+ it('uses default port 8060 when missing in options', async () => {
+ let options = {
+ host: '1.1.1.1'
+ };
+
+ let stub = sinon.stub(util as any, 'httpGet').resolves({
+ body: '',
+ statusCode: 200
+ });
+
+ await rokuECP['doRequest']('/query/my-route', options);
+ expect(stub.getCall(0).args).to.eql([`http://1.1.1.1:8060/query/my-route`, undefined]);
+ });
+
+ it('supports get and post methods', async () => {
+ let options = {
+ host: '1.1.1.1'
+ };
+
+ const getStub = sinon.stub(util as any, 'httpGet').resolves({
+ body: '',
+ statusCode: 200
+ });
+ const postStub = sinon.stub(util as any, 'httpPost').resolves({
+ body: '',
+ statusCode: 200
+ });
+
+ await rokuECP['doRequest']('/query/my-route', options, 'get');
+ expect(getStub.getCall(0).args).to.eql([`http://1.1.1.1:8060/query/my-route`, undefined]);
+ expect(getStub.callCount).to.eql(1);
+ expect(postStub.callCount).to.eql(0);
+
+ await rokuECP['doRequest']('/query/my-route', options, 'post');
+ expect(postStub.getCall(0).args).to.eql([`http://1.1.1.1:8060/query/my-route`, undefined]);
+ expect(getStub.callCount).to.eql(1);
+ expect(postStub.callCount).to.eql(1);
+
+ });
+ });
+
+ describe('getRegistry', () => {
+ it('calls doRequest with correct route and options', async () => {
+ let options = {
+ host: '1.1.1.1',
+ remotePort: 8080,
+ appId: 'dev'
+ };
+
+ let stub = sinon.stub(rokuECP as any, 'doRequest').resolves({
+ body: '',
+ statusCode: 200
+ });
+ sinon.stub(rokuECP as any, 'processRegistry').resolves({});
+
+ await rokuECP.getRegistry(options);
+ expect(stub.getCall(0).args).to.eql(['query/registry/dev', options]);
+ });
+
+ });
+
+ describe('parseRegistry', () => {
+ describe('non-error responses', () => {
+ it('handles ok response with no other properties', async () => {
+ let response = {
+ body: `
+
+ OK
+ Plugin dev not found
+
+ `,
+ statusCode: 200
+ };
+ let result = await rokuECP['processRegistry'](response as any);
+ expect(result).to.eql({
+ devId: undefined,
+ plugins: undefined,
+ spaceAvailable: undefined,
+ sections: {},
+ status: EcpStatus.ok
+ } as EcpRegistryData);
+ });
+
+ it('handles ok response with empty sections', async () => {
+ let response = {
+ body: `
+
+
+
+ 12345
+ 12,34,dev
+ 28075
+
+
+ OK
+
+ `,
+ statusCode: 200
+ };
+
+ let result = await rokuECP['processRegistry'](response as any);
+ expect(result).to.eql({
+ devId: '12345',
+ plugins: ['12', '34', 'dev'],
+ spaceAvailable: '28075',
+ sections: {},
+ status: EcpStatus.ok
+ } as EcpRegistryData);
+ });
+
+ it('handles ok response with populated sections', async () => {
+ let response = {
+ body: `
+
+
+
+ 12345
+ dev
+ 32590
+
+
+ section One
+
+ -
+ first key in section one
+ value one section one
+
+
+
+
+ section Two
+
+ -
+ first key in section two
+ value one section two
+
+ -
+ second key in section two
+ value two section two
+
+
+
+
+
+ OK
+
+ `
+ };
+ let result = await rokuECP['processRegistry'](response as any);
+ expect(result).to.eql({
+ devId: '12345',
+ plugins: ['dev'],
+ sections: {
+ 'section One': {
+ 'first key in section one': 'value one section one'
+ },
+ 'section Two': {
+ 'first key in section two': 'value one section two',
+ 'second key in section two': 'value two section two'
+ }
+ },
+ spaceAvailable: '32590',
+ status: EcpStatus.ok
+ } as EcpRegistryData);
+ });
+ });
+
+ describe('error responses', () => {
+ it('handles not in dev mode', async () => {
+ let response = {
+ body: `
+
+ FAILED
+ Plugin dev not found
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processRegistry'](response as any), 'Plugin dev not found');
+ });
+
+ it('handles device not keyed', async () => {
+ let response = {
+ body: `
+
+ FAILED
+ Device not keyed
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processRegistry'](response as any), 'Device not keyed');
+ });
+
+ it('handles failed status with missing error', async () => {
+ let response = {
+ body: `
+
+ FAILED
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processRegistry'](response as any), 'Unknown error');
+ });
+
+ it('handles error response without xml', async () => {
+ let response = {
+ body: `ECP command not allowed in Limited mode.`,
+ statusCode: 403
+ };
+ await expectThrowsAsync(() => rokuECP['processRegistry'](response as any), 'ECP command not allowed in Limited mode.');
+ });
+ });
+ });
+
+ describe('getAppState', () => {
+ it('calls doRequest with correct route and options', async () => {
+ let options = {
+ host: '1.1.1.1',
+ remotePort: 8080,
+ appId: 'dev'
+ };
+
+ let stub = sinon.stub(rokuECP as any, 'doRequest').resolves({
+ body: '',
+ statusCode: 200
+ });
+
+ sinon.stub(rokuECP as any, 'processAppState').resolves({});
+
+ await rokuECP.getAppState(options);
+ expect(stub.getCall(0).args).to.eql(['query/app-state/dev', options]);
+ });
+ });
+
+ describe('processAppState', () => {
+ describe('non-error responses', () => {
+ it('handles ok response', async () => {
+ let response = {
+ body: `
+
+
+ dev
+ my app
+ 10.0.0
+ 12345
+ active
+ OK
+
+ `,
+ statusCode: 200
+ };
+ let result = await rokuECP['processAppState'](response as any);
+ expect(result).to.eql({
+ appId: 'dev',
+ appDevId: '12345',
+ appTitle: 'my app',
+ appVersion: '10.0.0',
+ state: AppState.active,
+ status: EcpStatus.ok
+ } as EcpAppStateData);
+ });
+
+ it('handles ok response with unknown state', async () => {
+ let response = {
+ body: `
+
+
+ dev
+ my app
+ 10.0.0
+ 12345
+ bad
+ OK
+
+ `,
+ statusCode: 200
+ };
+ let result = await rokuECP['processAppState'](response as any);
+ expect(result).to.eql({
+ appId: 'dev',
+ appDevId: '12345',
+ appTitle: 'my app',
+ appVersion: '10.0.0',
+ state: AppState.unknown,
+ status: EcpStatus.ok
+ } as EcpAppStateData);
+ });
+
+ it('handles ok response with uppercase state', async () => {
+ let response = {
+ body: `
+
+
+ dev
+ my app
+ 10.0.0
+ 12345
+ ACTIVE
+ OK
+
+ `,
+ statusCode: 200
+ };
+ let result = await rokuECP['processAppState'](response as any);
+ expect(result).to.eql({
+ appId: 'dev',
+ appDevId: '12345',
+ appTitle: 'my app',
+ appVersion: '10.0.0',
+ state: AppState.active,
+ status: EcpStatus.ok
+ } as EcpAppStateData);
+ });
+ });
+
+ describe('error responses', () => {
+ it('handles failed status with missing error', async () => {
+ let response = {
+ body: `
+
+ FAILED
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processAppState'](response as any), 'Unknown error');
+ });
+
+ it('handles failed status with populated error', async () => {
+ let response = {
+ body: `
+
+ FAILED
+ App not found
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processAppState'](response as any), 'App not found');
+ });
+
+ it('handles error response without xml', async () => {
+ let response = {
+ body: `ECP command not allowed in Limited mode.`,
+ statusCode: 403
+ };
+ await expectThrowsAsync(() => rokuECP['processAppState'](response as any), 'ECP command not allowed in Limited mode.');
+ });
+ });
+ });
+
+ describe('exitApp', () => {
+ it('calls doRequest with correct route and options', async () => {
+ let options = {
+ host: '1.1.1.1',
+ remotePort: 8080,
+ appId: 'dev'
+ };
+
+ let stub = sinon.stub(rokuECP as any, 'doRequest').resolves({
+ body: '',
+ statusCode: 200
+ });
+ sinon.stub(rokuECP as any, 'processExitApp').resolves({});
+
+ await rokuECP.exitApp(options);
+ expect(stub.getCall(0).args).to.eql(['exit-app/dev', options, 'post']);
+ });
+ });
+
+ describe('processExitApp', () => {
+ describe('non-error responses', () => {
+ it('handles ok response', async () => {
+ let response = {
+ body: `
+
+
+ OK
+
+ `,
+ statusCode: 200
+ };
+ let result = await rokuECP['processExitApp'](response as any);
+ expect(result).to.eql({
+ status: EcpStatus.ok
+ });
+ });
+ });
+
+ describe('error responses', () => {
+ it('handles failed status with missing error', async () => {
+ let response = {
+ body: `
+
+ FAILED
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processExitApp'](response as any), 'Unknown error');
+ });
+
+ it('handles failed status with populated error', async () => {
+ let response = {
+ body: `
+
+ FAILED
+ App not found
+
+ `,
+ statusCode: 200
+ };
+ await expectThrowsAsync(() => rokuECP['processExitApp'](response as any), 'App not found');
+ });
+
+ it('handles error response without xml', async () => {
+ let response = {
+ body: `ECP command not allowed in Limited mode.`,
+ statusCode: 403
+ };
+ await expectThrowsAsync(() => rokuECP['processExitApp'](response as any), 'ECP command not allowed in Limited mode.');
+ });
+ });
+ });
+});
diff --git a/src/RokuECP.ts b/src/RokuECP.ts
new file mode 100644
index 00000000..a421c3da
--- /dev/null
+++ b/src/RokuECP.ts
@@ -0,0 +1,185 @@
+import { util } from './util';
+import type * as requestType from 'request';
+import type { Response } from 'request';
+
+export class RokuECP {
+ private async doRequest(route: string, options: BaseOptions, method: 'post' | 'get' = 'get'): Promise {
+ const url = `http://${options.host}:${options.remotePort ?? 8060}/${route.replace(/^\//, '')}`;
+ if (method === 'post') {
+ return util.httpPost(url, options.requestOptions);
+ } else {
+ return util.httpGet(url, options.requestOptions);
+ }
+ }
+
+ private getEcpStatus(response: ParsedEcpRoot, rootKey: string): EcpStatus {
+ return EcpStatus[response?.[rootKey]?.status?.[0]?.toLowerCase()] ?? EcpStatus.failed;
+ }
+
+ private async parseResponse(response: Response, rootKey: string, callback: (parsed: any, status: EcpStatus) => R): Promise {
+ if (typeof response.body === 'string') {
+ let parsed: ParsedEcpRoot;
+ try {
+ parsed = await util.parseXml(response.body);
+ } catch {
+ //if the response is not xml, just return the body as-is
+ throw new Error(response.body ?? 'Unknown error');
+ }
+
+ const status = this.getEcpStatus(parsed, rootKey);
+ if (status === EcpStatus.ok) {
+ return callback(parsed?.[rootKey], status);
+ } else {
+ throw new Error(parsed?.[rootKey]?.error?.[0] ?? 'Unknown error');
+ }
+ }
+ }
+
+ public async getRegistry(options: BaseOptions & { appId: string }) {
+ let result = await this.doRequest(`query/registry/${options.appId}`, options);
+ return this.processRegistry(result);
+ }
+
+ private async processRegistry(response: Response) {
+ return this.parseResponse(response, 'plugin-registry', (parsed: RegistryAsJson, status): EcpRegistryData => {
+ const registry = parsed?.registry?.[0];
+ let sections: EcpRegistryData['sections'] = {};
+
+ for (const section of registry?.sections?.[0]?.section ?? []) {
+ if (typeof section === 'string') {
+ continue;
+ }
+ let sectionName = section.name[0];
+ for (const item of section.items[0].item) {
+ sections[sectionName] ??= {};
+ sections[sectionName][item.key[0]] = item.value[0];
+ }
+ }
+
+ return {
+ devId: registry?.['dev-id']?.[0],
+ plugins: registry?.plugins?.[0]?.split(','),
+ sections: sections,
+ spaceAvailable: registry?.['space-available']?.[0],
+ status: status
+ };
+ });
+ }
+
+ public async getAppState(options: BaseOptions & { appId: string }) {
+ let result = await this.doRequest(`query/app-state/${options.appId}`, options);
+ return this.processAppState(result);
+ }
+
+ private async processAppState(response: Response) {
+ return this.parseResponse(response, 'app-state', (parsed: AppStateAsJson, status): EcpAppStateData => {
+ const state = AppState[parsed.state?.[0]?.toLowerCase()] ?? AppState.unknown;
+ return {
+ appId: parsed['app-id']?.[0],
+ appDevId: parsed['app-dev-id']?.[0],
+ appTitle: parsed['app-title']?.[0],
+ appVersion: parsed['app-version']?.[0],
+ state: state,
+ status: status
+ };
+ });
+ }
+
+ public async exitApp(options: BaseOptions & { appId: string }): Promise {
+ let result = await this.doRequest(`exit-app/${options.appId}`, options, 'post');
+ return this.processExitApp(result);
+ }
+
+ private async processExitApp(response: Response): Promise {
+ return this.parseResponse(response, 'exit-app', (parsed: ExitAppAsJson, status): EcpExitAppData => {
+ return { status: status };
+ });
+ }
+}
+
+export enum EcpStatus {
+ ok = 'ok',
+ failed = 'failed'
+}
+interface BaseOptions {
+ host: string;
+ remotePort?: number;
+ requestOptions?: requestType.CoreOptions;
+}
+
+interface BaseEcpResponse {
+ status: EcpStatus;
+ errorMessage?: string;
+}
+
+export type RokuEcpParam = Parameters[0];
+
+export type ParsedEcpRoot = {
+ [key in T1]: T2;
+};
+
+interface ParsedEcpBase {
+ status?: [string];
+ error?: [string] ;
+}
+
+interface RegistryAsJson extends ParsedEcpBase {
+ registry: [{
+ 'dev-id': [string];
+ plugins: [string];
+ sections: [{
+ section: [{
+ items: [{
+ item: [{
+ key: [string];
+ value: [string];
+ }];
+ }];
+ name: [string];
+ } | string];
+ }];
+ 'space-available': [string];
+ }];
+}
+
+export interface EcpRegistryData extends BaseEcpResponse {
+ devId?: string;
+ plugins?: Array;
+ sections?: Record>;
+ spaceAvailable?: string;
+ state?: string;
+}
+interface AppStateAsJson extends ParsedEcpBase {
+ 'app-id': [string];
+ 'app-title': [string];
+ 'app-version': [string];
+ 'app-dev-id': [string];
+ state: ['active' | 'background' | 'inactive'];
+}
+
+export enum AppState {
+ active = 'active',
+ background = 'background',
+ inactive = 'inactive',
+ unknown = 'unknown'
+}
+
+export interface EcpAppStateData {
+ appId?: string;
+ appTitle?: string;
+ appVersion?: string;
+ appDevId?: string;
+ state?: AppState;
+ status: EcpStatus;
+ errorMessage?: string;
+}
+
+type ExitAppAsJson = ParsedEcpBase;
+
+export interface EcpExitAppData {
+ status: EcpStatus;
+ errorMessage?: string;
+}
+
+
+export const rokuECP = new RokuECP();
diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts
index c1b4c2f6..49215c16 100644
--- a/src/debugSession/BrightScriptDebugSession.ts
+++ b/src/debugSession/BrightScriptDebugSession.ts
@@ -58,6 +58,7 @@ import { interfaces, components, events } from 'brighterscript/dist/roku-types';
import { globalCallables } from 'brighterscript/dist/globalCallables';
import { bscProjectWorkerPool } from '../bsc/threading/BscProjectWorkerPool';
import { populateVariableFromRegistryEcp } from './ecpRegistryUtils';
+import { AppState, rokuECP } from '../RokuECP';
const diagnosticSource = 'roku-debug';
@@ -1416,10 +1417,8 @@ export class BrightScriptDebugSession extends BaseDebugSession {
if (v.type === '$$Registry' && v.childVariables.length === 0) {
// This is a special scope variable used to load registry data via an ECP call
- const url = `http://${this.launchConfiguration.host}:${this.launchConfiguration.remotePort}/query/registry/dev`;
// Send the registry ECP call for the `dev` app as side loaded apps are always `dev`
- const response = await util.httpGet(url);
- await populateVariableFromRegistryEcp(response, v, this.variables, this.getEvaluateRefId.bind(this));
+ await populateVariableFromRegistryEcp({ host: this.launchConfiguration.host, remotePort: this.launchConfiguration.remotePort, appId: 'dev' }, v, this.variables, this.getEvaluateRefId.bind(this));
}
//query for child vars if we haven't done it yet or DAP is asking to resolve a lazy variable
@@ -2047,11 +2046,53 @@ export class BrightScriptDebugSession extends BaseDebugSession {
this.rokuAdapter.removeAllListeners();
}
await this.rokuAdapter.destroy();
+ await this.ensureAppIsInactive();
this.rokuAdapterDeferred = defer();
}
await this.launchRequest(response, args.arguments as LaunchConfiguration);
}
+ private exitAppTimeout = 5000;
+ private async ensureAppIsInactive() {
+ const startTime = Date.now();
+
+ while (true) {
+ if (Date.now() - startTime > this.exitAppTimeout) {
+ return;
+ }
+
+ try {
+ let appStateResult = await rokuECP.getAppState({
+ host: this.launchConfiguration.host,
+ remotePort: this.launchConfiguration.remotePort,
+ appId: 'dev',
+ requestOptions: { timeout: 300 }
+ });
+
+ const state = appStateResult.state;
+
+ if (state === AppState.active || state === AppState.background) {
+ // Suspends or terminates an app that is running:
+ // If the app supports Instant Resume and is running in the foreground, sending this command suspends the app (the app runs in the background).
+ // If the app supports Instant Resume and is running in the background or the app does not support Instant Resume and is running, sending this command terminates the app.
+ // This means that we might need to send this command twice to terminate the app.
+ await rokuECP.exitApp({
+ host: this.launchConfiguration.host,
+ remotePort: this.launchConfiguration.remotePort,
+ appId: 'dev',
+ requestOptions: { timeout: 300 }
+ });
+ } else if (state === AppState.inactive) {
+ return;
+ }
+ } catch (e) {
+ this.logger.error('Error attempting to exit application', e);
+ }
+
+ await util.sleep(200);
+ }
+ }
+
/**
* Used to track whether the entry breakpoint has already been handled
*/
diff --git a/src/debugSession/ecpRegistryUtils.spec.ts b/src/debugSession/ecpRegistryUtils.spec.ts
index 96bc5105..1ef68d30 100644
--- a/src/debugSession/ecpRegistryUtils.spec.ts
+++ b/src/debugSession/ecpRegistryUtils.spec.ts
@@ -4,6 +4,10 @@ import { VariableType } from '../debugProtocol/events/responses/VariablesRespons
import type { AugmentedVariable } from './BrightScriptDebugSession';
import { BrightScriptDebugSession } from './BrightScriptDebugSession';
import { populateVariableFromRegistryEcp } from './ecpRegistryUtils';
+import { rokuECP } from '../RokuECP';
+import { createSandbox } from 'sinon';
+
+const sinon = createSandbox();
describe('ecpRegistryUtils', () => {
let session: BrightScriptDebugSession;
@@ -12,6 +16,10 @@ describe('ecpRegistryUtils', () => {
session = new BrightScriptDebugSession();
});
+ afterEach(() => {
+ sinon.restore();
+ });
+
describe('populateVariableFromRegistryEcp', () => {
let refFactory = (key: string, frameId: number) => session['getEvaluateRefId'](key, frameId);
@@ -34,7 +42,8 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `
OK
@@ -42,7 +51,9 @@ describe('ecpRegistryUtils', () => {
`,
statusCode: 200
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(1);
expect(v.childVariables[0]).to.eql({
name: 'sections',
@@ -84,7 +95,8 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `
@@ -98,7 +110,9 @@ describe('ecpRegistryUtils', () => {
`,
statusCode: 200
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(4);
expect(v.childVariables[0]).to.eql({
name: 'devId',
@@ -173,7 +187,8 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `
@@ -210,7 +225,9 @@ describe('ecpRegistryUtils', () => {
`,
statusCode: 200
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(4);
expect(v.childVariables[0]).to.eql({
name: 'devId',
@@ -334,7 +351,8 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `
FAILED
@@ -342,7 +360,9 @@ describe('ecpRegistryUtils', () => {
`,
statusCode: 200
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(1);
expect(v.childVariables[0]).to.eql({
name: 'error',
@@ -361,7 +381,8 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `
FAILED
@@ -369,7 +390,9 @@ describe('ecpRegistryUtils', () => {
`,
statusCode: 200
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(1);
expect(v.childVariables[0]).to.eql({
name: 'error',
@@ -388,14 +411,17 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `
FAILED
`,
statusCode: 200
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(1);
expect(v.childVariables[0]).to.eql({
name: 'error',
@@ -414,10 +440,13 @@ describe('ecpRegistryUtils', () => {
type: '$$Registry',
childVariables: []
};
- await populateVariableFromRegistryEcp({
+
+ sinon.stub(rokuECP as any, 'doRequest').returns(Promise.resolve({
body: `ECP command not allowed in Limited mode.`,
statusCode: 403
- } as Response, v, session['variables'], refFactory);
+ } as Response));
+
+ await populateVariableFromRegistryEcp({ host: '', appId: '' }, v, session['variables'], refFactory);
expect(v.childVariables.length).to.eql(1);
expect(v.childVariables[0]).to.eql({
name: 'error',
diff --git a/src/debugSession/ecpRegistryUtils.ts b/src/debugSession/ecpRegistryUtils.ts
index 576fbf91..e3f1f9a8 100644
--- a/src/debugSession/ecpRegistryUtils.ts
+++ b/src/debugSession/ecpRegistryUtils.ts
@@ -1,12 +1,11 @@
import { VariableType } from '../debugProtocol/events/responses/VariablesResponse';
-import { util } from '../util';
+import { EcpStatus, rokuECP } from '../RokuECP';
+import type { RokuEcpParam } from '../RokuECP';
import type { AugmentedVariable } from './BrightScriptDebugSession';
-import type { Response } from 'request';
-export async function populateVariableFromRegistryEcp(response: Response, v: AugmentedVariable, variables: Record, refIdFactory: (key: string, frameId: number) => number) {
- let registryData = await convertRegistryEcpResponseToScope(response);
-
- if (registryData.status === 'OK') {
+export async function populateVariableFromRegistryEcp(options: RokuEcpParam<'getRegistry'>, v: AugmentedVariable, variables: Record, refIdFactory: (key: string, frameId: number) => number) {
+ try {
+ let registryData = await rokuECP.getRegistry(options);
// Add registry data to variable list
if (registryData.devId) {
v.childVariables.push({
@@ -114,91 +113,13 @@ export async function populateVariableFromRegistryEcp(response: Response, v: Aug
return sectionItemVariable;
}));
}
- } else {
+ } catch (e) {
v.childVariables.push({
name: 'error',
- value: `❌ Error: ${registryData.errorMessage ?? 'Unknown error'}`,
+ value: `❌ Error: ${e.message ?? 'Unknown error'}`,
type: VariableType.String,
variablesReference: 0,
childVariables: []
});
}
}
-
-async function convertRegistryEcpResponseToScope(response: Response): Promise {
- if (typeof response.body === 'string') {
- try {
- let parsed = await util.parseXml(response.body) as RegistryAsJson;
-
- const status = parsed['plugin-registry'].status[0];
-
- if (status === 'OK') {
-
- let registry = parsed['plugin-registry'].registry?.[0];
-
- let sections: EcpRegistryData['sections'] = {};
- for (const section of registry?.sections?.[0]?.section ?? []) {
- if (typeof section === 'string') {
- continue;
- }
- let sectionName = section.name[0];
- for (const item of section.items[0].item) {
- sections[sectionName] ??= {};
- sections[sectionName][item.key[0]] = item.value[0];
- }
- }
-
- return {
- devId: registry?.['dev-id']?.[0],
- plugins: registry?.plugins?.[0]?.split(','),
- sections: sections,
- spaceAvailable: registry?.['space-available']?.[0],
- status: 'OK'
- };
- } else {
- return {
- status: 'FAILED',
- errorMessage: parsed?.['plugin-registry']?.error?.[0] ?? 'Unknown error'
- };
- }
- } catch {
- //if the response is not xml, just return the body as-is
- return {
- status: 'FAILED',
- errorMessage: response.body
- };
- }
- }
-}
-
-export interface EcpRegistryData {
- devId?: string;
- plugins?: Array;
- sections?: Record>;
- spaceAvailable?: string;
- status: 'OK' | 'FAILED';
- errorMessage?: string;
-}
-
-interface RegistryAsJson {
- 'plugin-registry': {
- registry: [{
- 'dev-id': [string];
- plugins: [string];
- sections: [{
- section: [{
- items: [{
- item: [{
- key: [string];
- value: [string];
- }];
- }];
- name: [string];
- } | string];
- }];
- 'space-available': [string];
- }];
- status: [string];
- error?: [string];
- };
-}
diff --git a/src/util.ts b/src/util.ts
index 87dded56..51e0ff6d 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -425,9 +425,9 @@ class Util {
/**
* Do an http GET request
*/
- public httpGet(url: string) {
+ public httpGet(url: string, options?: requestType.CoreOptions) {
return new Promise((resolve, reject) => {
- request.get(url, (err, response) => {
+ request.get(url, options, (err, response) => {
return err ? reject(err) : resolve(response);
});
});
@@ -514,7 +514,7 @@ class Util {
/**
* Parse an xml file and get back a javascript object containing its results
*/
- public parseXml(text: string) {
+ public parseXml(text: string): Promise {
return new Promise((resolve, reject) => {
xml2js.parseString(text, (err, data) => {
if (err) {