Skip to content

Commit

Permalink
Auto-enable debug protocol on 12.5 devices (#517)
Browse files Browse the repository at this point in the history
* Auto-enable debug protocol on 12.5 devices

* Show "submit issue" button, silence telnet after 2 clicks
  • Loading branch information
TwitchBronBron authored Nov 6, 2023
1 parent 23208f0 commit 49e6b7e
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 28 deletions.
101 changes: 92 additions & 9 deletions src/DebugConfigurationProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert, expect } from 'chai';
import * as path from 'path';
import type { SinonStub } from 'sinon';
import { createSandbox } from 'sinon';
import type { WorkspaceFolder } from 'vscode';
import { QuickPickItemKind } from 'vscode';
Expand All @@ -13,6 +14,8 @@ import * as fsExtra from 'fs-extra';
import type { RokuDeviceDetails } from './ActiveDeviceManager';
import { ActiveDeviceManager } from './ActiveDeviceManager';
import { rokuDeploy } from 'roku-deploy';
import { GlobalStateManager } from './GlobalStateManager';
import { util } from './util';

const sinon = createSandbox();
const Module = require('module');
Expand All @@ -34,16 +37,11 @@ describe('BrightScriptConfigurationProvider', () => {

let configProvider: BrightScriptDebugConfigurationProvider;
let folder: WorkspaceFolder;
let globalStateManager: GlobalStateManager;

beforeEach(() => {
fsExtra.emptyDirSync(tempDir);
let context = {
workspaceState: {
update: () => {
return Promise.resolve();
}
}
};
globalStateManager = new GlobalStateManager(vscode.context);

folder = {
uri: Uri.file(rootDir),
Expand All @@ -56,10 +54,11 @@ describe('BrightScriptConfigurationProvider', () => {
let activeDeviceManager = new ActiveDeviceManager();

configProvider = new BrightScriptDebugConfigurationProvider(
<any>context,
vscode.context,
activeDeviceManager,
null,
vscode.window.createOutputChannel('Extension')
vscode.window.createOutputChannel('Extension'),
globalStateManager
);
});

Expand Down Expand Up @@ -460,6 +459,90 @@ describe('BrightScriptConfigurationProvider', () => {
'Enter manually'
]);
});
});

describe('processEnableDebugProtocolParameter', () => {
let value: string;
let stub: SinonStub;
beforeEach(() => {
stub = sinon.stub(vscode.window, 'showInformationMessage').callsFake(() => {
return Promise.resolve(value) as any;
});
});

it('sets true when clicked "okay"', async () => {
value = 'Okay';
const config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(config.enableDebugProtocol).to.eql(true);
});

it('sets true and flips global state when clicked "okay"', async () => {
value = `Okay (and dont warn again)`;
expect(globalStateManager.debugProtocolPopupSnoozeUntilDate).to.eql(undefined);
const config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(config.enableDebugProtocol).to.eql(true);
//2 weeks after now
expect(
globalStateManager.debugProtocolPopupSnoozeUntilDate.getTime()
).closeTo(Date.now() + (14 * 24 * 60 * 60 * 1000), 1000);
expect(globalStateManager.debugProtocolPopupSnoozeValue).to.eql(true);
});

it('sets false when clicked "No, use the telnet debugger"', async () => {
value = 'Use telnet';
const config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(config.enableDebugProtocol).to.eql(false);
});

it('thorws exception clicked "cancel"', async () => {
value = undefined;
let ex;
try {
await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
} catch (e) {
ex = e;
}
expect(ex?.message).to.eql('Debug session cancelled');
});

it('sets to true and does not prompt when "dont show again" was clicked', async () => {
value = `Okay (and dont warn again)`;
globalStateManager.debugProtocolPopupSnoozeUntilDate = new Date(Date.now() + (60 * 1000));
globalStateManager.debugProtocolPopupSnoozeValue = true;
let config = await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(config.enableDebugProtocol).to.eql(true);
expect(stub.called).to.be.false;
});

it('shows the alternate telnet prompt after 2 debug sessions', async () => {
value = `Use telnet`;

await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(stub.getCall(stub.callCount - 1).args[4]).to.eql('Use telnet');

await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(stub.getCall(stub.callCount - 1).args[4]).to.eql('Use telnet');

value = 'Use telnet (and ask less often)';
await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
expect(stub.getCall(stub.callCount - 1).args[4]).to.eql('Use telnet (and ask less often)');
});

it('shows the issue picker when selected', async () => {
value = `Report an issue`;
const reportStub = sinon.stub(util, 'openIssueReporter').returns(Promise.resolve());

try {
await configProvider['processEnableDebugProtocolParameter']({} as any, { softwareVersion: '12.5.0' });
} catch (e) { }

expect(reportStub.called).to.be.true;
});

it('turns truthy values into true', async () => {
value = `Report an issue`;
const config = await configProvider['processEnableDebugProtocolParameter']({ enableDebugProtocol: {} } as any, { softwareVersion: '12.5.0' });
expect(config.enableDebugProtocol).to.be.true;
});
});
});
62 changes: 59 additions & 3 deletions src/DebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Deferred, util as bslangUtil } from 'brighterscript';
import * as semver from 'semver';
import * as dotenv from 'dotenv';
import * as path from 'path';
import * as fsExtra from 'fs-extra';
Expand All @@ -22,6 +23,7 @@ import type { ActiveDeviceManager, RokuDeviceDetails } from './ActiveDeviceManag
import cloneDeep = require('clone-deep');
import { rokuDeploy } from 'roku-deploy';
import type { DeviceInfo } from 'roku-deploy';
import type { GlobalStateManager } from './GlobalStateManager';

