Skip to content

Commit 9cde9f8

Browse files
committed
Accept a roku-deploy device option in the launch config, deprecate top-level host
1 parent 37662e4 commit 9cde9f8

5 files changed

Lines changed: 134 additions & 20 deletions

File tree

src/LaunchConfiguration.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DeviceInfoRaw, FileEntry } from 'roku-deploy';
1+
import type { DeviceInfoRaw, DeviceOption, FileEntry } from 'roku-deploy';
22
import type { DebugProtocol } from '@vscode/debugprotocol';
33
import type { LogLevel } from './logging';
44

@@ -12,8 +12,18 @@ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArgument
1212
cwd: string;
1313
/**
1414
* The host or ip address for the target Roku
15+
* @deprecated Use `device` instead. When `device` is omitted, a local device config is built from this field.
1516
*/
16-
host: string;
17+
host?: string;
18+
19+
/**
20+
* The roku-deploy device option for the target device. This is the canonical way to address the device.
21+
* Supports every roku-deploy addressing scheme: a local network device (`{ host }`) or a
22+
* Roku Cloud Emulator device (`{ instanceUrl | id | esn, rceToken }`).
23+
* When omitted, a local device config is built from the deprecated `host` field. When provided with a
24+
* `host` property, it takes the place of the top-level `host` field.
25+
*/
26+
device?: DeviceOption;
1727

