Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support relaunch debug protocol #181

Merged
merged 21 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5f5e8de
Use the telnet output to know when the debug protocol port is open
Christian-Holbrook Dec 21, 2023
01c8af5
Call processTelnetOutput in the DebugProtocolAdapter. Fix broken unit…
Christian-Holbrook Dec 22, 2023
0879308
Update src/adapters/DebugProtocolAdapter.ts
Christian-Holbrook Jan 2, 2024
a47dcfb
Update the adapter connected promise function from deferredConnection…
Christian-Holbrook Jan 2, 2024
3aa7b82
Merge branch 'master' into parse-telnet-for-debug-prompt
Christian-Holbrook Jan 2, 2024
e13ab4a
When the app exits, clear breakpoints and various state to handle the…
Christian-Holbrook Jan 5, 2024
a71026e
Remove deferred promise. Create new signals to create debug protocol …
Christian-Holbrook Jan 8, 2024
c8baa7d
Add app-ready signal for telnet adapter
Christian-Holbrook Jan 9, 2024
4918406
Add comment clarify what the emit is for
Christian-Holbrook Jan 9, 2024
a0b2ac0
Merge branch 'master' into support-relaunch-debug-protocol
Christian-Holbrook Jan 9, 2024
4690606
Update src/debugProtocol/client/DebugProtocolClient.ts
Christian-Holbrook Jan 9, 2024
afa0e32
Update src/adapters/DebugProtocolAdapter.ts
Christian-Holbrook Jan 9, 2024
2d1a85e
Addressing pull request comments
Christian-Holbrook Jan 9, 2024
3c11754
Create DebugProtocolClient when initializing tests
Christian-Holbrook Jan 9, 2024
4a6be5d
Merge branch 'master' into support-relaunch-debug-protocol
Christian-Holbrook Jan 24, 2024
fbd20d4
Remove polling for DebugProtocolClient socket connection
Christian-Holbrook Jan 24, 2024
1066ab4
DebugProtocolClient will not destroy itself anymore
Christian-Holbrook Jan 29, 2024
1d64723
Merge branch 'master' into support-relaunch-debug-protocol
Christian-Holbrook Jan 29, 2024
870e78e
Merge branch 'master' into support-relaunch-debug-protocol
Christian-Holbrook Jan 30, 2024
b85f3e0
Rename socketDebugger to client. Before creating the client, check if…
Christian-Holbrook Jan 31, 2024
4318fdb
Remove reference to socketDebugger in unit tests
Christian-Holbrook Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/adapters/DebugProtocolAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ describe('DebugProtocolAdapter', function() {
* Handles the initial connection and the "stop at first byte code" flow
*/
async function initialize() {
sinon.stub(adapter, 'processTelnetOutput').callsFake(async () => {});

await adapter.connect();
client = adapter['socketDebugger'];
await adapter['createDebugProtocolClient']();

client = adapter['client'];
client['options'].shutdownTimeout = 100;
client['options'].exitChannelTimeout = 100;
//disable logging for tests because they clutter the test output
Expand Down
129 changes: 84 additions & 45 deletions src/adapters/DebugProtocolAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class DebugProtocolAdapter {
private compileErrorProcessor: CompileErrorProcessor;
private emitter: EventEmitter;
private chanperfTracker: ChanperfTracker;
private socketDebugger: DebugProtocolClient;
private client: DebugProtocolClient;
private nextFrameId = 1;

private stackFramesCache: Record<number, StackFrame> = {};
Expand All @@ -71,11 +71,9 @@ export class DebugProtocolAdapter {
* Get the version of the protocol for the Roku device we're currently connected to.
*/
public get activeProtocolVersion() {
return this.socketDebugger?.protocolVersion;
return this.client?.protocolVersion;
}

public readonly supportsMultipleRuns = false;

/**
* Subscribe to an event exactly once
* @param eventName
Expand All @@ -84,6 +82,7 @@ export class DebugProtocolAdapter {
public once(eventname: 'chanperf'): Promise<ChanperfData>;
public once(eventName: 'close'): Promise<void>;
public once(eventName: 'app-exit'): Promise<void>;
public once(eventName: 'app-ready'): Promise<void>;
public once(eventName: 'diagnostics'): Promise<BSDebugDiagnostic>;
public once(eventName: 'connected'): Promise<boolean>;
public once(eventname: 'console-output'): Promise<string>; // TODO: might be able to remove this at some point
Expand Down Expand Up @@ -119,6 +118,7 @@ export class DebugProtocolAdapter {
public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void);
public on(eventName: 'suspend', handler: () => void);
public on(eventName: 'start', handler: () => void);
public on(eventName: 'waiting-for-debugger', handler: () => void);
public on(eventname: 'unhandled-console-output', handler: (output: string) => void);
public on(eventName: string, handler: (payload: any) => void) {
this.emitter?.on(eventName, handler);
Expand All @@ -130,7 +130,7 @@ export class DebugProtocolAdapter {
private emit(eventName: 'suspend');
private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent);
private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]);
private emit(eventName: 'app-exit' | 'cannot-continue' | 'chanperf' | 'close' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output', data?);
private emit(eventName: 'app-exit' | 'app-ready' | 'cannot-continue' | 'chanperf' | 'close' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output' | 'waiting-for-debugger', data?);
private emit(eventName: string, data?) {
//emit these events on next tick, otherwise they will be processed immediately which could cause issues
setTimeout(() => {
Expand Down Expand Up @@ -203,19 +203,36 @@ export class DebugProtocolAdapter {
}

public get isAtDebuggerPrompt() {
return this.socketDebugger?.isStopped ?? false;
return this.client?.isStopped ?? false;
}

/**
* Connect to the telnet session. This should be called before the channel is launched.
*/
public async connect() {
//Start processing telnet output to look for compile errors or the debugger prompt
await this.processTelnetOutput();

this.on('waiting-for-debugger', () => {
void this.createDebugProtocolClient();
});
}

public async createDebugProtocolClient() {
let deferred = defer();
this.socketDebugger = new DebugProtocolClient(this.options);
if (this.client) {
await Promise.race([
util.sleep(2000),
await this.client.destroy()
]);
this.client = undefined;
}
this.client = new DebugProtocolClient(this.options);
await this.client.connect();
try {
// Emit IO from the debugger.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.socketDebugger.on('io-output', async (responseText) => {
this.client.on('io-output', async (responseText) => {
if (typeof responseText === 'string') {
responseText = this.chanperfTracker.processLog(responseText);
responseText = await this.rendezvousTracker.processLog(responseText);
Expand All @@ -225,7 +242,7 @@ export class DebugProtocolAdapter {
});

// Emit IO from the debugger.
this.socketDebugger.on('protocol-version', (data: ProtocolVersionDetails) => {
this.client.on('protocol-version', (data: ProtocolVersionDetails) => {
if (data.errorCode === PROTOCOL_ERROR_CODES.SUPPORTED) {
this.emit('console-output', data.message);
} else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_TESTED) {
Expand All @@ -246,35 +263,39 @@ export class DebugProtocolAdapter {
});

// Listen for the close event
this.socketDebugger.on('close', () => {
this.client.on('close', () => {
this.emit('close');
this.beginAppExit();
void this.client.destroy();
this.client = undefined;
});

// Listen for the app exit event
this.socketDebugger.on('app-exit', () => {
this.client.on('app-exit', () => {
this.emit('app-exit');
void this.client.destroy();
this.client = undefined;
});

this.socketDebugger.on('suspend', (data) => {
this.client.on('suspend', (data) => {
this.clearCache();
this.emit('suspend');
});

this.socketDebugger.on('runtime-error', (data) => {
this.client.on('runtime-error', (data) => {
console.debug('hasRuntimeError!!', data);
this.emit('runtime-error', <BrightScriptRuntimeError>{
message: data.data.stopReasonDetail,
errorCode: data.data.stopReason
});
});

this.socketDebugger.on('cannot-continue', () => {
this.client.on('cannot-continue', () => {
this.emit('cannot-continue');
});

//handle when the device verifies breakpoints
this.socketDebugger.on('breakpoints-verified', (event) => {
this.client.on('breakpoints-verified', (event) => {
let unverifiableDeviceIds = [] as number[];

//mark the breakpoints as verified
Expand All @@ -287,21 +308,12 @@ export class DebugProtocolAdapter {
//if there were any unsuccessful breakpoint verifications, we need to ask the device to delete those breakpoints as they've gone missing on our side
if (unverifiableDeviceIds.length > 0) {
this.logger.warn('Could not find breakpoints to verify. Removing from device:', { deviceBreakpointIds: unverifiableDeviceIds });
void this.socketDebugger.removeBreakpoints(unverifiableDeviceIds);
void this.client.removeBreakpoints(unverifiableDeviceIds);
}
this.emit('breakpoints-verified', event);
});

this.connected = await this.socketDebugger.connect();

this.logger.log(`Closing telnet connection used for compile errors`);
if (this.compileClient) {
this.compileClient.removeAllListeners();
this.compileClient.destroy();
this.compileClient = undefined;
}

this.socketDebugger.on('compile-error', (update) => {
this.client.on('compile-error', (update) => {
let diagnostics: BSDebugDiagnostic[] = [];
diagnostics.push({
path: update.data.filePath,
Expand All @@ -313,7 +325,12 @@ export class DebugProtocolAdapter {
this.emit('diagnostics', diagnostics);
});

this.client.on('control-socket-connected', () => {
this.emit('app-ready');
});

this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected });
this.connected = true;
this.emit('connected', this.connected);

//the adapter is connected and running smoothly. resolve the promise
Expand All @@ -338,7 +355,13 @@ export class DebugProtocolAdapter {
return semver.satisfies(this.deviceInfo.brightscriptDebuggerVersion, '>=3.1.0');
}

public async watchCompileOutput() {
private processingTelnetOutput = false;
public async processTelnetOutput() {
if (this.processingTelnetOutput) {
return;
}
this.processingTelnetOutput = true;

let deferred = defer();
try {
this.compileClient = new Socket();
Expand Down Expand Up @@ -376,6 +399,7 @@ export class DebugProtocolAdapter {
lastPartialLine = '';
}
// Emit the completed io string.
this.findWaitForDebuggerPrompt(responseText.trim());
this.compileErrorProcessor.processUnhandledLines(responseText.trim());
this.emit('unhandled-console-output', responseText.trim());
}
Expand All @@ -389,30 +413,39 @@ export class DebugProtocolAdapter {
return deferred.promise;
}

private findWaitForDebuggerPrompt(responseText: string) {
let lines = responseText.split(/\r?\n/g);
for (const line of lines) {
if (/Waiting for debugger on \d+\.\d+\.\d+\.\d+:8081/g.exec(line)) {
this.emit('waiting-for-debugger');
}
}
}

/**
* Send command to step over
*/
public async stepOver(threadId: number) {
this.clearCache();
return this.socketDebugger.stepOver(threadId);
return this.client.stepOver(threadId);
}

public async stepInto(threadId: number) {
this.clearCache();
return this.socketDebugger.stepIn(threadId);
return this.client.stepIn(threadId);
}

public async stepOut(threadId: number) {
this.clearCache();
return this.socketDebugger.stepOut(threadId);
return this.client.stepOut(threadId);
}

/**
* Tell the brightscript program to continue (i.e. resume program)
*/
public async continue() {
this.clearCache();
return this.socketDebugger.continue();
return this.client.continue();
}

/**
Expand All @@ -421,7 +454,7 @@ export class DebugProtocolAdapter {
public async pause() {
this.clearCache();
//send the kill signal, which breaks into debugger mode
return this.socketDebugger.pause();
return this.client.pause();
}

/**
Expand All @@ -437,7 +470,7 @@ export class DebugProtocolAdapter {
* @param command
* @returns the output of the command (if possible)
*/
public async evaluate(command: string, frameId: number = this.socketDebugger.primaryThread): Promise<RokuAdapterEvaluateResponse> {
public async evaluate(command: string, frameId: number = this.client.primaryThread): Promise<RokuAdapterEvaluateResponse> {
if (this.supportsExecuteCommand) {
if (!this.isAtDebuggerPrompt) {
throw new Error('Cannot run evaluate: debugger is not paused');
Expand All @@ -449,7 +482,7 @@ export class DebugProtocolAdapter {
}
this.logger.log('evaluate ', { command, frameId });

const response = await this.socketDebugger.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex);
const response = await this.client.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex);
this.logger.info('evaluate response', { command, response });
if (response.data.executeSuccess) {
return {
Expand All @@ -475,14 +508,14 @@ export class DebugProtocolAdapter {
}
}

public async getStackTrace(threadIndex: number = this.socketDebugger.primaryThread) {
public async getStackTrace(threadIndex: number = this.client.primaryThread) {
if (!this.isAtDebuggerPrompt) {
throw new Error('Cannot get stack trace: debugger is not paused');
}
return this.resolve(`stack trace for thread ${threadIndex}`, async () => {
let thread = await this.getThreadByThreadId(threadIndex);
let frames: StackFrame[] = [];
let stackTraceData = await this.socketDebugger.getStackTrace(threadIndex);
let stackTraceData = await this.client.getStackTrace(threadIndex);
for (let i = 0; i < stackTraceData?.data?.entries?.length ?? 0; i++) {
let frameData = stackTraceData.data.entries[i];
let stackFrame: StackFrame = {
Expand Down Expand Up @@ -541,13 +574,13 @@ export class DebugProtocolAdapter {
variablePath[0] = variablePath[0].toLowerCase();
}

let response = await this.socketDebugger.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
let response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);

if (this.enableVariablesLowerCaseRetry && response.data.errorCode !== ErrorCode.OK) {
// Temporary workaround related to casing issues over the protocol
logger.log(`Retrying expression as lower case:`, expression);
variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase());
response = await this.socketDebugger.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
}
return response;
}
Expand Down Expand Up @@ -685,14 +718,14 @@ export class DebugProtocolAdapter {
}
return this.resolve('threads', async () => {
let threads: Thread[] = [];
let threadsResponse = await this.socketDebugger.threads();
let threadsResponse = await this.client.threads();

for (let i = 0; i < threadsResponse.data?.threads?.length ?? 0; i++) {
let threadInfo = threadsResponse.data.threads[i];
let thread = <Thread>{
// NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
// NOTE: Rely on the thead index from the threads update event.
isSelected: this.socketDebugger.primaryThread === i,
isSelected: this.client.primaryThread === i,
// isSelected: threadInfo.isPrimary,
filePath: threadInfo.filePath,
functionName: threadInfo.functionName,
Expand Down Expand Up @@ -738,9 +771,9 @@ export class DebugProtocolAdapter {
this.isDestroyed = true;

// destroy the debug client if it's defined
if (this.socketDebugger) {
if (this.client) {
try {
await this.socketDebugger.destroy();
await this.client.destroy();
} catch (e) {
this.logger.error(e);
}
Expand All @@ -749,6 +782,12 @@ export class DebugProtocolAdapter {
this.cache = undefined;
this.removeAllListeners();
this.emitter = undefined;

if (this.compileClient) {
this.compileClient.removeAllListeners();
this.compileClient.destroy();
this.compileClient = undefined;
}
}

/**
Expand Down Expand Up @@ -791,7 +830,7 @@ export class DebugProtocolAdapter {
public async _syncBreakpoints() {
//we can't send breakpoints unless we're stopped (or in a protocol version that supports sending them while running).
//So...if we're not stopped, quit now. (we'll get called again when the stop event happens)
if (!this.socketDebugger?.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) {
if (!this.client?.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) {
return;
}

Expand All @@ -801,7 +840,7 @@ export class DebugProtocolAdapter {

// REMOVE breakpoints (delete these breakpoints from the device)
if (diff.removed.length > 0) {
const response = await this.socketDebugger.removeBreakpoints(
const response = await this.client.removeBreakpoints(
//TODO handle retrying to remove breakpoints that don't have deviceIds yet but might get one in the future
diff.removed.map(x => x.deviceId).filter(x => typeof x === 'number')
);
Expand Down Expand Up @@ -837,7 +876,7 @@ export class DebugProtocolAdapter {
}
}
for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) {
const response = await this.socketDebugger.addBreakpoints(breakpoints);
const response = await this.client.addBreakpoints(breakpoints);

//if the response was successful, and we have the correct number of breakpoints in the response
if (response.data.errorCode === ErrorCode.OK && response?.data?.breakpoints?.length === breakpoints.length) {
Expand Down
Loading