• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / roku-debug / 28819206890

06 Jul 2026 07:56PM UTC coverage: 72.936% (+0.3%) from 72.68%
28819206890

Pull #323

github

web-flow
Merge b8cb99869 into 1f928695c
Pull Request #323: apply postfix for Library statement

3847 of 5504 branches covered (69.89%)

Branch coverage included in aggregate %.

92 of 119 new or added lines in 3 files covered. (77.31%)

712 existing lines in 3 files now uncovered.

6022 of 8027 relevant lines covered (75.02%)

47.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.58
/src/adapters/DebugProtocolAdapter.ts
1
import * as EventEmitter from 'events';
2✔
2
import { Socket } from 'net';
2✔
3
import { DiagnosticSeverity, util as bscUtil } from 'brighterscript';
2✔
4
import type { BSDebugDiagnostic } from '../CompileErrorProcessor';
5
import { CompileErrorProcessor } from '../CompileErrorProcessor';
2✔
6
import type { RendezvousHistory, RendezvousTracker } from '../RendezvousTracker';
7
import type { ChanperfData } from '../ChanperfTracker';
8
import { ChanperfTracker } from '../ChanperfTracker';
2✔
9
import { ErrorCode, PROTOCOL_ERROR_CODES, UpdateType } from '../debugProtocol/Constants';
2✔
10
import { defer, util } from '../util';
2✔
11
import { logger } from '../logging';
2✔
12
import type { AdapterOptions, HighLevelType, RokuAdapterEvaluateResponse } from '../interfaces';
13
import type { BreakpointManager } from '../managers/BreakpointManager';
14
import type { ProjectManager } from '../managers/ProjectManager';
15
import type { BreakpointsVerifiedEvent, ConstructorOptions, ProtocolVersionDetails } from '../debugProtocol/client/DebugProtocolClient';
16
import { DebugProtocolClient } from '../debugProtocol/client/DebugProtocolClient';
2✔
17
import { ProtocolCapabilities } from '../debugProtocol/client/ProtocolCapabilities';
2✔
18
import type { Variable } from '../debugProtocol/events/responses/VariablesResponse';
19
import { VariableType } from '../debugProtocol/events/responses/VariablesResponse';
2✔
20
import type { TelnetAdapter } from './TelnetAdapter';
21
import type { DeviceInfo } from 'roku-deploy';
22
import type { ThreadsResponse } from '../debugProtocol/events/responses/ThreadsResponse';
23
import type { ExceptionBreakpoint } from '../debugProtocol/events/requests/SetExceptionBreakpointsRequest';
24
import { insertCustomVariables, overrideKeyTypesForCustomVariables } from './customVariableUtils';
2✔
25
import type { DebugProtocol } from '@vscode/debugprotocol';
26
import { SocketConnectionInUseError } from '../Exceptions';
2✔
27

28
/**
29
 * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it.
30
 */
31
export class DebugProtocolAdapter {
2✔
32
    constructor(
33
        private options: AdapterOptions & ConstructorOptions,
21✔
34
        private projectManager: ProjectManager,
21✔
35
        private breakpointManager: BreakpointManager,
21✔
36
        private rendezvousTracker: RendezvousTracker,
21✔
37
        private deviceInfo: DeviceInfo
21✔
38
    ) {
39
        util.normalizeAdapterOptions(this.options);
21✔
40
        this.emitter = new EventEmitter();
21✔
41
        this.chanperfTracker = new ChanperfTracker();
21✔
42
        this.compileErrorProcessor = new CompileErrorProcessor();
21✔
43
        this.connected = false;
21✔
44
        //capabilities derived from device-info; used to answer questions before the debug
45
        //protocol client has connected and completed its handshake
46
        this.fallbackCapabilities = new ProtocolCapabilities(this.deviceInfo?.brightscriptDebuggerVersion, this.deviceInfo?.softwareVersion);
21!
47

48
        // watch for chanperf events
49
        this.chanperfTracker.on('chanperf', (output) => {
21✔
50
            this.emit('chanperf', output);
×
51
        });
52
    }
53

54
    private logger = logger.createLogger(`[padapter]`);
21✔
55

56
    /**
57
     * Capabilities seeded from the device-info `brightscript-debugger-version`. Used as the
58
     * source of truth for protocol capability questions before the debug protocol client has
59
     * connected and completed its handshake.
60
     */
61
    private fallbackCapabilities: ProtocolCapabilities;
62

63
    /**
64
     * The current authoritative capabilities for protocol-version-driven questions: the live
65
     * client's capabilities once it exists, otherwise the device-info-seeded fallback.
66
     */
67
    private get capabilities(): ProtocolCapabilities {
68
        return this.client?.capabilities ?? this.fallbackCapabilities;
50✔
69
    }
70

71
    /**
72
     * Indicates whether the adapter has successfully established a connection with the device
73
     */
74
    public connected: boolean;
75

76
    private compileClient: Socket;
77
    private compileErrorProcessor: CompileErrorProcessor;
78

79
    /**
80
     * Stop surfacing compile errors, waiting for in-flight device output to settle first. Use this around an
81
     * operation that intentionally produces transient compile errors (e.g. deleting interdependent component
82
     * libraries). Pair with `resumeCompileErrors()`.
83
     */
84
    public async pauseCompileErrors() {
UNCOV
85
        await this.compileErrorProcessor.settle();
×
UNCOV
86
        this.compileErrorProcessor.pause();
×
87
    }
88

89
    /**
90
     * Resume surfacing compile errors, waiting for the noisy output to settle first so it isn't surfaced.
91
     */
92
    public async resumeCompileErrors() {
UNCOV
93
        await this.compileErrorProcessor.settle();
×
UNCOV
94
        this.compileErrorProcessor.resume();
×
95
    }
96

97
    private emitter: EventEmitter;
98
    private chanperfTracker: ChanperfTracker;
99
    private client: DebugProtocolClient;
100
    private nextFrameId = 1;
21✔
101

102
    private stackFramesCache: Record<number, StackFrame> = {};
21✔
103
    private cache = {};
21✔
104

105
    /**
106
     * Get the version of the protocol for the Roku device we're currently connected to.
107
     */
108
    public get activeProtocolVersion() {
UNCOV
109
        return this.client?.protocolVersion;
×
110
    }
111

112
    /**
113
     * Subscribe to an event exactly once
114
     * @param eventName
115
     */
116
    public once(eventName: 'cannot-continue'): Promise<void>;
117
    public once(eventname: 'chanperf'): Promise<ChanperfData>;
118
    public once(eventName: 'close'): Promise<void>;
119
    public once(eventName: 'app-exit'): Promise<void>;
120
    public once(eventName: 'app-ready'): Promise<void>;
121
    public once(eventName: 'diagnostics'): Promise<BSDebugDiagnostic>;
122
    public once(eventName: 'connected'): Promise<boolean>;
123
    public once(eventname: 'console-output'): Promise<string>; // TODO: might be able to remove this at some point
124
    public once(eventname: 'protocol-version'): Promise<ProtocolVersionDetails>;
125
    public once(eventname: 'rendezvous'): Promise<RendezvousHistory>;
126
    public once(eventName: 'runtime-error'): Promise<BrightScriptRuntimeError>;
127
    public once(eventName: 'suspend'): Promise<void>;
128
    public once(eventName: 'start'): Promise<void>;
129
    public once(eventname: 'device-unresponsive'): Promise<void>;
130
    public once(eventname: 'unhandled-console-output'): Promise<string>;
131
    public once(eventName: string) {
132
        return new Promise((resolve) => {
16✔
133
            const disconnect = this.on(eventName as Parameters<DebugProtocolAdapter['on']>[0], (...args) => {
16✔
134
                disconnect();
16✔
135
                resolve(...args);
16✔
136
            });
137
        });
138
    }
139

140
    /**
141
     * Subscribe to various events
142
     * @param eventName
143
     * @param handler
144
     */
145
    public on(eventName: 'breakpoints-verified', handler: (event: BreakpointsVerifiedEvent) => any);
146
    public on(eventName: 'cannot-continue', handler: () => any);
147
    public on(eventname: 'chanperf', handler: (output: ChanperfData) => any);
148
    public on(eventName: 'close', handler: () => any);
149
    public on(eventName: 'app-exit', handler: () => any);
150
    public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => any);