1828
/**
1929
* The raw `device-info` for the target Roku. When supplied, the debug session uses this instead of

src/debugProtocol/client/DebugProtocolClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,9 +1274,10 @@ export interface BreakpointSpec {
12741274

12751275
export interface ConstructorOptions {
12761276
/**
1277-
* The host/ip address of the Roku
1277+
* The host/ip address of the Roku. The debug protocol connects over the local network, so a
1278+
* session for a device without a host (like a Roku Cloud Emulator device) cannot connect yet.
12781279
*/
1279-
host: string;
1280+
host?: string;
12801281
/**
12811282
* The port number used to send all debugger commands. This is static/unchanging for Roku devices,
12821283
* but is configurable here to support unit testing or alternate runtimes (i.e. https://www.npmjs.com/package/brs)

src/debugSession/BrightScriptDebugSession.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3093,6 +3093,62 @@ describe('BrightScriptDebugSession', () => {
30933093
});
30943094
});
30953095

3096+
describe('device option', () => {
3097+
it('builds a local device config from host when no device is provided', () => {
3098+
(session as any).launchConfiguration = { host: '1.2.3.4' };
3099+
expect(session['device']).to.eql({ host: '1.2.3.4' });
3100+
expect(session['isLocalDevice']).to.be.true;
3101+
expect(session['deviceLabel']).to.equal('1.2.3.4');
3102+
});
3103+
3104+
it('builds the device option from the deprecated host field during normalize', () => {
3105+
const config = session['normalizeLaunchConfig']({ host: '1.2.3.4' } as any);
3106+
expect(config.device).to.eql({ host: '1.2.3.4' });
3107+
});
3108+
3109+
it('takes host from a local device config during normalize', () => {
3110+
const config = session['normalizeLaunchConfig']({ device: { host: '5.6.7.8' }, host: '1.2.3.4' } as any);
3111+
expect(config.host).to.equal('5.6.7.8');
3112+
(session as any).launchConfiguration = config;
3113+
expect(session['device']).to.eql({ host: '5.6.7.8' });
3114+
expect(session['isLocalDevice']).to.be.true;
3115+
});
3116+
3117+
it('passes a cloud emulator device config through untouched and never leaks the token in the label', () => {
3118+
const device = { instanceUrl: 'https://device.rce.roku.com/instance/abc', rceToken: 'secret' };
3119+
(session as any).launchConfiguration = { device: device };
3120+
expect(session['device']).to.equal(device);
3121+
expect(session['isLocalDevice']).to.be.false;
3122+
expect(session['deviceLabel']).to.equal('https://device.rce.roku.com/instance/abc');
3123+
expect(session['deviceLabel']).not.to.include('secret');
3124+
});
3125+
3126+
it('labels id-addressed and esn-addressed cloud emulator devices by their identifier', () => {
3127+
(session as any).launchConfiguration = { device: { id: '83', rceToken: 'secret' } };
3128+
expect(session['deviceLabel']).to.equal('83');
3129+
(session as any).launchConfiguration = { device: { esn: 'XY020078HH5S', rceToken: 'secret' } };
3130+
expect(session['deviceLabel']).to.equal('XY020078HH5S');
3131+
});
3132+
3133+
it('sends the launch config device to sideload', async () => {
3134+
const device = { instanceUrl: 'https://device.rce.roku.com/instance/abc', rceToken: 'secret' };
3135+
(session as any).launchConfiguration = {
3136+
...session['launchConfiguration'],
3137+
device: device,
3138+
outDir: tempDir
3139+
};
3140+
rokuAdapter.connected = true;
3141+
const sideloadStub = sinon.stub(session.rokuDeploy, 'sideload').callsFake(() => {
3142+
(session['rokuAdapter'] as TelnetAdapter)['emit']('app-ready');
3143+
return Promise.resolve({ message: 'success', results: [] });
3144+
});
3145+
3146+
await (session as any).publish();
3147+
3148+
expect(sideloadStub.getCall(0).args[0].device).to.equal(device);
3149+
});
3150+
});
3151+
30963152
describe('publish', () => {
30973153
it('waits 60 seconds before aborting when the app never becomes ready', async () => {
30983154
session['publishTimeout'] = 60_000;

src/debugSession/BrightScriptDebugSession.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import * as fsExtra from 'fs-extra';
22
import { orderBy } from 'natural-orderby';
33
import * as path from 'path';
44
import * as semver from 'semver';
5-
import { rokuDeploy, CompileError, isUpdateCheckRequiredError, isConnectionResetError, EcpNetworkAccessModeDisabledError } from 'roku-deploy';
6-
import type { DeviceInfo, DeviceOption, RokuDeploy, SideloadOptions } from 'roku-deploy';
5+
import { rokuDeploy, CompileError, isUpdateCheckRequiredError, isConnectionResetError, EcpNetworkAccessModeDisabledError, isLocalDeviceConfig, isRceDeviceConfig, isRceByUrl, isRceById } from 'roku-deploy';
6+
import type { DeviceInfo, DeviceOption, LocalDeviceConfig, RokuDeploy, SideloadOptions } from 'roku-deploy';
77
import {
88
BreakpointEvent,
99
LoggingDebugSession,
@@ -359,12 +359,39 @@ export class BrightScriptDebugSession extends LoggingDebugSession {
359359
public rokuDeploy = rokuDeploy as unknown as RokuDeploy;
360360

361361
/**
362-
* The roku-deploy `device` option for the target device. This is the single place where the launch
363-
* configuration is converted into a device config, so future device addressing schemes (like the
364-
* Roku Cloud Emulator) only need to be handled here.
362+
* The roku-deploy `device` option for the target device. This is the canonical way to address the
363+
* device; the deprecated `launchConfiguration.host` field is only used as a fallback when the
364+
* config has not been normalized yet (normalizeLaunchConfig always sets `device`).
365365
*/
366366
private get device(): DeviceOption {
367-
return { host: this.launchConfiguration.host };
367+
return this.launchConfiguration.device ?? { host: this.launchConfiguration.host };
368+
}
369+
370+
/**
371+
* Is the target device addressed over the local network (by host/ip)? Only local devices get
372+
* host-based treatment like DNS resolution.
373+
*/
374+
private get isLocalDevice(): boolean {
375+
const device = this.device;
376+
return typeof device === 'object' && isLocalDeviceConfig(device);
377+
}
378+
379+
/**
380+
* A short human-readable identifier for the target device, safe for log and error messages
381+
* (never includes credentials like the rceToken)
382+
*/
383+
private get deviceLabel(): string {
384+
const device = this.device;
385+
if (typeof device === 'string') {
386+
return device;
387+
}
388+
if (isRceDeviceConfig(device)) {
389+
if (isRceByUrl(device)) {
390+
return device.instanceUrl;
391+
}
392+
return isRceById(device) ? device.id : device.esn;
393+
}
394+
return device.host ?? this.launchConfiguration.host;
368395
}
369396