/**
* An id to represent the "Enter manually" option in the host picker
Expand All @@ -35,7 +37,8 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
private context: ExtensionContext,
private activeDeviceManager: ActiveDeviceManager,
private telemetryManager: TelemetryManager,
private extensionOutputChannel: vscode.OutputChannel
private extensionOutputChannel: vscode.OutputChannel,
private globalStateManager: GlobalStateManager
) {
this.context = context;
this.activeDeviceManager = activeDeviceManager;
Expand Down Expand Up @@ -79,15 +82,21 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio

private configDefaults: any;

/**
* A counter used to track how often the user clicks the "use telnet" button in the popup
*/
private useTelnetCounter = 0;

/**
* Massage a debug configuration just before a debug session is being launched,
* e.g. add all missing attributes to the debug configuration.
*/
public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, config: BrightScriptLaunchConfiguration, token?: CancellationToken): Promise<BrightScriptLaunchConfiguration> {
let deviceInfo: DeviceInfo;
let result: BrightScriptLaunchConfiguration;
try {
// merge user and workspace settings into the config
let result = this.processUserWorkspaceSettings(config);
result = this.processUserWorkspaceSettings(config);

//force a specific staging folder path because sometimes this conflicts with bsconfig.json
result.stagingFolderPath = path.join('${outDir}/.roku-deploy-staging');
Expand All @@ -110,6 +119,8 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
throw new Error(`Cannot deploy: developer mode is disabled on '${result.host}'`);
}

result = await this.processEnableDebugProtocolParameter(result, deviceInfo);

await this.context.workspaceState.update('enableDebuggerAutoRecovery', result.enableDebuggerAutoRecovery);

return result;
Expand All @@ -121,11 +132,57 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
//send telemetry about this debug session (don't worry, it gets sanitized...we're just checking if certain features are being used)
this.telemetryManager?.sendStartDebugSessionEvent(
this.processUserWorkspaceSettings(config) as any,
result,
deviceInfo
);
}
}