151
    public on(eventName: 'launch-status', handler: (message: string) => any);
152
    public on(eventName: 'connected', handler: (params: boolean) => any);
153
    public on(eventname: 'console-output', handler: (output: string) => any); // TODO: might be able to remove this at some point.
154
    public on(eventname: 'protocol-version', handler: (output: ProtocolVersionDetails) => any);
155
    public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => any);
156
    public on(eventName: 'suspend', handler: () => any);
157
    public on(eventName: 'start', handler: () => any);
158
    public on(eventName: 'waiting-for-debugger', handler: () => any);
159
    public on(eventName: 'device-unresponsive', handler: (data: { lastCommand: string }) => any);
160
    public on(eventname: 'unhandled-console-output', handler: (output: string) => any);
161
    public on(eventName: string, handler: (payload: any) => any) {
162
        this.emitter?.on(eventName, handler);
33!
163
        return () => {
33✔
164
            this.emitter?.removeListener(eventName, handler);
16!
165
        };
166
    }
167

168
    private emit(eventName: 'suspend');
169
    private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent);
170
    private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]);
171
    private emit(eventName: 'launch-status', message: string);
172
    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' | 'device-unresponsive', data?);
173
    private emit(eventName: string, data?) {
174
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
175
        setTimeout(() => {
72✔
176
            //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists
177
            if (!this.emitter) {
72!
UNCOV
178
                return;
×
179
            }
180
            //drop stale 'suspend'/'runtime-error' events when the debugger has already resumed.
181
            //emit() defers via setTimeout, so isStopped can flip false between queue and fire (e.g. auto-continue on entry breakpoint),
182
            //causing downstream handlers to call getThreads() against a running debugger.
183
            //See https://github.com/rokucommunity/vscode-brightscript-language/issues/798
184
            if ((eventName === 'suspend' || eventName === 'runtime-error') && !this.isAtDebuggerPrompt) {
72✔
185
                this.logger.warn(`Dropping stale "${eventName}" event because debugger is no longer paused`);
1✔
186
                return;
1✔
187
            }
188
            this.emitter.emit(eventName, data);
71✔
189
        }, 0);
190
    }
191

192
    /**
193
     * Does the current client support exception breakpoints? Resolved via the live client's
194
     * capabilities when connected, otherwise the device-info-seeded fallback.
195
     */
196
    public get supportsExceptionBreakpoints(): boolean {
UNCOV
197
        return this.capabilities.supportsExceptionBreakpoints;
×
198
    }
199

200
    /**
201
     * Does the current client support conditional breakpoints? Same fallback semantics as
202
     * `supportsExceptionBreakpoints`.
203
     */
204
    public get supportsConditionalBreakpoints(): boolean {
UNCOV
205
        return this.capabilities.supportsConditionalBreakpoints;
×
206
    }
207

208
    /**
209
     * Does the current client support hit-count breakpoints?
210
     */
211
    public get supportsHitConditionalBreakpoints(): boolean {
UNCOV
212
        return this.capabilities.supportsHitConditionalBreakpoints;
×
213
    }
214

215
    /**
216
     * The debugger needs to tell us when to be active (i.e. when the package was deployed)
217
     */
218
    public isActivated = false;
21✔
219

220
    /**
221
     * This will be set to true When the roku emits the [scrpt.ctx.run.enter] text,
222
     * which indicates that the app is running on the Roku
223
     */
224
    public isAppRunning = false;
21✔
225

226
    public activate() {
UNCOV
227
        this.isActivated = true;
×
UNCOV
228
        this.handleStartupIfReady();
×
229
    }
230

231
    public async sendErrors() {
UNCOV
232
        await this.compileErrorProcessor.sendErrors();
×
233
    }
234

235
    private handleStartupIfReady() {
236
        if (this.isActivated && this.isAppRunning) {
16!
237
            this.emit('start');
×
238

239
            //if we are already sitting at a debugger prompt, we need to emit the first suspend event.
240
            //If not, then there are probably still messages being received, so let the normal handler
241
            //emit the suspend event when it's ready
242
            if (this.isAtDebuggerPrompt === true) {
×
243
                this.emit('suspend');
×
244
            }
245
        }
246
    }
247

248
    /**
249
     * Wait until the client has stopped sending messages. This is used mainly during .connect so we can ignore all old messages from the server
250
     * @param client
251
     * @param maxWaitMilliseconds
252
     */
253
    private settleCompileClient(client: Socket, maxWaitMilliseconds = 400) {
×
UNCOV
254
        return new Promise<string>((resolve) => {
×
255
            let timeoutStarted = false;
×
256
            let callCount = -1;
×
257
            let logs = '';
×
258

259
            function handler(buffer) {
UNCOV
260
                callCount++;
×
UNCOV
261
                logs += buffer.toString();
×
262
                let myCallCount = callCount;
×
UNCOV
263
                timeoutStarted = true;
×
UNCOV
264
                setTimeout(() => {
×
265
                    if (myCallCount === callCount) {
×
266
                        // stop listening for data events
267
                        client.removeListener('data', handler);
×
UNCOV
268
                        resolve(logs);
×
269
                    }
270
                }, maxWaitMilliseconds);
271
            }
272

UNCOV
273
            const startTimeout = () => {
×
UNCOV
274
                if (timeoutStarted === false) {
×
UNCOV
275
                    handler(Buffer.from(''));
×
276
                }
277
            };
278

279
            // watch for data events
UNCOV
280
            client.on('data', handler);
×
281

282
            // watch for different connection related events to start the timeout logic
UNCOV
283
            client.on('ready', startTimeout);
×
UNCOV
284
            client.on('end', startTimeout);
×
UNCOV
285
            client.on('closed', startTimeout);
×
286
        });
287
    }
288

289
    public get isAtDebuggerPrompt() {
290
        return this.client?.isStopped ?? false;
81!
291
    }