370397
private componentLibraryServer = new ComponentLibraryServer();
@@ -606,6 +633,15 @@ export class BrightScriptDebugSession extends LoggingDebugSession {
606633
* @returns
607634
*/
608635
private normalizeLaunchConfig(config: LaunchConfiguration) {
636+
//`device` is the canonical way to address the target device; `host` is a deprecated alias.
637+
//when a local device config is supplied, its host takes the place of the top-level `host` field
638+
//(which every host-based connection like telnet and the debug protocol still reads).
639+
//when no device is supplied, build one from the deprecated `host` field.
640+
if (typeof config.device === 'object' && isLocalDeviceConfig(config.device) && config.device.host) {
641+
config.host = config.device.host;
642+
} else if (!config.device) {
643+
config.device = { host: config.host };
644+
}
609645
config.cwd ??= process.cwd();
610646
config.outDir ??= s`${config.cwd}/out`;
611647
config.stagingDir ??= rokuDeploy.getStagingDir({ outDir: config.outDir, cwd: config.cwd });
@@ -653,11 +689,17 @@ export class BrightScriptDebugSession extends LoggingDebugSession {
653689

654690
this.sendLaunchProgress('start', 'Finding device on network');
655691

656-
//do a DNS lookup for the host to fix issues with roku rejecting ECP
657-
try {
658-
this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host);
659-
} catch (e) {
660-
return this.shutdown(`Could not resolve ip address for host '${this.launchConfiguration.host}'`);
692+
//do a DNS lookup for the host to fix issues with roku rejecting ECP.
693+
//only applies to local devices; other device types (like the Roku Cloud Emulator) are not addressed by host
694+
if (this.isLocalDevice) {
695+
try {
696+
const resolvedHost = await util.dnsLookup(this.launchConfiguration.host);
697+
//keep the device config and the deprecated top-level host field in sync with the resolved host
698+
this.launchConfiguration.host = resolvedHost;
699+
(this.launchConfiguration.device as LocalDeviceConfig).host = resolvedHost;
700+
} catch (e) {
701+
return this.shutdown(`Could not resolve ip address for host '${this.launchConfiguration.host}'`);
702+
}
661703
}
662704

663705
// fetch device info if not supplied via launch config
@@ -668,17 +710,17 @@ export class BrightScriptDebugSession extends LoggingDebugSession {
668710
this.deviceInfo = await rokuDeploy.getDeviceInfo({ device: this.device, ecpPort: this.launchConfiguration.remotePort, enhance: true, timeout: 4_000 });
669711
}
670712
if (this.deviceInfo.ecpSettingMode === 'limited') {
671-
return await this.shutdown(`To allow the debugger to communicate properly, please ensure on the Roku device that 'Settings' > 'System' > 'Advanced system settings' > 'Control by mobile apps' is set to "Enabled" or "Permissive". Current mode: Limited (device: ${this.launchConfiguration.host})`);
713+
return await this.shutdown(`To allow the debugger to communicate properly, please ensure on the Roku device that 'Settings' > 'System' > 'Advanced system settings' > 'Control by mobile apps' is set to "Enabled" or "Permissive". Current mode: Limited (device: ${this.deviceLabel})`);
672714
}
673715
} catch (e) {
674716
if (e instanceof EcpNetworkAccessModeDisabledError) {
675-
return this.shutdown(`To allow the debugger to communicate properly, please ensure on the Roku device that 'Settings' > 'System' > 'Advanced system settings' > 'Control by mobile apps' is set to "Enabled" or "Permissive". Current mode: Disabled (device: ${this.launchConfiguration.host})`);
717+
return this.shutdown(`To allow the debugger to communicate properly, please ensure on the Roku device that 'Settings' > 'System' > 'Advanced system settings' > 'Control by mobile apps' is set to "Enabled" or "Permissive". Current mode: Disabled (device: ${this.deviceLabel})`);
676718
}
677-
return this.shutdown(`Unable to connect to roku at '${this.launchConfiguration.host}'. Verify the IP address is correct and that the device is powered on and connected to same network as this computer.`);
719+
return this.shutdown(`Unable to connect to roku at '${this.deviceLabel}'. Verify the device address is correct and that the device is powered on and reachable.`);
678720
}
679721

680722
if (this.deviceInfo && !this.deviceInfo.developerEnabled) {
681-
return await this.shutdown(`Developer mode is not enabled for host '${this.launchConfiguration.host}'.`);
723+
return await this.shutdown(`Developer mode is not enabled for device '${this.deviceLabel}'.`);
682724
}
683725

684726
// everything is ready, send the response to the launch request so the UI can update and configuration can begin

src/interfaces.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ export interface RokuAdapterEvaluateResponse {
1616
}
1717

1818
export interface AdapterOptions {
19-
host: string;
19+
/**
20+
* The host or ip address of the target device. The telnet and debug-protocol adapters connect
21+
* over the local network, so a session for a device without a host (like a Roku Cloud Emulator
22+
* device) cannot connect an adapter yet.
23+
*/
24+
host?: string;
2025
brightScriptConsolePort?: number;
2126
remotePort?: number;
2227
/**

0 commit comments

Comments
 (0)