private async processEnableDebugProtocolParameter(config: BrightScriptLaunchConfiguration, deviceInfo: DeviceInfo) {
if (config.enableDebugProtocol !== undefined || !semver.gte(deviceInfo?.softwareVersion ?? '0.0.0', '12.5.0')) {
config.enableDebugProtocol = config.enableDebugProtocol ? true : false;
return config;
}

//auto-pick a value if user chose to snooze the popup
if (this.globalStateManager.debugProtocolPopupSnoozeUntilDate && this.globalStateManager.debugProtocolPopupSnoozeUntilDate > new Date()) {
config.enableDebugProtocol = this.globalStateManager.debugProtocolPopupSnoozeValue;
return config;
}

//enable the debug protocol by default if the user hasn't defined this prop, and the target RokuOS is 12.5 or greater
const result = await vscode.window.showInformationMessage('New Debug Protocol Enabled', {
modal: true,
detail: `We've activated Roku's debug protocol for this session. This will become the default choice in the future and may be implemented without additional notice. Your feedback during this testing phase is invaluable.`
}, 'Okay', `Okay (and dont warn again)`, this.useTelnetCounter < 2 ? 'Use telnet' : 'Use telnet (and ask less often)', 'Report an issue');


if (result === 'Okay') {
config.enableDebugProtocol = true;
} else if (result === `Okay (and dont warn again)`) {
config.enableDebugProtocol = true;
this.globalStateManager.debugProtocolPopupSnoozeValue = config.enableDebugProtocol;
//snooze for 2 weeks
this.globalStateManager.debugProtocolPopupSnoozeUntilDate = new Date(Date.now() + (14 * 24 * 60 * 60 * 1000));
} else if (result === 'Use telnet') {
this.useTelnetCounter++;
config.enableDebugProtocol = false;
} else if (result === 'Use telnet (and ask less often)') {
this.useTelnetCounter = 0;
config.enableDebugProtocol = false;
this.globalStateManager.debugProtocolPopupSnoozeValue = config.enableDebugProtocol;
//snooze for 12 hours
this.globalStateManager.debugProtocolPopupSnoozeUntilDate = new Date(Date.now() + (12 * 60 * 60 * 1000));
} else if (result === 'Report an issue') {
await util.openIssueReporter({ deviceInfo: deviceInfo });
throw new Error('Debug session cancelled');
} else {
throw new Error('Debug session cancelled');
}

return config;
}