292

293
    private firstConnectDeferred = defer<void>();
21✔
294

295
    /**
296
     * Resolves when the first connection to the client is established
297
     */
298
    public onReady() {
UNCOV
299
        return this.firstConnectDeferred.promise;
×
300
    }
301

302
    /**
303
     * Connect to the telnet session. This should be called before the channel is launched.
304
     */
305
    public async connect(): Promise<void> {
306
        //Start processing telnet output to look for compile errors or the debugger prompt
307
        await this.processTelnetOutput();
16✔
308

309
        this.on('waiting-for-debugger', async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
16✔
UNCOV
310
            await this.createDebugProtocolClient();
×
311

312
            //if this is the first time we are connecting, resolve the promise.
313
            //(future events fire for "reconnect" situations, we don't need to resolve again for those)
UNCOV
314
            if (!this.firstConnectDeferred.isCompleted) {
×
UNCOV
315
                this.firstConnectDeferred.resolve();
×
316
            }
317
        });
318
    }
319

320
    public async createDebugProtocolClient() {
321
        let deferred = defer();
16✔
322
        if (this.client) {
16!
323
            await Promise.race([
×
324
                util.sleep(2000),
325
                await this.client.destroy()
326
            ]);
UNCOV
327
            this.client = undefined;
×
328
            //keep `connected` in sync with `client` so the _syncBreakpoints entry guard
329
            //(and similar checks elsewhere) reflects the actual state. Restored to true below
330
            //once the new client finishes connecting.
UNCOV
331
            this.connected = false;
×
332
        }
333
        this.client = new DebugProtocolClient(this.options);
16✔
334
        try {
16✔
335
            // Emit IO from the debugger.
336
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
337
            this.client.on('io-output', async (responseText) => {
16✔
UNCOV
338
                if (typeof responseText === 'string') {
×
UNCOV
339
                    responseText = this.chanperfTracker.processLog(responseText);
×
UNCOV
340
                    responseText = await this.rendezvousTracker.processLog(responseText);
×
UNCOV
341
                    this.emit('unhandled-console-output', responseText);
×
UNCOV
342
                    this.emit('console-output', responseText);
×
343
                }
344
            });
345

346
            // Emit IO from the debugger.
347
            this.client.on('protocol-version', (data: ProtocolVersionDetails) => {
16✔
348
                if (data.errorCode === PROTOCOL_ERROR_CODES.SUPPORTED) {
16!
349
                    this.emit('console-output', data.message);
16✔
UNCOV
350
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_TESTED) {
×
UNCOV
351
                    this.emit('unhandled-console-output', data.message);
×
UNCOV
352
                    this.emit('console-output', data.message);
×
UNCOV
353
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_SUPPORTED) {
×
UNCOV
354
                    this.emit('unhandled-console-output', data.message);
×
UNCOV
355
                    this.emit('console-output', data.message);
×
356
                }
357

358
            });
359

360
            // Listen for the close event
361
            this.client.on('close', () => {
16✔
362
                this.emit('close');
1✔
363
                this.beginAppExit();
1✔
364
                void this.client?.destroy();
1!
365
                this.client = undefined;
1✔
366
                //the protocol client is gone — keep `connected` in sync so any subsequent
367
                //syncBreakpoints / setExceptionBreakpoints calls during the close→app-exit
368
                //window early-return instead of dereferencing the undefined client.
369
                //See https://github.com/rokucommunity/vscode-brightscript-language/issues/811
370
                this.connected = false;
1✔
371
            });
372

373
            // Listen for the app exit event
374
            this.client.on('app-exit', () => {
16✔
UNCOV
375
                this.emit('app-exit');
×
376
                void this.client?.destroy();
×
UNCOV
377
                this.client = undefined;
×
378
            });
379

380
            this.client.on('suspend', (data) => {
16✔
381
                this.clearCache();
17✔
382
                this.emit('suspend');
17✔
383
            });
384

385
            this.client.on('runtime-error', (data) => {
16✔
UNCOV
386
                console.debug('hasRuntimeError!!', data);
×
UNCOV
387
                this.emit('runtime-error', <BrightScriptRuntimeError>{
×
388
                    message: data.data.stopReasonDetail,
389
                    errorCode: data.data.stopReason
390
                });
391
            });
392

393
            this.client.on('cannot-continue', () => {
16✔
UNCOV
394
                this.emit('cannot-continue');
×
395
            });
396

397
            //handle when the device verifies breakpoints
398
            this.client.on('breakpoints-verified', (event) => {
16✔
399
                let unverifiableDeviceIds = [] as number[];
6✔
400

401
                //mark the breakpoints as verified
402
                for (let breakpoint of event?.breakpoints ?? []) {
6!
403
                    const success = this.breakpointManager.verifyBreakpoint(breakpoint.id, true);
5✔
404
                    if (!success) {
5✔
405
                        unverifiableDeviceIds.push(breakpoint.id);
3✔
406
                    }
407
                }
408
                //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
409
                if (unverifiableDeviceIds.length > 0) {
6✔
410
                    this.logger.warn('Could not find breakpoints to verify. Removing from device:', { deviceBreakpointIds: unverifiableDeviceIds });
3✔
411
                    void this.client.removeBreakpoints(unverifiableDeviceIds);
3✔
412
                }
413
                this.emit('breakpoints-verified', event);
5✔
414
            });
415

416
            this.client.on('compile-error', (update) => {
16✔
UNCOV
417
                let diagnostics: BSDebugDiagnostic[] = [];
×
UNCOV
418
                diagnostics.push({
×
419
                    path: update.data.filePath,
420
                    range: bscUtil.createRange(update.data.lineNumber - 1, 0, update.data.lineNumber - 1, 999),
421
                    message: update.data.errorMessage,
422
                    severity: DiagnosticSeverity.Error,
423
                    code: undefined
424
                });
425
                this.emit('diagnostics', diagnostics);
×
426
            });
427

428
            await this.client.connect();
16✔
429

430
            this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected });
16✔
431
            this.connected = true;
16✔
432
            this.isAppRunning = true;
16✔
433
            this.handleStartupIfReady();
16✔
434
            this.emit('connected', this.connected);
16✔
435
            this.emit('app-ready');
16✔
436
            //flush any breakpoints that were queued while we were waiting for the client to connect.
437
            //setBreakpointsRequest is called by VS Code before the channel is uploaded, so the initial
438
            //sync bails out (no client yet); this re-sync pushes those queued breakpoints to the device.
439
            void this.syncBreakpoints();
16✔
440
            //also replay any queued exception breakpoint filters
441
            if (this.pendingExceptionBreakpointFilters) {
16!
UNCOV
442
                const queuedFilters = this.pendingExceptionBreakpointFilters;
×
UNCOV
443
                this.pendingExceptionBreakpointFilters = undefined;
×
UNCOV
444
                void this.client.setExceptionBreakpoints(queuedFilters);
×
445
            }
446

447
            //the adapter is connected and running smoothly. resolve the promise
448
            deferred.resolve();
16✔
449
        } catch (e) {
UNCOV
450
            deferred.reject(e);
×
451
        }
