diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 94badd12..5065b547 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -20,6 +20,7 @@ import type { Variable } from '../debugProtocol/events/responses/VariablesRespon import { VariableType } from '../debugProtocol/events/responses/VariablesResponse'; import type { TelnetAdapter } from './TelnetAdapter'; import type { DeviceInfo } from 'roku-deploy'; +import type { ThreadsResponse } from '../debugProtocol/events/responses/ThreadsResponse'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. @@ -228,7 +229,6 @@ export class DebugProtocolAdapter { 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 @@ -266,14 +266,14 @@ export class DebugProtocolAdapter { this.client.on('close', () => { this.emit('close'); this.beginAppExit(); - void this.client.destroy(); + void this.client?.destroy(); this.client = undefined; }); // Listen for the app exit event this.client.on('app-exit', () => { this.emit('app-exit'); - void this.client.destroy(); + void this.client?.destroy(); this.client = undefined; }); @@ -325,13 +325,12 @@ export class DebugProtocolAdapter { this.emit('diagnostics', diagnostics); }); - this.client.on('control-socket-connected', () => { - this.emit('app-ready'); - }); + await this.client.connect(); this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected }); this.connected = true; this.emit('connected', this.connected); + this.emit('app-ready'); //the adapter is connected and running smoothly. resolve the promise deferred.resolve(); @@ -718,7 +717,24 @@ export class DebugProtocolAdapter { } return this.resolve('threads', async () => { let threads: Thread[] = []; - let threadsResponse = await this.client.threads(); + let threadsResponse: ThreadsResponse; + // sometimes roku threads are stubborn and haven't stopped yet, causing our ThreadsRequest to fail with "not stopped". + // A nice simple fix for this is to just send a "pause" request again, which seems to fix the issue. + // we'll do this a few times just to make sure we've tried our best to get the list of threads. + for (let i = 0; i < 3; i++) { + threadsResponse = await this.client.threads(); + if (threadsResponse.data.errorCode === ErrorCode.NOT_STOPPED) { + this.logger.log(`Threads request retrying... ${i}:\n`, threadsResponse); + threadsResponse = undefined; + const pauseResponse = await this.client.pause(true); + await util.sleep(100); + } else { + break; + } + } + if (!threadsResponse) { + return []; + } for (let i = 0; i < threadsResponse.data?.threads?.length ?? 0; i++) { let threadInfo = threadsResponse.data.threads[i]; diff --git a/src/debugProtocol/client/DebugProtocolClient.ts b/src/debugProtocol/client/DebugProtocolClient.ts index 55c78e22..a48f085f 100644 --- a/src/debugProtocol/client/DebugProtocolClient.ts +++ b/src/debugProtocol/client/DebugProtocolClient.ts @@ -185,7 +185,6 @@ export class DebugProtocolClient { public on(eventName: 'runtime-error' | 'suspend', handler: (data: T) => void); public on(eventName: 'io-output', handler: (output: string) => void); public on(eventName: 'protocol-version', handler: (data: ProtocolVersionDetails) => void); - public on(eventName: 'control-socket-connected', handler: () => void); public on(eventName: 'handshake-verified', handler: (data: HandshakeResponse) => void); // public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); // public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); @@ -202,7 +201,7 @@ export class DebugProtocolClient { private emit(eventName: 'data', update: Buffer); private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent); private emit(eventName: 'suspend' | 'runtime-error', data: AllThreadsStoppedUpdate | ThreadAttachedUpdate); - private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'control-socket-connected' | 'start', data?); + private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'start', data?); private async emit(eventName: string, data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues await util.sleep(0); @@ -230,7 +229,6 @@ export class DebugProtocolClient { client: this, server: connection }); - await this.emit('control-socket-connected'); return connection; } @@ -337,16 +335,17 @@ export class DebugProtocolClient { } } - public pause() { + public pause(force = false) { return this.processStopRequest( StopRequest.fromJson({ requestId: this.requestIdSequence++ - }) + }), + force ); } - private async processStopRequest(request: StopRequest) { - if (this.isStopped === false) { + private async processStopRequest(request: StopRequest, force = false) { + if (this.isStopped === false || force) { return this.sendRequest(request); } } diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 8c038d60..a8120578 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -777,6 +777,15 @@ export class BrightScriptDebugSession extends BaseDebugSession { new Thread(thread.threadId, `Thread ${thread.threadId}`) ); } + + if (threads.length === 0) { + threads = [{ + id: 1001, + name: 'unable to retrieve threads: not stopped', + isFake: true + }]; + } + } else { this.logger.log('Skipped getting threads because the RokuAdapter is not accepting input at this time.'); } @@ -793,7 +802,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { try { this.logger.log('stackTraceRequest'); - let frames = []; + let frames: DebugProtocol.StackFrame[] = []; //this is a bit of a hack. If there's a compile error, send a full stack frame so we can show the compile error like a runtime crash if (this.compileError) { @@ -805,6 +814,15 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.compileError.range.start.line + 1, this.compileError.range.start.character + 1 )); + } else if (args.threadId === 1001) { + frames.push(new StackFrame( + 0, + 'ERROR: threads would not stop', + new Source('main.brs', s`${this.launchConfiguration.stagingDir}/manifest`), + 1, + 1 + )); + this.showPopupMessage('Unable to suspend threads. Debugger is in an unstable state, please press Continue to resume debugging', 'warn'); } else { if (this.rokuAdapter.isAtDebuggerPrompt) { let stackTrace = await this.rokuAdapter.getStackTrace(args.threadId);