/**
* There are several debug-level config values that can be stored in user settings, so get those
*/
Expand Down Expand Up @@ -262,7 +319,6 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
config.packagePort = config.packagePort ? config.packagePort : this.configDefaults.packagePort;
config.remotePort = config.remotePort ? config.remotePort : this.configDefaults.remotePort;
config.logfilePath ??= null;
config.enableDebugProtocol = config.enableDebugProtocol ? true : false;
config.cwd = folderUri.fsPath;
config.rendezvousTracking = config.rendezvousTracking === false ? false : true;
config.deleteDevChannelBeforeInstall = config.deleteDevChannelBeforeInstall === true;
Expand Down
25 changes: 23 additions & 2 deletions src/GlobalStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export class GlobalStateManager {
private keys = {
lastRunExtensionVersion: 'lastRunExtensionVersion',
lastSeenReleaseNotesVersion: 'lastSeenReleaseNotesVersion',
sendRemoteTextHistory: 'sendRemoteTextHistory'
sendRemoteTextHistory: 'sendRemoteTextHistory',
debugProtocolPopupSnoozeUntilDate: 'debugProtocolPopupSnoozeUntilDate',
debugProtocolPopupSnoozeValue: 'debugProtocolPopupSnoozeValue'
};
private remoteTextHistoryLimit: number;
private remoteTextHistoryEnabled: boolean;
Expand All @@ -36,10 +38,29 @@ export class GlobalStateManager {
void this.context.globalState.update(this.keys.lastSeenReleaseNotesVersion, value);
}


public get debugProtocolPopupSnoozeUntilDate(): Date {
const epoch = this.context.globalState.get<number>(this.keys.debugProtocolPopupSnoozeUntilDate);
if (epoch) {
return new Date(epoch);
}
}
public set debugProtocolPopupSnoozeUntilDate(value: Date) {
void this.context.globalState.update(this.keys.debugProtocolPopupSnoozeUntilDate, value?.getTime());
}


public get debugProtocolPopupSnoozeValue(): boolean {
return this.context.globalState.get<boolean>(this.keys.debugProtocolPopupSnoozeValue);
}
public set debugProtocolPopupSnoozeValue(value: boolean) {
void this.context.globalState.update(this.keys.debugProtocolPopupSnoozeValue, value);
}


public get sendRemoteTextHistory(): string[] {
return this.context.globalState.get(this.keys.sendRemoteTextHistory) ?? [];
}

public set sendRemoteTextHistory(history: string[]) {
history ??= [];
// only update the results if the user has the the history enabled
Expand Down
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as fsExtra from 'fs-extra';

export let ROKU_DEBUG_VERSION: string;
try {
ROKU_DEBUG_VERSION = fsExtra.readJsonSync(__dirname + '/../node_modules/roku-debug/package.json').version;
} catch (e) { }

export const EXTENSION_ID = 'RokuCommunity.brightscript';
5 changes: 2 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ import { RtaManager } from './managers/RtaManager';
import { WebviewViewProviderManager } from './managers/WebviewViewProviderManager';
import { ViewProviderId } from './viewProviders/ViewProviderId';
import { DiagnosticManager } from './managers/DiagnosticManager';

const EXTENSION_ID = 'RokuCommunity.brightscript';
import { EXTENSION_ID } from './constants';

export class Extension {
public outputChannel: vscode.OutputChannel;
Expand Down Expand Up @@ -130,7 +129,7 @@ export class Extension {
);

//register the debug configuration provider
let configProvider = new BrightScriptDebugConfigurationProvider(context, activeDeviceManager, this.telemetryManager, this.extensionOutputChannel);
let configProvider = new BrightScriptDebugConfigurationProvider(context, activeDeviceManager, this.telemetryManager, this.extensionOutputChannel, this.globalStateManager);
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider('brightscript', configProvider)
);
Expand Down
33 changes: 22 additions & 11 deletions src/managers/TelemetryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,29 @@ export class TelemetryManager implements Disposable {
/**
* Track when a debug session has been started
*/
public sendStartDebugSessionEvent(event: BrightScriptLaunchConfiguration & { preLaunchTask: string }, deviceInfo?: DeviceInfo) {
public sendStartDebugSessionEvent(initialConfig: BrightScriptLaunchConfiguration & { preLaunchTask: string }, finalConfig: BrightScriptLaunchConfiguration, deviceInfo?: DeviceInfo) {
let debugConnectionType: 'debugProtocol' | 'telnet';
let enableDebugProtocol = finalConfig?.enableDebugProtocol ?? initialConfig?.enableDebugProtocol;
if (enableDebugProtocol === true) {
debugConnectionType = 'debugProtocol';
} else if (enableDebugProtocol === false) {
debugConnectionType = 'telnet';
} else {
debugConnectionType = undefined;
}

this.reporter.sendTelemetryEvent('startDebugSession', {
enableDebugProtocol: boolToString(event.enableDebugProtocol),
retainDeploymentArchive: boolToString(event.retainDeploymentArchive),
retainStagingFolder: boolToString(event.retainStagingFolder),
injectRaleTrackerTask: boolToString(event.injectRaleTrackerTask),
isFilesDefined: isDefined(event.files),
isPreLaunchTaskDefined: isDefined(event.preLaunchTask),
isComponentLibrariesDefined: isDefined(event.componentLibraries),
isDeepLinkUrlDefined: isDefined(event.deepLinkUrl),
isStagingFolderPathDefined: isDefined(event.stagingFolderPath),
isLogfilePathDefined: isDefined(event.logfilePath),
enableDebugProtocol: boolToString(initialConfig.enableDebugProtocol),
debugConnectionType: debugConnectionType?.toString(),
retainDeploymentArchive: boolToString(initialConfig.retainDeploymentArchive),
retainStagingFolder: boolToString(initialConfig.retainStagingFolder),
injectRaleTrackerTask: boolToString(initialConfig.injectRaleTrackerTask),
isFilesDefined: isDefined(initialConfig.files),
isPreLaunchTaskDefined: isDefined(initialConfig.preLaunchTask),
isComponentLibrariesDefined: isDefined(initialConfig.componentLibraries),
isDeepLinkUrlDefined: isDefined(initialConfig.deepLinkUrl),
isStagingFolderPathDefined: isDefined(initialConfig.stagingFolderPath),
isLogfilePathDefined: isDefined(initialConfig.logfilePath),
isExtensionLogfilePathDefined: isDefined(
vscode.workspace.getConfiguration('brightscript').get<string>('extensionLogfilePath')
),
Expand Down
6 changes: 6 additions & 0 deletions src/mockVscode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ export let vscode = {
} as OutputChannel;
},
registerTreeDataProvider: function(viewId: string, treeDataProvider: TreeDataProvider<any>) { },
showInformationMessage: function(message: string) {

},
showWarningMessage: function(message: string) {

},
showErrorMessage: function(message: string) {

},
Expand Down
Loading

0 comments on commit 49e6b7e

Please sign in to comment.