452
        return deferred.promise;
16✔
453
    }
454

455
    private beginAppExit() {
456
        this.compileErrorProcessor.compileErrorTimer = setTimeout(() => {
1✔
457
            this.isAppRunning = false;
1✔
458
            this.emit('app-exit');
1✔
459
        }, 200);
460
    }
461

462
    /**
463
     * Determines if the current version of the debug protocol supports emitting compile error updates.
464
     */
465
    public get supportsCompileErrorReporting() {
UNCOV
466
        return this.capabilities.supportsCompileErrorReporting;
×
467
    }
468

469
    /**
470
     * Indicate if virtual variables should be auto resolved when they are encountered.
471
     */
472
    public get autoResolveVirtualVariables() {
473
        return this.options.autoResolveVirtualVariables;
1✔
474
    }
475

476
    private processingTelnetOutput = false;
21✔
477
    public async processTelnetOutput() {
478
        if (this.processingTelnetOutput) {
1!
UNCOV
479
            return;
×
480
        }
481
        this.processingTelnetOutput = true;
1✔
482

483
        let deferred = defer();
1✔
484
        try {
1✔
485
            this.compileClient = new Socket({ allowHalfOpen: false });
1✔
486
            util.registerSocketLogging(this.compileClient, this.logger, 'CompileClient');
1✔
487

488
            this.compileErrorProcessor.on('diagnostics', (errors) => {
1✔
UNCOV
489
                this.compileClient.end();
×
UNCOV
490
                this.emit('diagnostics', errors);
×
491
            });
492

493
            this.compileErrorProcessor.on('launch-status', (message) => {
1✔
UNCOV
494
                this.emit('launch-status', message);
×
495
            });
496

497
            //if the connection fails, reject the connect promise.
498
            //Use tryReject (not reject) because this handler persists for the socket's lifetime.
499
            //After a successful connection the deferred is already resolved, so a post-connection
500
            //socket error (e.g. ECONNRESET on device disconnect) must not crash the process.
501
            this.compileClient.on('error', (err) => {
1✔
502
                deferred.tryReject(new Error(`Error with connection to: ${this.options.host}:${this.options.brightScriptConsolePort} \n\n ${err.message} `));
1✔
503
            });
504
            this.logger.info('Connecting via telnet to gather compile info', { host: this.options.host, port: this.options.brightScriptConsolePort });
1✔
505
            this.compileClient.connect(this.options.brightScriptConsolePort, this.options.host, () => {
1✔
506
                this.logger.log(`CONNECTED via telnet to gather compile info`, { host: this.options.host, port: this.options.brightScriptConsolePort });
×
507
            });
508

509
            this.logger.debug('Waiting for the compile client to settle');
1✔
510
            const settledLogs = await this.settleCompileClient(this.compileClient);
1✔
511
            this.logger.debug('Compile client has settled');
1✔
512
            this.logger.trace('Settled logs:', settledLogs);
1✔
513

514
            if (settledLogs.trim().startsWith('Console connection is already in use.')) {
1!
515
                throw new SocketConnectionInUseError(`Telnet connection ${this.options.host}:${this.options.brightScriptConsolePort} already is use`, {
×
516
                    port: this.options.brightScriptConsolePort,
517
                    host: this.options.host
518
                });
519
            }
520

521
            let lastPartialLine = '';
1✔
522
            this.compileClient.on('data', (buffer) => {
1✔
UNCOV
523
                let responseText = buffer.toString();
×
UNCOV
524
                this.logger.info('CompileClient received data', { responseText });
×
525

UNCOV
526
                let logResult = util.handleLogFragments(lastPartialLine, buffer.toString());
×
527

528
                // Save any remaining partial line for the next event
UNCOV
529
                lastPartialLine = logResult.remaining;
×
UNCOV
530
                if (logResult.completed) {
×
531
                    // Emit the completed io string.
UNCOV
532
                    this.findWaitForDebuggerPrompt(logResult.completed);
×
UNCOV
533
                    this.compileErrorProcessor.processUnhandledLines(logResult.completed);
×
UNCOV
534
                    this.logger.debug('CompileClient data:', logResult.completed);
×
UNCOV
535
                    this.emit('unhandled-console-output', logResult.completed);
×
536
                } else {
537
                    this.logger.debug('CompileClient buffer was split:', lastPartialLine);
×
538
                }
539
            });
540

541
            this.compileClient.on('close', () => {
1✔
542
                this.logger.log('compileClient socket closed');
1✔
543
                this.compileClientClosed.tryResolve();
1✔
544
            });
545

546
            // connected to telnet. resolve the promise
547
            deferred.resolve();
1✔
548
        } catch (e) {
549
            deferred.reject(e);
×
550
        }
551
        return deferred.promise;
1✔
552
    }
553

554
    private findWaitForDebuggerPrompt(responseText: string) {
555
        let lines = responseText.split(/\r?\n/g);
×
UNCOV
556
        for (const line of lines) {
×
UNCOV
557
            if (/Waiting for debugger on \d+\.\d+\.\d+\.\d+:8081/g.exec(line)) {
×
UNCOV
558
                this.emit('waiting-for-debugger');
×
559
            }
560
        }
561
    }
562

563
    /**
564
     * Send command to step over
565
     */
566
    public async stepOver(threadId: number) {
567
        this.clearCache();
×
568
        return this.client.stepOver(threadId);
×
569
    }
570

571
    public async stepInto(threadId: number) {
UNCOV
572
        this.clearCache();
×
UNCOV
573
        return this.client.stepIn(threadId);
×
574
    }
575

576
    public async stepOut(threadId: number) {
577
        this.clearCache();
×
UNCOV
578
        return this.client.stepOut(threadId);
×
579
    }
580

581
    /**
582
     * Tell the brightscript program to continue (i.e. resume program)
583
     */
584
    public async continue() {
UNCOV
585
        this.clearCache();
×
UNCOV
586
        return this.client.continue();
×
587
    }
588

589
    /**
590
     * Tell the brightscript program to pause (fall into debug mode)
591
     */
592
    public async pause() {
UNCOV
593
        this.clearCache();
×
594
        //send the kill signal, which breaks into debugger mode
595
        return this.client.pause();
×
596
    }
597

598
    /**
599
     * Clears the state, which means that everything will be retrieved fresh next time it is requested
600
     */
601
    public clearCache() {
602
        this.cache = {};
17✔
603
        this.stackFramesCache = {};
17✔
604
    }
605

606
    /**
607
     * Execute a command directly on the roku. Returns the output of the command
608
     * @param command
609
     * @returns the output of the command (if possible)
610
     */
611
    public async evaluate(command: string, frameId: number = this.client.primaryThread): Promise<RokuAdapterEvaluateResponse> {
×
UNCOV
612
        if (this.capabilities.supportsExecuteCommand) {
×
613
            if (!this.isAtDebuggerPrompt) {
×
UNCOV
614
                throw new Error('Cannot run evaluate: debugger is not paused');
×
615
            }
616

UNCOV
617
            let stackFrame = this.getStackFrameById(frameId);
×
618
            if (!stackFrame) {
×
UNCOV
619
                throw new Error('Cannot execute command without a corresponding frame');
×
620
            }
UNCOV
621
            this.logger.log('evaluate ', { command, frameId });
×
622

UNCOV
623
            const response = await this.client.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex);
×
624
            this.logger.info('evaluate response', { command, response });
×
UNCOV
625
            if (response.data.executeSuccess) {
×
UNCOV
626
                return {
×
627
                    message: undefined,
628
                    type: 'message'
629
                };
630
            } else {
UNCOV
631
                const messages = [
×
632
                    ...response?.data?.compileErrors ?? [],
×
633
                    ...response?.data?.runtimeErrors ?? [],
×
634
                    ...response?.data?.otherErrors ?? []
×
635
                ];
UNCOV
636
                return {
×
637
                    message: messages[0] ?? 'Unknown error executing command',
×
638
                    type: 'error'
639
                };
640
            }
641
        } else {
UNCOV
642
            return {
×
643
                message: `Execute commands are not supported on debug protocol: ${this.activeProtocolVersion}, v3.0.0 or greater is required.`,
644
                type: 'error'
645
            };
646
        }
647
    }
648

649
    public async getStackTrace(threadIndex: number = this.client.primaryThread) {
×
650
        if (!this.isAtDebuggerPrompt) {
20!
UNCOV
651
            throw new Error('Cannot get stack trace: debugger is not paused');
×
652
        }
653
        return this.resolve(`stack trace for thread ${threadIndex}`, async () => {
20✔
654
            let thread = await this.getThreadByThreadId(threadIndex);
19✔
655
            let frames: StackFrame[] = [];
19✔
656
            let stackTraceData = await this.client.getStackTrace(threadIndex);
19✔
657

658
            // Non-OK error code (e.g. THREAD_DETACHED) means we can not provide the stack trace
659
            if (stackTraceData?.data?.errorCode !== undefined && stackTraceData.data.errorCode !== ErrorCode.OK) {
19✔
660
                this.logger.warn(`getStackTrace for thread ${threadIndex} failed with errorCode ${stackTraceData.data.errorCode}`);
2✔
661
                return frames;
2✔
662
            }
663
            for (let i = 0; i < (stackTraceData?.data?.entries?.length ?? 0); i++) {
17✔
664
                let frameData = stackTraceData.data.entries[i];
16✔
665
                let stackFrame: StackFrame = {
16✔
666
                    frameId: this.nextFrameId++,
667
                    // frame index is the reverse of the returned order.
668
                    frameIndex: stackTraceData.data.entries.length - i - 1,
669
                    threadIndex: threadIndex,
670
                    filePath: frameData.filePath,
671
                    lineNumber: frameData.lineNumber,
672
                    // eslint-disable-next-line no-nested-ternary
673
                    functionIdentifier: this.cleanUpFunctionName(i === 0 ? (frameData.functionName) ? frameData.functionName : thread.functionName : frameData.functionName)
32!
674
                };
675
                this.stackFramesCache[stackFrame.frameId] = stackFrame;
16✔
676
                frames.push(stackFrame);
16✔
677
            }
678
            //if the first frame is missing any data, supplement with thread information
679
            if (frames[0]) {
17✔
680
                frames[0].filePath ??= thread.filePath;
16!
681
                frames[0].lineNumber ??= thread.lineNumber;
16!
682
            }
683

684
            return frames;
17✔
685
        });
686
    }
687

688
    public getStackFrameById(frameId: number): StackFrame {
689
        return this.stackFramesCache[frameId];
2✔
690
    }
691

692
    private cleanUpFunctionName(functionName): string {
693
        return functionName.substring(functionName.lastIndexOf('@') + 1);
16✔
694
    }
695

696
    /**
697
     * Get info about the specified variable.
698
     * @param expression the expression for the specified variable (i.e. `m`, `someVar.value`, `arr[1][2].three`). If empty string/undefined is specified, all local variables are retrieved instead
699
     */
700
    private async getVariablesResponse(expression: string, frameId: number) {
701
        const logger = this.logger.createLogger('[getVariable]');
2✔
702
        logger.info('begin', { expression });
2✔
703
        if (!this.isAtDebuggerPrompt) {
2!
UNCOV
704
            throw new Error('Cannot resolve variable: debugger is not paused');
×
705
        }
706

707
        let frame = this.getStackFrameById(frameId);
2✔
708
        if (!frame) {
2!
UNCOV
709
            throw new Error('Cannot request variable without a corresponding frame');
×
710
        }
711

712
        logger.info(`Expression:`, JSON.stringify(expression));
2✔
713
        let variablePath = expression === '' ? [] : util.getVariablePath(expression);
2✔
714

715
        // Temporary workaround related to casing issues over the protocol
716
        if (this.capabilities.enableVariablesLowerCaseRetry && variablePath?.length > 0) {
2!
UNCOV
717
            variablePath[0] = variablePath[0].toLowerCase();
×
718
        }
719

720
        let response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
2✔
721

722
        if (this.capabilities.enableVariablesLowerCaseRetry && response.data.errorCode !== ErrorCode.OK) {
2!
723
            // Temporary workaround related to casing issues over the protocol
UNCOV
724
            logger.log(`Retrying expression as lower case:`, expression);
×
UNCOV
725
            variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase());
×
UNCOV
726
            response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
×
727
        }
728
        return response;
2✔
729
    }
730

731
    /**
732
     * Get the variable for the specified expression.
733
     */
734
    public async getVariable(expression: string, frameId: number): Promise<EvaluateContainer> {
735
        const response = await this.getVariablesResponse(expression, frameId);
1✔
736

737
        if (Array.isArray(response?.data?.variables)) {
1!
738
            const container = this.createEvaluateContainer(
1✔
739
                response.data.variables[0],
740
                //the name of the top container is the expression itself
741
                expression,
742
                //this is the top-level container, so there are no parent keys to this entry
743
                undefined
744
            );
745
            await insertCustomVariables(this, expression, container);
1✔
746
            container.namedVariables = container.children.length - container.indexedVariables;
1✔
747
            return container;
1✔
748
        }
749
    }
750

751
    /**
752
     * Get the list of local variables
753
     */
754
    public async getLocalVariables(frameId: number) {
755
        const response = await this.getVariablesResponse('', frameId);
1✔
756

757
        if (response?.data?.errorCode === ErrorCode.OK && Array.isArray(response?.data?.variables)) {
1!
758
            //create a top-level container to hold all the local vars
759
            const container = this.createEvaluateContainer(
1✔
760
                //dummy data
761
                {
762
                    isConst: false,
763
                    isContainer: true,
764
                    keyType: VariableType.String,
765
                    refCount: undefined,
766
                    type: VariableType.AssociativeArray,
767
                    value: undefined,
768
                    children: response.data.variables
769
                },
770
                //no name, this is a dummy container
771
                undefined,
772
                //there's no parent path
773
                undefined
774
            );
775
            container.indexedVariables = 0;
1✔
776
            container.namedVariables = container.children.length;
1✔
777
            return container;
1✔
778
        }
779
    }
780

781
    /**
782
     * Create an EvaluateContainer for the given variable. If the variable has children, those are created and attached as well
783
     * @param variable a Variable object from the debug protocol debugger
784
     * @param name the name of this variable. For example, `alpha.beta.charlie`, this value would be `charlie`. For local vars, this is the root variable name (i.e. `alpha`)
785
     * @param parentEvaluateName the string used to derive the parent, _excluding_ this variable's name (i.e. `alpha.beta` or `alpha[0]`)
786
     */
787
    private createEvaluateContainer(variable: Variable, name: string | number, parentEvaluateName: string): EvaluateContainer {
788
        let value;
789
        let variableType = variable.type;
10✔
790
        if (variable.value === null) {
10!
UNCOV
791
            value = 'roInvalid';
×
792
        } else if (variableType === VariableType.String) {
10✔
793
            value = `\"${variable.value}\"`;
2✔
794
        } else {
795
            value = variable.value;
8✔
796
        }
797

798
        if (variableType === VariableType.SubtypedObject) {
10!
799
            //subtyped objects can only have string values
UNCOV
800
            let parts = (variable.value as string).split('; ');
×
801
            // Pull the primary type from the value.
UNCOV
802
            (variableType as string) = parts[0];
×
803

804
            // Format the value to be more readable in the UI.
805
            // Example: `roSGNode; Group` = `roSGNode (Group)`
UNCOV
806
            (value as string) = `${parts[0]}(${parts[1]})`;
×
807
        } else if (variableType === VariableType.Object || variableType === VariableType.Interface) {
10!
808
            // We want the type to reflect `roAppInfo` or `roDeviceInfo` for example in the UI
809
            // so set the type to be the value from the device
UNCOV
810
            variableType = value;
×
811
        } else if (variableType === VariableType.AssociativeArray) {
10✔
812
            // We want the type to reflect `function` in the UI
813
            value = VariableType.AssociativeArray;
7✔
814
        }
815

816
        //build full evaluate name for this var. (i.e. `alpha["beta"]` + ["charlie"]` === `alpha["beta"]["charlie"]`)
817
        let evaluateName: string;
818
        if (!parentEvaluateName?.trim()) {
10✔
819
            evaluateName = name?.toString();
6✔
820
        } else if (variable.isVirtual) {
4!
UNCOV
821
            evaluateName = `${parentEvaluateName}.${name}`;
×
822
        } else if (typeof name === 'string') {
4✔
823
            evaluateName = `${parentEvaluateName}["${name}"]`;
3✔
824
        } else if (typeof name === 'number') {
1!
825
            evaluateName = `${parentEvaluateName}[${name}]`;
1✔
826
        }
827

828
        let container: EvaluateContainer = {
10✔
829
            name: name?.toString() ?? '',
60✔
830
            evaluateName: evaluateName ?? '',
30✔
831
            type: variableType ?? '',
30!
832
            value: value ?? null,
30!
833
            highLevelType: undefined,
834
            //non object/array variables don't have a key type
835
            keyType: variable.keyType as unknown as KeyType,
836
            namedVariables: 0,
837
            indexedVariables: 0,
838
            //non object/array variables still need to have an empty `children` array to help upstream logic. The `keyType` being null is how we know it doesn't actually have children
839
            children: []
840
        };
841

842
        // In preparation for adding custom variables some variables need to be marked
843
        // as keyable/container like even thought they are not on device.
844
        overrideKeyTypesForCustomVariables(this, container);
10✔
845

846
        if (container.keyType === KeyType.integer) {
10✔
847
            container.indexedVariables = variable.childCount ?? variable.children?.length ?? undefined;
2!
848
            // We do not know how many named variables there are, if any, so we will always tell the DAP client to ask for them
849
            container.namedVariables = 1;
2✔
850
        } else if (container.keyType === KeyType.string) {
8✔
851
            // container.namedVariables = variable.childCount ?? variable.children?.length ?? undefined;
852
            // Force one so DAP client always asks for all named vars
853
            container.namedVariables = 1;
5✔
854
        }
855

856
        //recursively generate children containers
857
        if ([KeyType.integer, KeyType.string].includes(container.keyType) && Array.isArray(variable.children)) {
10✔
858
            container.children = [];
4✔
859

860
            container.namedVariables = 0;
4✔
861
            container.indexedVariables = 0;
4✔
862

863
            for (let i = 0; i < variable.children.length; i++) {
4✔
864
                const childVariable = variable.children[i];
6✔
865
                if (childVariable.name === undefined) {
6!
UNCOV
866
                    container.indexedVariables++;
×
867
                }
868
                const childContainer = this.createEvaluateContainer(
6✔
869
                    childVariable,
870
                    container.keyType === KeyType.integer && !childVariable.isVirtual ? i : childVariable.name,
13✔
871
                    container.evaluateName
872
                );
873
                container.children.push(childContainer);
6✔
874
            }
875
        }
876

877
        //show virtual variables in the UI
878
        if (variable.isVirtual) {
10!
UNCOV
879
            if (!container.presentationHint) {
×
UNCOV
880
                container.presentationHint = {};
×
881
            }
UNCOV
882
            container.presentationHint.kind = 'virtual';
×
883
        }
884

885
        return container;
10✔
886
    }
887

888
    /**
889
     * Cache items by a unique key
890
     * @param expression
891
     * @param factory
892
     */
893
    private resolve<T>(key: string, factory: () => T | Thenable<T>): Promise<T> {
894
        if (this.cache[key]) {
39✔
895
            this.logger.log('return cashed response', key, this.cache[key]);
4✔
896
            return this.cache[key];
4✔
897
        }
898
        this.cache[key] = Promise.resolve<T>(factory());
35✔
899
        return this.cache[key];
35✔
900
    }
901

902
    /**
903
     * Get a list of threads. The active thread will always be first in the list.
904
     */
905
    public async getThreads() {
906
        if (!this.isAtDebuggerPrompt) {
19!
UNCOV
907
            throw new Error('Cannot get threads: debugger is not paused');
×
908
        }
909
        return this.resolve('threads', async () => {
19✔
910
            let threads: Thread[] = [];
16✔
911
            let threadsResponse: ThreadsResponse;
912
            // sometimes roku threads are stubborn and haven't stopped yet, causing our ThreadsRequest to fail with "not stopped".
913
            // A nice simple fix for this is to just send a "pause" request again, which seems to fix the issue.
914
            // we'll do this a few times just to make sure we've tried our best to get the list of threads.
915
            for (let i = 0; i < 3; i++) {
16✔
916
                threadsResponse = await this.client.threads();
16✔
917
                if (threadsResponse.data.errorCode === ErrorCode.NOT_STOPPED) {
16!
UNCOV
918
                    this.logger.log(`Threads request retrying... ${i}:\n`, threadsResponse);
×
UNCOV
919
                    threadsResponse = undefined;
×
UNCOV
920
                    const pauseResponse = await this.client.pause(true);
×
UNCOV
921
                    await util.sleep(100);
×
922
                } else {
923
                    break;
16✔
924
                }
925
            }
926
            if (!threadsResponse) {
16!
UNCOV
927
                return [];
×
928
            }
929

930
            for (let i = 0; i < (threadsResponse.data?.threads?.length ?? 0); i++) {
16!
931
                let threadInfo = threadsResponse.data.threads[i];
16✔
932
                let thread = <Thread>{
16✔
933
                    // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
934
                    // NOTE: Rely on the thead index from the threads update event.
935
                    isSelected: this.client.primaryThread === i,
936
                    // isSelected: threadInfo.isPrimary,
937
                    isDetached: threadInfo.isDetached,
938
                    filePath: threadInfo.filePath,
939
                    functionName: threadInfo.functionName,
940
                    lineNumber: threadInfo.lineNumber, //threadInfo.lineNumber is 1-based. Thread requires 1-based line numbers
941
                    lineContents: threadInfo.codeSnippet,
942
                    threadId: i,
943
                    osThreadId: threadInfo.osThreadId,
944
                    name: threadInfo.name,
945
                    type: threadInfo.type
946
                };
947
                threads.push(thread);
16✔
948
            }
949
            //make sure the selected thread is at the top
950
            threads.sort((a, b) => {
16✔
951
                return a.isSelected ? -1 : 1;
×
952
            });
953

954
            return threads;
16✔
955
        });
956
    }
957

958
    private async getThreadByThreadId(threadId: number) {
959
        let threads = await this.getThreads();
19✔
960
        for (let thread of threads) {
19✔
961
            if (thread.threadId === threadId) {
19✔
962
                return thread;
18✔
963
            }
964
        }
965
    }
966

967
    public removeAllListeners() {
968
        if (this.emitter) {
×
969
            this.emitter.removeAllListeners();
×
970
        }
971
    }
972

973
    /**
974
     * Indicates whether this class had `.destroy()` called at least once. Mostly used for checking externally to see if
975
     * the whole debug session has been terminated or is in a bad state.
976
     */
977
    public isDestroyed = false;
21✔
978
    /**
979
     * Disconnect from the telnet session and unset all objects
980
     */
981
    public async destroy() {
982
        this.isDestroyed = true;
×
983

984
        // destroy the debug client if it's defined
UNCOV
985
        if (this.client) {
×
UNCOV
986
            try {
×
UNCOV
987
                await this.client.destroy();
×
988
            } catch (e) {
UNCOV
989
                this.logger.error(e);
×
990
            }
991
        }
992

UNCOV
993
        try {
×
994
            let shutdownTimeMax = this.options?.shutdownTimeout ?? 10_000;
×
995
            await this.destroyCompileClient(shutdownTimeMax);
×
996
        } catch (e) {
UNCOV
997
            this.logger.error(e);
×
998
        }
999

UNCOV
1000
        this.cache = undefined;
×
UNCOV
1001
        this.removeAllListeners();
×
UNCOV
1002
        this.emitter = undefined;
×
1003
    }
1004

1005
    /**
1006
     * Promise that is resolved when the compile client socket is closed
1007
     */
1008
    private compileClientClosed = defer<void>();
21✔
1009
    private isDestroyingCompileClient = false;
21✔
1010

1011
    private async destroyCompileClient(timeout: number) {
UNCOV
1012
        if (this.compileClient && !this.isDestroyingCompileClient) {
×
UNCOV
1013
            this.isDestroyingCompileClient = true;
×
UNCOV
1014
            this.compileClient?.end();
×
1015

1016
            //wait for the compileClient to be closed
UNCOV
1017
            await Promise.race([
×
1018
                this.compileClientClosed.promise,
1019
                util.sleep(timeout)
1020
            ]);
1021

UNCOV
1022
            this.logger.log('[destroy] compileClient is: ', this.compileClientClosed.isResolved ? 'closed' : 'not closed');
×
1023

1024
            //destroy the compileClient
UNCOV
1025
            this.compileClient?.removeAllListeners();
×
UNCOV
1026
            this.compileClient?.destroy();
×
1027
            this.compileClient = undefined;
×
UNCOV
1028
            this.isDestroyingCompileClient = false;
×
1029
        }
1030
    }
1031

1032
    /**
1033
     * Passes the log level down to the RendezvousTracker and ChanperfTracker
1034
     * @param outputLevel the consoleOutput from the launch config
1035
     */
1036
    public setConsoleOutput(outputLevel: string) {
UNCOV
1037
        this.chanperfTracker.setConsoleOutput(outputLevel);
×
UNCOV
1038
        this.rendezvousTracker.setConsoleOutput(outputLevel);
×
1039
    }
1040

1041
    /**
1042
     * Sends a call to the RendezvousTracker to clear the current rendezvous history
1043
     */
1044
    public clearRendezvousHistory() {
1045
        this.rendezvousTracker.clearHistory();
×
1046
    }
1047

1048
    /**
1049
     * Sends a call to the ChanperfTracker to clear the current chanperf history
1050
     */
1051
    public clearChanperfHistory() {
UNCOV
1052
        this.chanperfTracker.clearHistory();
×
1053
    }
1054

1055
    /**
1056
     * The most recently requested exception breakpoint filters. Stored so we can replay them
1057
     * to the debug protocol client once it connects (the session can send these before the
1058
     * device has launched and the client has finished its handshake).
1059
     */
1060
    private pendingExceptionBreakpointFilters: ExceptionBreakpoint[] | undefined;
1061

1062
    public async setExceptionBreakpoints(filters: ExceptionBreakpoint[]) {
UNCOV
1063
        if (!this.capabilities.supportsExceptionBreakpoints) {
×
UNCOV
1064
            return undefined;
×
1065
        }
1066
        //if the client isn't connected yet, queue the filters for replay on connect
UNCOV
1067
        if (!this.connected) {
×
UNCOV
1068
            this.pendingExceptionBreakpointFilters = filters;
×
UNCOV
1069
            return undefined;
×
1070
        }
UNCOV
1071
        return this.client.setExceptionBreakpoints(filters);
×
1072
    }
1073

1074
    private syncBreakpointsPromise = Promise.resolve();
21✔
1075
    public async syncBreakpoints() {
1076
        this.logger.log('syncBreakpoints()');
42✔
1077
        //wait for the previous sync to finish
1078
        this.syncBreakpointsPromise = this.syncBreakpointsPromise
42✔
1079
            //ignore any errors
1080
            .catch(() => { })
1081
            //run the next sync
1082
            .then(() => this._syncBreakpoints());
42✔
1083

1084
        //return the new promise, which will resolve once our latest `syncBreakpoints()` call is finished
1085
        return this.syncBreakpointsPromise;
42✔
1086
    }
1087

1088
    public async _syncBreakpoints() {
1089
        //we need to actually be connected to the device before we can push breakpoints. We'll get
1090
        //called again once the debug protocol client has connected.
1091
        if (!this.connected) {
32✔
1092
            this.logger.info('Cannot sync breakpoints because the debug protocol client has not connected yet');
2✔
1093
            return;
2✔
1094
        }
1095
        //we can't send breakpoints unless we're stopped (or in a protocol version that supports sending them while running).
1096
        //So...if we're not stopped, quit now. (we'll get called again when the stop event happens)
1097
        if (!this.capabilities.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) {
30✔
1098
            this.logger.info('Cannot sync breakpoints because the debugger', this.capabilities.supportsBreakpointRegistrationWhileRunning ? 'does not support sending breakpoints while running' : 'is not paused');
16!
1099
            return;
16✔
1100
        }
1101

1102
        //compute breakpoint changes since last sync
1103
        const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects());
14✔
1104
        this.logger.log('Syncing breakpoints', diff);
14✔
1105

1106
        if (diff.added.length === 0 && diff.removed.length === 0) {
14✔
1107
            this.logger.debug('No breakpoints to sync');
2✔
1108
            return;
2✔
1109
        }
1110

1111
        //getDiff above can yield to other microtasks. If the protocol client closed while we were
1112
        //awaiting (mid-sync TOCTOU), bail out before dereferencing this.client. The app-exit handler
1113
        //resets the breakpoint baseline so the next reconnect re-pushes any pending changes.
1114
        //See https://github.com/rokucommunity/vscode-brightscript-language/issues/811
1115
        if (!this.client) {
12✔
1116
            this.logger.info('Skipping breakpoint sync because the protocol client closed mid-sync');
2✔
1117
            return;
2✔
1118
        }
1119

1120
        // REMOVE breakpoints (delete these breakpoints from the device)
1121
        if (diff.removed.length > 0) {
10✔
1122
            const response = await this.client.removeBreakpoints(
3✔
1123
                //TODO handle retrying to remove breakpoints that don't have deviceIds yet but might get one in the future
1124
                diff.removed.map(x => x.deviceId).filter(x => typeof x === 'number')
5✔
1125
            );
1126

1127
            if (response.data?.errorCode === ErrorCode.NOT_STOPPED) {
3!
1128
                this.breakpointManager.failedDeletions.push(...diff.removed);
1✔
1129
            }
1130
        }
1131

1132
        if (diff.added.length > 0) {
10✔
1133
            //the removeBreakpoints await above can also yield; re-check before attempting the add
1134
            if (!this.client) {
8!
UNCOV
1135
                this.logger.info('Skipping breakpoint add because the protocol client closed mid-sync');
×
UNCOV
1136
                return;
×
1137
            }
1138
            const breakpointsToSendToDevice = diff.added.map(breakpoint => {
8✔
1139
                const hitCount = parseInt(breakpoint.hitCondition);
11✔
1140
                return {
11✔
1141
                    filePath: breakpoint.pkgPath,
1142
                    lineNumber: breakpoint.line,
1143
                    hitCount: !isNaN(hitCount) ? hitCount : undefined,
11!
1144
                    conditionalExpression: breakpoint.condition,
1145
                    srcHash: breakpoint.srcHash,
1146
                    destHash: breakpoint.destHash,
1147
                    componentLibraryName: breakpoint.componentLibraryName
1148
                };
1149
            });
1150

1151
            //split the list into conditional and non-conditional breakpoints.
1152
            //(TODO we can eliminate this splitting logic once the conditional breakpoints "continue" bug in protocol is fixed)
1153
            const standardBreakpoints: typeof breakpointsToSendToDevice = [];
8✔
1154
            const conditionalBreakpoints: typeof breakpointsToSendToDevice = [];
8✔
1155
            for (const breakpoint of breakpointsToSendToDevice) {
8✔
1156
                if (breakpoint?.conditionalExpression?.trim()) {
11!
1157
                    conditionalBreakpoints.push(breakpoint);
1✔
1158
                } else {
1159
                    standardBreakpoints.push(breakpoint);
10✔
1160
                }
1161
            }
1162
            for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) {
8✔
1163
                const response = await this.client.addBreakpoints(breakpoints);
16✔
1164

1165
                //if the response was successful, and we have the correct number of breakpoints in the response
1166
                if (response.data.errorCode === ErrorCode.OK && response?.data?.breakpoints?.length === breakpoints.length) {
16!
1167
                    for (let i = 0; i < (response?.data?.breakpoints?.length ?? 0); i++) {
14!
1168
                        const deviceBreakpoint = response.data.breakpoints[i];
9✔
1169

1170
                        if (typeof deviceBreakpoint?.id === 'number') {
9!
1171
                            //sync this breakpoint's deviceId with the roku-assigned breakpoint ID
1172
                            this.breakpointManager.setBreakpointDeviceId(
9✔
1173
                                breakpoints[i].srcHash,
1174
                                breakpoints[i].destHash,
1175
                                deviceBreakpoint.id
1176
                            );
1177
                        }
1178

1179
                        //this breakpoint had an issue. remove it from the client
1180
                        if (deviceBreakpoint.errorCode !== ErrorCode.OK) {
9✔
1181
                            this.breakpointManager.deleteBreakpoint(breakpoints[i].srcHash);
1✔
1182
                        }
1183
                    }
1184
                    //the entire response was bad. delete these breakpoints from the client
1185
                } else {
1186
                    this.breakpointManager.deleteBreakpoints(
2✔
1187
                        breakpoints.map(x => x.srcHash)
2✔
1188
                    );
1189
                }
1190
            }
1191
        }
1192
    }
1193

1194
    public isTelnetAdapter(): this is TelnetAdapter {
UNCOV
1195
        return false;
×
1196
    }
1197

1198
    public isDebugProtocolAdapter(): this is DebugProtocolAdapter {
UNCOV
1199
        return true;
×
1200
    }
1201
}
1202

1203
export interface StackFrame {
1204
    frameId: number;
1205
    frameIndex: number;
1206
    threadIndex: number;
1207
    filePath: string;
1208
    lineNumber: number;
1209
    functionIdentifier: string;
1210
}
1211

1212
export enum EventName {
2✔
1213
    suspend = 'suspend'
2✔
1214
}
1215

1216
export interface EvaluateContainer {
1217
    name: string;
1218
    evaluateName: string;
1219
    type: string;
1220
    value?: any;
1221
    keyType?: KeyType;
1222
    namedVariables?: number;
1223
    indexedVariables?: number;
1224
    highLevelType?: HighLevelType;
1225
    children: EvaluateContainer[];
1226
    isCustom?: boolean;
1227
    evaluateNow?: boolean;
1228
    presentationHint?: DebugProtocol.VariablePresentationHint;
1229
}
1230

1231
export enum KeyType {
2✔
1232
    string = 'String',
2✔
1233
    integer = 'Integer',
2✔
1234
    legacy = 'Legacy'
2✔
1235
}
1236

1237
export interface Thread {
1238
    isSelected: boolean;
1239
    isDetached?: boolean;
1240
    /**
1241
     * The 1-based line number for the thread
1242
     */
1243
    lineNumber: number;
1244
    filePath: string;
1245
    functionName: string;
1246
    lineContents: string;
1247
    threadId: number;
1248
    osThreadId?: string;
1249
    name?: string;
1250
    type?: string;
1251
}
1252

1253
interface BrightScriptRuntimeError {
1254
    message: string;
1255
    errorCode: string;
1256
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc