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

rokucommunity / roku-debug / 26512433608

27 May 2026 12:57PM UTC coverage: 70.204% (+0.1%) from 70.065%
26512433608

Pull #357

github

web-flow
Merge 38028c5f3 into 91807065f
Pull Request #357: Reset connected flag on close so _syncBreakpoints doesn't crash on a missing client

3341 of 5050 branches covered (66.16%)

Branch coverage included in aggregate %.

5 of 8 new or added lines in 1 file covered. (62.5%)

5558 of 7626 relevant lines covered (72.88%)

37.28 hits per line

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

59.56
/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,
20✔
34
        private projectManager: ProjectManager,
20✔
35
        private breakpointManager: BreakpointManager,
20✔
36
        private rendezvousTracker: RendezvousTracker,
20✔
37
        private deviceInfo: DeviceInfo
20✔
38
    ) {
39
        util.normalizeAdapterOptions(this.options);
20✔
40
        this.emitter = new EventEmitter();
20✔
41
        this.chanperfTracker = new ChanperfTracker();
20✔
42
        this.compileErrorProcessor = new CompileErrorProcessor();
20✔
43
        this.connected = false;
20✔
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);
20!
47

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

54
    private logger = logger.createLogger(`[padapter]`);
20✔
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;
48✔
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
    private emitter: EventEmitter;
79
    private chanperfTracker: ChanperfTracker;
80
    private client: DebugProtocolClient;
81
    private nextFrameId = 1;
20✔
82

83
    private stackFramesCache: Record<number, StackFrame> = {};
20✔
84
    private cache = {};
20✔
85

86
    /**
87
     * Get the version of the protocol for the Roku device we're currently connected to.
88
     */
89
    public get activeProtocolVersion() {
90
        return this.client?.protocolVersion;
×
91
    }
92

93
    /**
94
     * Subscribe to an event exactly once
95
     * @param eventName
96
     */
97
    public once(eventName: 'cannot-continue'): Promise<void>;
98
    public once(eventname: 'chanperf'): Promise<ChanperfData>;
99
    public once(eventName: 'close'): Promise<void>;
100
    public once(eventName: 'app-exit'): Promise<void>;
101
    public once(eventName: 'app-ready'): Promise<void>;
102
    public once(eventName: 'diagnostics'): Promise<BSDebugDiagnostic>;
103
    public once(eventName: 'connected'): Promise<boolean>;
104
    public once(eventname: 'console-output'): Promise<string>; // TODO: might be able to remove this at some point
105
    public once(eventname: 'protocol-version'): Promise<ProtocolVersionDetails>;
106
    public once(eventname: 'rendezvous'): Promise<RendezvousHistory>;
107
    public once(eventName: 'runtime-error'): Promise<BrightScriptRuntimeError>;
108
    public once(eventName: 'suspend'): Promise<void>;
109
    public once(eventName: 'start'): Promise<void>;
110
    public once(eventname: 'device-unresponsive'): Promise<void>;
111
    public once(eventname: 'unhandled-console-output'): Promise<string>;
112
    public once(eventName: string) {
113
        return new Promise((resolve) => {
15✔
114
            const disconnect = this.on(eventName as Parameters<DebugProtocolAdapter['on']>[0], (...args) => {
15✔
115
                disconnect();
15✔
116
                resolve(...args);
15✔
117
            });
118
        });
119
    }
120

121
    /**
122
     * Subscribe to various events
123
     * @param eventName
124
     * @param handler
125
     */
126
    public on(eventName: 'breakpoints-verified', handler: (event: BreakpointsVerifiedEvent) => any);
127
    public on(eventName: 'cannot-continue', handler: () => any);
128
    public on(eventname: 'chanperf', handler: (output: ChanperfData) => any);
129
    public on(eventName: 'close', handler: () => any);
130
    public on(eventName: 'app-exit', handler: () => any);
131
    public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => any);
132
    public on(eventName: 'launch-status', handler: (message: string) => any);
133
    public on(eventName: 'connected', handler: (params: boolean) => any);
134
    public on(eventname: 'console-output', handler: (output: string) => any); // TODO: might be able to remove this at some point.
135
    public on(eventname: 'protocol-version', handler: (output: ProtocolVersionDetails) => any);
136
    public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => any);
137
    public on(eventName: 'suspend', handler: () => any);
138
    public on(eventName: 'start', handler: () => any);
139
    public on(eventName: 'waiting-for-debugger', handler: () => any);
140
    public on(eventName: 'device-unresponsive', handler: (data: { lastCommand: string }) => any);
141
    public on(eventname: 'unhandled-console-output', handler: (output: string) => any);
142
    public on(eventName: string, handler: (payload: any) => any) {
143
        this.emitter?.on(eventName, handler);
30!
144
        return () => {
30✔
145
            this.emitter?.removeListener(eventName, handler);
15!
146
        };
147
    }
148

149
    private emit(eventName: 'suspend');
150
    private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent);
151
    private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]);
152
    private emit(eventName: 'launch-status', message: string);
153
    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?);
154
    private emit(eventName: string, data?) {
155
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
156
        setTimeout(() => {
67✔
157
            //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists
158
            if (this.emitter) {
67!
159
                this.emitter.emit(eventName, data);
67✔
160
            }
161
        }, 0);
162
    }
163

164
    /**
165
     * Does the current client support exception breakpoints? Resolved via the live client's
166
     * capabilities when connected, otherwise the device-info-seeded fallback.
167
     */
168
    public get supportsExceptionBreakpoints(): boolean {
169
        return this.capabilities.supportsExceptionBreakpoints;
×
170
    }
171

172
    /**
173
     * Does the current client support conditional breakpoints? Same fallback semantics as
174
     * `supportsExceptionBreakpoints`.
175
     */
176
    public get supportsConditionalBreakpoints(): boolean {
177
        return this.capabilities.supportsConditionalBreakpoints;
×
178
    }
179

180
    /**
181
     * Does the current client support hit-count breakpoints?
182
     */
183
    public get supportsHitConditionalBreakpoints(): boolean {
184
        return this.capabilities.supportsHitConditionalBreakpoints;
×
185
    }
186

187
    /**
188
     * The debugger needs to tell us when to be active (i.e. when the package was deployed)
189
     */
190
    public isActivated = false;
20✔
191

192
    /**
193
     * This will be set to true When the roku emits the [scrpt.ctx.run.enter] text,
194
     * which indicates that the app is running on the Roku
195
     */
196
    public isAppRunning = false;
20✔
197

198
    public activate() {
199
        this.isActivated = true;
×
200
        this.handleStartupIfReady();
×
201
    }
202

203
    public async sendErrors() {
204
        await this.compileErrorProcessor.sendErrors();
×
205
    }
206

207
    private handleStartupIfReady() {
208
        if (this.isActivated && this.isAppRunning) {
15!
209
            this.emit('start');
×
210

211
            //if we are already sitting at a debugger prompt, we need to emit the first suspend event.
212
            //If not, then there are probably still messages being received, so let the normal handler
213
            //emit the suspend event when it's ready
214
            if (this.isAtDebuggerPrompt === true) {
×
215
                this.emit('suspend');
×
216
            }
217
        }
218
    }
219

220
    /**
221
     * Wait until the client has stopped sending messages. This is used mainly during .connect so we can ignore all old messages from the server
222
     * @param client
223
     * @param maxWaitMilliseconds
224
     */
225
    private settleCompileClient(client: Socket, maxWaitMilliseconds = 400) {
×
226
        return new Promise<string>((resolve) => {
×
227
            let timeoutStarted = false;
×
228
            let callCount = -1;
×
229
            let logs = '';
×
230

231
            function handler(buffer) {
232
                callCount++;
×
233
                logs += buffer.toString();
×
234
                let myCallCount = callCount;
×
235
                timeoutStarted = true;
×
236
                setTimeout(() => {
×
237
                    if (myCallCount === callCount) {
×
238
                        // stop listening for data events
239
                        client.removeListener('data', handler);
×
240
                        resolve(logs);
×
241
                    }
242
                }, maxWaitMilliseconds);
243
            }
244

245
            const startTimeout = () => {
×
246
                if (timeoutStarted === false) {
×
247
                    handler(Buffer.from(''));
×
248
                }
249
            };
250

251
            // watch for data events
252
            client.on('data', handler);
×
253

254
            // watch for different connection related events to start the timeout logic
255
            client.on('ready', startTimeout);
×
256
            client.on('end', startTimeout);
×
257
            client.on('closed', startTimeout);
×
258
        });
259
    }
260

261
    public get isAtDebuggerPrompt() {
262
        return this.client?.isStopped ?? false;
61!
263
    }
264

265
    private firstConnectDeferred = defer<void>();
20✔
266

267
    /**
268
     * Resolves when the first connection to the client is established
269
     */
270
    public onReady() {
271
        return this.firstConnectDeferred.promise;
×
272
    }
273

274
    /**
275
     * Connect to the telnet session. This should be called before the channel is launched.
276
     */
277
    public async connect(): Promise<void> {
278
        //Start processing telnet output to look for compile errors or the debugger prompt
279
        await this.processTelnetOutput();
15✔
280

281
        this.on('waiting-for-debugger', async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
15✔
282
            await this.createDebugProtocolClient();
×
283

284
            //if this is the first time we are connecting, resolve the promise.
285
            //(future events fire for "reconnect" situations, we don't need to resolve again for those)
286
            if (!this.firstConnectDeferred.isCompleted) {
×
287
                this.firstConnectDeferred.resolve();
×
288
            }
289
        });
290
    }
291

292
    public async createDebugProtocolClient() {
293
        let deferred = defer();
15✔
294
        if (this.client) {
15!
295
            await Promise.race([
×
296
                util.sleep(2000),
297
                await this.client.destroy()
298
            ]);
299
            this.client = undefined;
×
300
            //keep `connected` in sync with `client` so the _syncBreakpoints entry guard
301
            //(and similar checks elsewhere) reflects the actual state. Restored to true below
302
            //once the new client finishes connecting.
NEW
303
            this.connected = false;
×
304
        }
305
        this.client = new DebugProtocolClient(this.options);
15✔
306
        try {
15✔
307
            // Emit IO from the debugger.
308
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
309
            this.client.on('io-output', async (responseText) => {
15✔
310
                if (typeof responseText === 'string') {
×
311
                    responseText = this.chanperfTracker.processLog(responseText);
×
312
                    responseText = await this.rendezvousTracker.processLog(responseText);
×
313
                    this.emit('unhandled-console-output', responseText);
×
314
                    this.emit('console-output', responseText);
×
315
                }
316
            });
317

318
            // Emit IO from the debugger.
319
            this.client.on('protocol-version', (data: ProtocolVersionDetails) => {
15✔
320
                if (data.errorCode === PROTOCOL_ERROR_CODES.SUPPORTED) {
15!
321
                    this.emit('console-output', data.message);
15✔
322
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_TESTED) {
×
323
                    this.emit('unhandled-console-output', data.message);
×
324
                    this.emit('console-output', data.message);
×
325
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_SUPPORTED) {
×
326
                    this.emit('unhandled-console-output', data.message);
×
327
                    this.emit('console-output', data.message);
×
328
                }
329

330
            });
331

332
            // Listen for the close event
333
            this.client.on('close', () => {
15✔
334
                this.emit('close');
1✔
335
                this.beginAppExit();
1✔
336
                void this.client?.destroy();
1!
337
                this.client = undefined;
1✔
338
                //the protocol client is gone — keep `connected` in sync so any subsequent
339
                //syncBreakpoints / setExceptionBreakpoints calls during the close→app-exit
340
                //window early-return instead of dereferencing the undefined client.
341
                //See https://github.com/rokucommunity/vscode-brightscript-language/issues/811
342
                this.connected = false;
1✔
343
            });
344

345
            // Listen for the app exit event
346
            this.client.on('app-exit', () => {
15✔
347
                this.emit('app-exit');
×
348
                void this.client?.destroy();
×
349
                this.client = undefined;
×
350
            });
351

352
            this.client.on('suspend', (data) => {
15✔
353
                this.clearCache();
15✔
354
                this.emit('suspend');
15✔
355
            });
356

357
            this.client.on('runtime-error', (data) => {
15✔
358
                console.debug('hasRuntimeError!!', data);
×
359
                this.emit('runtime-error', <BrightScriptRuntimeError>{
×
360
                    message: data.data.stopReasonDetail,
361
                    errorCode: data.data.stopReason
362
                });
363
            });
364

365
            this.client.on('cannot-continue', () => {
15✔
366
                this.emit('cannot-continue');
×
367
            });
368

369
            //handle when the device verifies breakpoints
370
            this.client.on('breakpoints-verified', (event) => {
15✔
371
                let unverifiableDeviceIds = [] as number[];
6✔
372

373
                //mark the breakpoints as verified
374
                for (let breakpoint of event?.breakpoints ?? []) {
6!
375
                    const success = this.breakpointManager.verifyBreakpoint(breakpoint.id, true);
5✔
376
                    if (!success) {
5✔
377
                        unverifiableDeviceIds.push(breakpoint.id);
3✔
378
                    }
379
                }
380
                //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
381
                if (unverifiableDeviceIds.length > 0) {
6✔
382
                    this.logger.warn('Could not find breakpoints to verify. Removing from device:', { deviceBreakpointIds: unverifiableDeviceIds });
3✔
383
                    void this.client.removeBreakpoints(unverifiableDeviceIds);
3✔
384
                }
385
                this.emit('breakpoints-verified', event);
5✔
386
            });
387

388
            this.client.on('compile-error', (update) => {
15✔
389
                let diagnostics: BSDebugDiagnostic[] = [];
×
390
                diagnostics.push({
×
391
                    path: update.data.filePath,
392
                    range: bscUtil.createRange(update.data.lineNumber - 1, 0, update.data.lineNumber - 1, 999),
393
                    message: update.data.errorMessage,
394
                    severity: DiagnosticSeverity.Error,
395
                    code: undefined
396
                });
397
                this.emit('diagnostics', diagnostics);
×
398
            });
399

400
            await this.client.connect();
15✔
401

402
            this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected });
15✔
403
            this.connected = true;
15✔
404
            this.isAppRunning = true;
15✔
405
            this.handleStartupIfReady();
15✔
406
            this.emit('connected', this.connected);
15✔
407
            this.emit('app-ready');
15✔
408
            //flush any breakpoints that were queued while we were waiting for the client to connect.
409
            //setBreakpointsRequest is called by VS Code before the channel is uploaded, so the initial
410
            //sync bails out (no client yet); this re-sync pushes those queued breakpoints to the device.
411
            void this.syncBreakpoints();
15✔
412
            //also replay any queued exception breakpoint filters
413
            if (this.pendingExceptionBreakpointFilters) {
15!
414
                const queuedFilters = this.pendingExceptionBreakpointFilters;
×
415
                this.pendingExceptionBreakpointFilters = undefined;
×
416
                void this.client.setExceptionBreakpoints(queuedFilters);
×
417
            }
418

419
            //the adapter is connected and running smoothly. resolve the promise
420
            deferred.resolve();
15✔
421
        } catch (e) {
422
            deferred.reject(e);
×
423
        }
424
        return deferred.promise;
15✔
425
    }
426

427
    private beginAppExit() {
428
        this.compileErrorProcessor.compileErrorTimer = setTimeout(() => {
1✔
429
            this.isAppRunning = false;
1✔
430
            this.emit('app-exit');
1✔
431
        }, 200);
432
    }
433

434
    /**
435
     * Determines if the current version of the debug protocol supports emitting compile error updates.
436
     */
437
    public get supportsCompileErrorReporting() {
438
        return this.capabilities.supportsCompileErrorReporting;
×
439
    }
440

441
    /**
442
     * Indicate if virtual variables should be auto resolved when they are encountered.
443
     */
444
    public get autoResolveVirtualVariables() {
445
        return this.options.autoResolveVirtualVariables;
1✔
446
    }
447

448
    private processingTelnetOutput = false;
20✔
449
    public async processTelnetOutput() {
450
        if (this.processingTelnetOutput) {
1!
451
            return;
×
452
        }
453
        this.processingTelnetOutput = true;
1✔
454

455
        let deferred = defer();
1✔
456
        try {
1✔
457
            this.compileClient = new Socket({ allowHalfOpen: false });
1✔
458
            util.registerSocketLogging(this.compileClient, this.logger, 'CompileClient');
1✔
459

460
            this.compileErrorProcessor.on('diagnostics', (errors) => {
1✔
461
                this.compileClient.end();
×
462
                this.emit('diagnostics', errors);
×
463
            });
464

465
            this.compileErrorProcessor.on('launch-status', (message) => {
1✔
466
                this.emit('launch-status', message);
×
467
            });
468

469
            //if the connection fails, reject the connect promise.
470
            //Use tryReject (not reject) because this handler persists for the socket's lifetime.
471
            //After a successful connection the deferred is already resolved, so a post-connection
472
            //socket error (e.g. ECONNRESET on device disconnect) must not crash the process.
473
            this.compileClient.on('error', (err) => {
1✔
474
                deferred.tryReject(new Error(`Error with connection to: ${this.options.host}:${this.options.brightScriptConsolePort} \n\n ${err.message} `));
1✔
475
            });
476
            this.logger.info('Connecting via telnet to gather compile info', { host: this.options.host, port: this.options.brightScriptConsolePort });
1✔
477
            this.compileClient.connect(this.options.brightScriptConsolePort, this.options.host, () => {
1✔
478
                this.logger.log(`CONNECTED via telnet to gather compile info`, { host: this.options.host, port: this.options.brightScriptConsolePort });
×
479
            });
480

481
            this.logger.debug('Waiting for the compile client to settle');
1✔
482
            const settledLogs = await this.settleCompileClient(this.compileClient);
1✔
483
            this.logger.debug('Compile client has settled');
1✔
484
            this.logger.trace('Settled logs:', settledLogs);
1✔
485

486
            if (settledLogs.trim().startsWith('Console connection is already in use.')) {
1!
487
                throw new SocketConnectionInUseError(`Telnet connection ${this.options.host}:${this.options.brightScriptConsolePort} already is use`, {
×
488
                    port: this.options.brightScriptConsolePort,
489
                    host: this.options.host
490
                });
491
            }
492

493
            let lastPartialLine = '';
1✔
494
            this.compileClient.on('data', (buffer) => {
1✔
495
                let responseText = buffer.toString();
×
496
                this.logger.info('CompileClient received data', { responseText });
×
497

498
                let logResult = util.handleLogFragments(lastPartialLine, buffer.toString());
×
499

500
                // Save any remaining partial line for the next event
501
                lastPartialLine = logResult.remaining;
×
502
                if (logResult.completed) {
×
503
                    // Emit the completed io string.
504
                    this.findWaitForDebuggerPrompt(logResult.completed);
×
505
                    this.compileErrorProcessor.processUnhandledLines(logResult.completed);
×
506
                    this.logger.debug('CompileClient data:', logResult.completed);
×
507
                    this.emit('unhandled-console-output', logResult.completed);
×
508
                } else {
509
                    this.logger.debug('CompileClient buffer was split:', lastPartialLine);
×
510
                }
511
            });
512

513
            this.compileClient.on('close', () => {
1✔
514
                this.logger.log('compileClient socket closed');
1✔
515
                this.compileClientClosed.tryResolve();
1✔
516
            });
517

518
            // connected to telnet. resolve the promise
519
            deferred.resolve();
1✔
520
        } catch (e) {
521
            deferred.reject(e);
×
522
        }
523
        return deferred.promise;
1✔
524
    }
525

526
    private findWaitForDebuggerPrompt(responseText: string) {
527
        let lines = responseText.split(/\r?\n/g);
×
528
        for (const line of lines) {
×
529
            if (/Waiting for debugger on \d+\.\d+\.\d+\.\d+:8081/g.exec(line)) {
×
530
                this.emit('waiting-for-debugger');
×
531
            }
532
        }
533
    }
534

535
    /**
536
     * Send command to step over
537
     */
538
    public async stepOver(threadId: number) {
539
        this.clearCache();
×
540
        return this.client.stepOver(threadId);
×
541
    }
542

543
    public async stepInto(threadId: number) {
544
        this.clearCache();
×
545
        return this.client.stepIn(threadId);
×
546
    }
547

548
    public async stepOut(threadId: number) {
549
        this.clearCache();
×
550
        return this.client.stepOut(threadId);
×
551
    }
552

553
    /**
554
     * Tell the brightscript program to continue (i.e. resume program)
555
     */
556
    public async continue() {
557
        this.clearCache();
×
558
        return this.client.continue();
×
559
    }
560

561
    /**
562
     * Tell the brightscript program to pause (fall into debug mode)
563
     */
564
    public async pause() {
565
        this.clearCache();
×
566
        //send the kill signal, which breaks into debugger mode
567
        return this.client.pause();
×
568
    }
569

570
    /**
571
     * Clears the state, which means that everything will be retrieved fresh next time it is requested
572
     */
573
    public clearCache() {
574
        this.cache = {};
15✔
575
        this.stackFramesCache = {};
15✔
576
    }
577

578
    /**
579
     * Execute a command directly on the roku. Returns the output of the command
580
     * @param command
581
     * @returns the output of the command (if possible)
582
     */
583
    public async evaluate(command: string, frameId: number = this.client.primaryThread): Promise<RokuAdapterEvaluateResponse> {
×
584
        if (this.capabilities.supportsExecuteCommand) {
×
585
            if (!this.isAtDebuggerPrompt) {
×
586
                throw new Error('Cannot run evaluate: debugger is not paused');
×
587
            }
588

589
            let stackFrame = this.getStackFrameById(frameId);
×
590
            if (!stackFrame) {
×
591
                throw new Error('Cannot execute command without a corresponding frame');
×
592
            }
593
            this.logger.log('evaluate ', { command, frameId });
×
594

595
            const response = await this.client.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex);
×
596
            this.logger.info('evaluate response', { command, response });
×
597
            if (response.data.executeSuccess) {
×
598
                return {
×
599
                    message: undefined,
600
                    type: 'message'
601
                };
602
            } else {
603
                const messages = [
×
604
                    ...response?.data?.compileErrors ?? [],
×
605
                    ...response?.data?.runtimeErrors ?? [],
×
606
                    ...response?.data?.otherErrors ?? []
×
607
                ];
608
                return {
×
609
                    message: messages[0] ?? 'Unknown error executing command',
×
610
                    type: 'error'
611
                };
612
            }
613
        } else {
614
            return {
×
615
                message: `Execute commands are not supported on debug protocol: ${this.activeProtocolVersion}, v3.0.0 or greater is required.`,
616
                type: 'error'
617
            };
618
        }
619
    }
620

621
    public async getStackTrace(threadIndex: number = this.client.primaryThread) {
×
622
        if (!this.isAtDebuggerPrompt) {
19!
623
            throw new Error('Cannot get stack trace: debugger is not paused');
×
624
        }
625
        return this.resolve(`stack trace for thread ${threadIndex}`, async () => {
19✔
626
            let thread = await this.getThreadByThreadId(threadIndex);
18✔
627
            let frames: StackFrame[] = [];
18✔
628
            let stackTraceData = await this.client.getStackTrace(threadIndex);
18✔
629

630
            // Non-OK error code (e.g. THREAD_DETACHED) means we can not provide the stack trace
631
            if (stackTraceData?.data?.errorCode !== undefined && stackTraceData.data.errorCode !== ErrorCode.OK) {
18✔
632
                this.logger.warn(`getStackTrace for thread ${threadIndex} failed with errorCode ${stackTraceData.data.errorCode}`);
2✔
633
                return frames;
2✔
634
            }
635
            for (let i = 0; i < (stackTraceData?.data?.entries?.length ?? 0); i++) {
16✔
636
                let frameData = stackTraceData.data.entries[i];
15✔
637
                let stackFrame: StackFrame = {
15✔
638
                    frameId: this.nextFrameId++,
639
                    // frame index is the reverse of the returned order.
640
                    frameIndex: stackTraceData.data.entries.length - i - 1,
641
                    threadIndex: threadIndex,
642
                    filePath: frameData.filePath,
643
                    lineNumber: frameData.lineNumber,
644
                    // eslint-disable-next-line no-nested-ternary
645
                    functionIdentifier: this.cleanUpFunctionName(i === 0 ? (frameData.functionName) ? frameData.functionName : thread.functionName : frameData.functionName)
30!
646
                };
647
                this.stackFramesCache[stackFrame.frameId] = stackFrame;
15✔
648
                frames.push(stackFrame);
15✔
649
            }
650
            //if the first frame is missing any data, supplement with thread information
651
            if (frames[0]) {
16✔
652
                frames[0].filePath ??= thread.filePath;
15!
653
                frames[0].lineNumber ??= thread.lineNumber;
15!
654
            }
655

656
            return frames;
16✔
657
        });
658
    }
659

660
    public getStackFrameById(frameId: number): StackFrame {
661
        return this.stackFramesCache[frameId];
2✔
662
    }
663

664
    private cleanUpFunctionName(functionName): string {
665
        return functionName.substring(functionName.lastIndexOf('@') + 1);
15✔
666
    }
667

668
    /**
669
     * Get info about the specified variable.
670
     * @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
671
     */
672
    private async getVariablesResponse(expression: string, frameId: number) {
673
        const logger = this.logger.createLogger('[getVariable]');
2✔
674
        logger.info('begin', { expression });
2✔
675
        if (!this.isAtDebuggerPrompt) {
2!
676
            throw new Error('Cannot resolve variable: debugger is not paused');
×
677
        }
678

679
        let frame = this.getStackFrameById(frameId);
2✔
680
        if (!frame) {
2!
681
            throw new Error('Cannot request variable without a corresponding frame');
×
682
        }
683

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

687
        // Temporary workaround related to casing issues over the protocol
688
        if (this.capabilities.enableVariablesLowerCaseRetry && variablePath?.length > 0) {
2!
689
            variablePath[0] = variablePath[0].toLowerCase();
×
690
        }
691

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

694
        if (this.capabilities.enableVariablesLowerCaseRetry && response.data.errorCode !== ErrorCode.OK) {
2!
695
            // Temporary workaround related to casing issues over the protocol
696
            logger.log(`Retrying expression as lower case:`, expression);
×
697
            variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase());
×
698
            response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
×
699
        }
700
        return response;
2✔
701
    }
702

703
    /**
704
     * Get the variable for the specified expression.
705
     */
706
    public async getVariable(expression: string, frameId: number): Promise<EvaluateContainer> {
707
        const response = await this.getVariablesResponse(expression, frameId);
1✔
708

709
        if (Array.isArray(response?.data?.variables)) {
1!
710
            const container = this.createEvaluateContainer(
1✔
711
                response.data.variables[0],
712
                //the name of the top container is the expression itself
713
                expression,
714
                //this is the top-level container, so there are no parent keys to this entry
715
                undefined
716
            );
717
            await insertCustomVariables(this, expression, container);
1✔
718
            container.namedVariables = container.children.length - container.indexedVariables;
1✔
719
            return container;
1✔
720
        }
721
    }
722

723
    /**
724
     * Get the list of local variables
725
     */
726
    public async getLocalVariables(frameId: number) {
727
        const response = await this.getVariablesResponse('', frameId);
1✔
728

729
        if (response?.data?.errorCode === ErrorCode.OK && Array.isArray(response?.data?.variables)) {
1!
730
            //create a top-level container to hold all the local vars
731
            const container = this.createEvaluateContainer(
1✔
732
                //dummy data
733
                {
734
                    isConst: false,
735
                    isContainer: true,
736
                    keyType: VariableType.String,
737
                    refCount: undefined,
738
                    type: VariableType.AssociativeArray,
739
                    value: undefined,
740
                    children: response.data.variables
741
                },
742
                //no name, this is a dummy container
743
                undefined,
744
                //there's no parent path
745
                undefined
746
            );
747
            container.indexedVariables = 0;
1✔
748
            container.namedVariables = container.children.length;
1✔
749
            return container;
1✔
750
        }
751
    }
752

753
    /**
754
     * Create an EvaluateContainer for the given variable. If the variable has children, those are created and attached as well
755
     * @param variable a Variable object from the debug protocol debugger
756
     * @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`)
757
     * @param parentEvaluateName the string used to derive the parent, _excluding_ this variable's name (i.e. `alpha.beta` or `alpha[0]`)
758
     */
759
    private createEvaluateContainer(variable: Variable, name: string | number, parentEvaluateName: string): EvaluateContainer {
760
        let value;
761
        let variableType = variable.type;
10✔
762
        if (variable.value === null) {
10!
763
            value = 'roInvalid';
×
764
        } else if (variableType === VariableType.String) {
10✔
765
            value = `\"${variable.value}\"`;
2✔
766
        } else {
767
            value = variable.value;
8✔
768
        }
769

770
        if (variableType === VariableType.SubtypedObject) {
10!
771
            //subtyped objects can only have string values
772
            let parts = (variable.value as string).split('; ');
×
773
            // Pull the primary type from the value.
774
            (variableType as string) = parts[0];
×
775

776
            // Format the value to be more readable in the UI.
777
            // Example: `roSGNode; Group` = `roSGNode (Group)`
778
            (value as string) = `${parts[0]}(${parts[1]})`;
×
779
        } else if (variableType === VariableType.Object || variableType === VariableType.Interface) {
10!
780
            // We want the type to reflect `roAppInfo` or `roDeviceInfo` for example in the UI
781
            // so set the type to be the value from the device
782
            variableType = value;
×
783
        } else if (variableType === VariableType.AssociativeArray) {
10✔
784
            // We want the type to reflect `function` in the UI
785
            value = VariableType.AssociativeArray;
7✔
786
        }
787

788
        //build full evaluate name for this var. (i.e. `alpha["beta"]` + ["charlie"]` === `alpha["beta"]["charlie"]`)
789
        let evaluateName: string;
790
        if (!parentEvaluateName?.trim()) {
10✔
791
            evaluateName = name?.toString();
6✔
792
        } else if (variable.isVirtual) {
4!
793
            evaluateName = `${parentEvaluateName}.${name}`;
×
794
        } else if (typeof name === 'string') {
4✔
795
            evaluateName = `${parentEvaluateName}["${name}"]`;
3✔
796
        } else if (typeof name === 'number') {
1!
797
            evaluateName = `${parentEvaluateName}[${name}]`;
1✔
798
        }
799

800
        let container: EvaluateContainer = {
10✔
801
            name: name?.toString() ?? '',
60✔
802
            evaluateName: evaluateName ?? '',
30✔
803
            type: variableType ?? '',
30!
804
            value: value ?? null,
30!
805
            highLevelType: undefined,
806
            //non object/array variables don't have a key type
807
            keyType: variable.keyType as unknown as KeyType,
808
            namedVariables: 0,
809
            indexedVariables: 0,
810
            //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
811
            children: []
812
        };
813

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

818
        if (container.keyType === KeyType.integer) {
10✔
819
            container.indexedVariables = variable.childCount ?? variable.children?.length ?? undefined;
2!
820
            // We do not know how many named variables there are, if any, so we will always tell the DAP client to ask for them
821
            container.namedVariables = 1;
2✔
822
        } else if (container.keyType === KeyType.string) {
8✔
823
            // container.namedVariables = variable.childCount ?? variable.children?.length ?? undefined;
824
            // Force one so DAP client always asks for all named vars
825
            container.namedVariables = 1;
5✔
826
        }
827

828
        //recursively generate children containers
829
        if ([KeyType.integer, KeyType.string].includes(container.keyType) && Array.isArray(variable.children)) {
10✔
830
            container.children = [];
4✔
831

832
            container.namedVariables = 0;
4✔
833
            container.indexedVariables = 0;
4✔
834

835
            for (let i = 0; i < variable.children.length; i++) {
4✔
836
                const childVariable = variable.children[i];
6✔
837
                if (childVariable.name === undefined) {
6!
838
                    container.indexedVariables++;
×
839
                }
840
                const childContainer = this.createEvaluateContainer(
6✔
841
                    childVariable,
842
                    container.keyType === KeyType.integer && !childVariable.isVirtual ? i : childVariable.name,
13✔
843
                    container.evaluateName
844
                );
845
                container.children.push(childContainer);
6✔
846
            }
847
        }
848

849
        //show virtual variables in the UI
850
        if (variable.isVirtual) {
10!
851
            if (!container.presentationHint) {
×
852
                container.presentationHint = {};
×
853
            }
854
            container.presentationHint.kind = 'virtual';
×
855
        }
856

857
        return container;
10✔
858
    }
859

860
    /**
861
     * Cache items by a unique key
862
     * @param expression
863
     * @param factory
864
     */
865
    private resolve<T>(key: string, factory: () => T | Thenable<T>): Promise<T> {
866
        if (this.cache[key]) {
37✔
867
            this.logger.log('return cashed response', key, this.cache[key]);
4✔
868
            return this.cache[key];
4✔
869
        }
870
        this.cache[key] = Promise.resolve<T>(factory());
33✔
871
        return this.cache[key];
33✔
872
    }
873

874
    /**
875
     * Get a list of threads. The active thread will always be first in the list.
876
     */
877
    public async getThreads() {
878
        if (!this.isAtDebuggerPrompt) {
18!
879
            throw new Error('Cannot get threads: debugger is not paused');
×
880
        }
881
        return this.resolve('threads', async () => {
18✔
882
            let threads: Thread[] = [];
15✔
883
            let threadsResponse: ThreadsResponse;
884
            // sometimes roku threads are stubborn and haven't stopped yet, causing our ThreadsRequest to fail with "not stopped".
885
            // A nice simple fix for this is to just send a "pause" request again, which seems to fix the issue.
886
            // we'll do this a few times just to make sure we've tried our best to get the list of threads.
887
            for (let i = 0; i < 3; i++) {
15✔
888
                threadsResponse = await this.client.threads();
15✔
889
                if (threadsResponse.data.errorCode === ErrorCode.NOT_STOPPED) {
15!
890
                    this.logger.log(`Threads request retrying... ${i}:\n`, threadsResponse);
×
891
                    threadsResponse = undefined;
×
892
                    const pauseResponse = await this.client.pause(true);
×
893
                    await util.sleep(100);
×
894
                } else {
895
                    break;
15✔
896
                }
897
            }
898
            if (!threadsResponse) {
15!
899
                return [];
×
900
            }
901

902
            for (let i = 0; i < (threadsResponse.data?.threads?.length ?? 0); i++) {
15!
903
                let threadInfo = threadsResponse.data.threads[i];
15✔
904
                let thread = <Thread>{
15✔
905
                    // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
906
                    // NOTE: Rely on the thead index from the threads update event.
907
                    isSelected: this.client.primaryThread === i,
908
                    // isSelected: threadInfo.isPrimary,
909
                    isDetached: threadInfo.isDetached,
910
                    filePath: threadInfo.filePath,
911
                    functionName: threadInfo.functionName,
912
                    lineNumber: threadInfo.lineNumber, //threadInfo.lineNumber is 1-based. Thread requires 1-based line numbers
913
                    lineContents: threadInfo.codeSnippet,
914
                    threadId: i
915
                };
916
                threads.push(thread);
15✔
917
            }
918
            //make sure the selected thread is at the top
919
            threads.sort((a, b) => {
15✔
920
                return a.isSelected ? -1 : 1;
×
921
            });
922

923
            return threads;
15✔
924
        });
925
    }
926

927
    private async getThreadByThreadId(threadId: number) {
928
        let threads = await this.getThreads();
18✔
929
        for (let thread of threads) {
18✔
930
            if (thread.threadId === threadId) {
18✔
931
                return thread;
17✔
932
            }
933
        }
934
    }
935

936
    public removeAllListeners() {
937
        if (this.emitter) {
×
938
            this.emitter.removeAllListeners();
×
939
        }
940
    }
941

942
    /**
943
     * Indicates whether this class had `.destroy()` called at least once. Mostly used for checking externally to see if
944
     * the whole debug session has been terminated or is in a bad state.
945
     */
946
    public isDestroyed = false;
20✔
947
    /**
948
     * Disconnect from the telnet session and unset all objects
949
     */
950
    public async destroy() {
951
        this.isDestroyed = true;
×
952

953
        // destroy the debug client if it's defined
954
        if (this.client) {
×
955
            try {
×
956
                await this.client.destroy();
×
957
            } catch (e) {
958
                this.logger.error(e);
×
959
            }
960
        }
961

962
        try {
×
963
            let shutdownTimeMax = this.options?.shutdownTimeout ?? 10_000;
×
964
            await this.destroyCompileClient(shutdownTimeMax);
×
965
        } catch (e) {
966
            this.logger.error(e);
×
967
        }
968

969
        this.cache = undefined;
×
970
        this.removeAllListeners();
×
971
        this.emitter = undefined;
×
972
    }
973

974
    /**
975
     * Promise that is resolved when the compile client socket is closed
976
     */
977
    private compileClientClosed = defer<void>();
20✔
978
    private isDestroyingCompileClient = false;
20✔
979

980
    private async destroyCompileClient(timeout: number) {
981
        if (this.compileClient && !this.isDestroyingCompileClient) {
×
982
            this.isDestroyingCompileClient = true;
×
983
            this.compileClient?.end();
×
984

985
            //wait for the compileClient to be closed
986
            await Promise.race([
×
987
                this.compileClientClosed.promise,
988
                util.sleep(timeout)
989
            ]);
990

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

993
            //destroy the compileClient
994
            this.compileClient?.removeAllListeners();
×
995
            this.compileClient?.destroy();
×
996
            this.compileClient = undefined;
×
997
            this.isDestroyingCompileClient = false;
×
998
        }
999
    }
1000

1001
    /**
1002
     * Passes the log level down to the RendezvousTracker and ChanperfTracker
1003
     * @param outputLevel the consoleOutput from the launch config
1004
     */
1005
    public setConsoleOutput(outputLevel: string) {
1006
        this.chanperfTracker.setConsoleOutput(outputLevel);
×
1007
        this.rendezvousTracker.setConsoleOutput(outputLevel);
×
1008
    }
1009

1010
    /**
1011
     * Sends a call to the RendezvousTracker to clear the current rendezvous history
1012
     */
1013
    public clearRendezvousHistory() {
1014
        this.rendezvousTracker.clearHistory();
×
1015
    }
1016

1017
    /**
1018
     * Sends a call to the ChanperfTracker to clear the current chanperf history
1019
     */
1020
    public clearChanperfHistory() {
1021
        this.chanperfTracker.clearHistory();
×
1022
    }
1023

1024
    /**
1025
     * The most recently requested exception breakpoint filters. Stored so we can replay them
1026
     * to the debug protocol client once it connects (the session can send these before the
1027
     * device has launched and the client has finished its handshake).
1028
     */
1029
    private pendingExceptionBreakpointFilters: ExceptionBreakpoint[] | undefined;
1030

1031
    public async setExceptionBreakpoints(filters: ExceptionBreakpoint[]) {
1032
        if (!this.capabilities.supportsExceptionBreakpoints) {
×
1033
            return undefined;
×
1034
        }
1035
        //if the client isn't connected yet, queue the filters for replay on connect
1036
        if (!this.connected) {
×
1037
            this.pendingExceptionBreakpointFilters = filters;
×
1038
            return undefined;
×
1039
        }
1040
        return this.client.setExceptionBreakpoints(filters);
×
1041
    }
1042

1043
    private syncBreakpointsPromise = Promise.resolve();
20✔
1044
    public async syncBreakpoints() {
1045
        this.logger.log('syncBreakpoints()');
41✔
1046
        //wait for the previous sync to finish
1047
        this.syncBreakpointsPromise = this.syncBreakpointsPromise
41✔
1048
            //ignore any errors
1049
            .catch(() => { })
1050
            //run the next sync
1051
            .then(() => this._syncBreakpoints());
41✔
1052

1053
        //return the new promise, which will resolve once our latest `syncBreakpoints()` call is finished
1054
        return this.syncBreakpointsPromise;
41✔
1055
    }
1056

1057
    public async _syncBreakpoints() {
1058
        //we need to actually be connected to the device before we can push breakpoints. We'll get
1059
        //called again once the debug protocol client has connected.
1060
        if (!this.connected) {
31✔
1061
            this.logger.info('Cannot sync breakpoints because the debug protocol client has not connected yet');
2✔
1062
            return;
2✔
1063
        }
1064
        //we can't send breakpoints unless we're stopped (or in a protocol version that supports sending them while running).
1065
        //So...if we're not stopped, quit now. (we'll get called again when the stop event happens)
1066
        if (!this.capabilities.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) {
29✔
1067
            this.logger.info('Cannot sync breakpoints because the debugger', this.capabilities.supportsBreakpointRegistrationWhileRunning ? 'does not support sending breakpoints while running' : 'is not paused');
15!
1068
            return;
15✔
1069
        }
1070

1071
        //compute breakpoint changes since last sync
1072
        const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects());
14✔
1073
        this.logger.log('Syncing breakpoints', diff);
14✔
1074

1075
        if (diff.added.length === 0 && diff.removed.length === 0) {
14✔
1076
            this.logger.debug('No breakpoints to sync');
2✔
1077
            return;
2✔
1078
        }
1079

1080
        //getDiff above can yield to other microtasks. If the protocol client closed while we were
1081
        //awaiting (mid-sync TOCTOU), bail out before dereferencing this.client. The app-exit handler
1082
        //resets the breakpoint baseline so the next reconnect re-pushes any pending changes.
1083
        //See https://github.com/rokucommunity/vscode-brightscript-language/issues/811
1084
        if (!this.client) {
12✔
1085
            this.logger.info('Skipping breakpoint sync because the protocol client closed mid-sync');
2✔
1086
            return;
2✔
1087
        }
1088

1089
        // REMOVE breakpoints (delete these breakpoints from the device)
1090
        if (diff.removed.length > 0) {
10✔
1091
            const response = await this.client.removeBreakpoints(
3✔
1092
                //TODO handle retrying to remove breakpoints that don't have deviceIds yet but might get one in the future
1093
                diff.removed.map(x => x.deviceId).filter(x => typeof x === 'number')
5✔
1094
            );
1095

1096
            if (response.data?.errorCode === ErrorCode.NOT_STOPPED) {
3!
1097
                this.breakpointManager.failedDeletions.push(...diff.removed);
1✔
1098
            }
1099
        }
1100

1101
        if (diff.added.length > 0) {
10✔
1102
            //the removeBreakpoints await above can also yield; re-check before attempting the add
1103
            if (!this.client) {
8!
NEW
1104
                this.logger.info('Skipping breakpoint add because the protocol client closed mid-sync');
×
NEW
1105
                return;
×
1106
            }
1107
            const breakpointsToSendToDevice = diff.added.map(breakpoint => {
8✔
1108
                const hitCount = parseInt(breakpoint.hitCondition);
11✔
1109
                return {
11✔
1110
                    filePath: breakpoint.pkgPath,
1111
                    lineNumber: breakpoint.line,
1112
                    hitCount: !isNaN(hitCount) ? hitCount : undefined,
11!
1113
                    conditionalExpression: breakpoint.condition,
1114
                    srcHash: breakpoint.srcHash,
1115
                    destHash: breakpoint.destHash,
1116
                    componentLibraryName: breakpoint.componentLibraryName
1117
                };
1118
            });
1119

1120
            //split the list into conditional and non-conditional breakpoints.
1121
            //(TODO we can eliminate this splitting logic once the conditional breakpoints "continue" bug in protocol is fixed)
1122
            const standardBreakpoints: typeof breakpointsToSendToDevice = [];
8✔
1123
            const conditionalBreakpoints: typeof breakpointsToSendToDevice = [];
8✔
1124
            for (const breakpoint of breakpointsToSendToDevice) {
8✔
1125
                if (breakpoint?.conditionalExpression?.trim()) {
11!
1126
                    conditionalBreakpoints.push(breakpoint);
1✔
1127
                } else {
1128
                    standardBreakpoints.push(breakpoint);
10✔
1129
                }
1130
            }
1131
            for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) {
8✔
1132
                const response = await this.client.addBreakpoints(breakpoints);
16✔
1133

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

1139
                        if (typeof deviceBreakpoint?.id === 'number') {
9!
1140
                            //sync this breakpoint's deviceId with the roku-assigned breakpoint ID
1141
                            this.breakpointManager.setBreakpointDeviceId(
9✔
1142
                                breakpoints[i].srcHash,
1143
                                breakpoints[i].destHash,
1144
                                deviceBreakpoint.id
1145
                            );
1146
                        }
1147

1148
                        //this breakpoint had an issue. remove it from the client
1149
                        if (deviceBreakpoint.errorCode !== ErrorCode.OK) {
9✔
1150
                            this.breakpointManager.deleteBreakpoint(breakpoints[i].srcHash);
1✔
1151
                        }
1152
                    }
1153
                    //the entire response was bad. delete these breakpoints from the client
1154
                } else {
1155
                    this.breakpointManager.deleteBreakpoints(
2✔
1156
                        breakpoints.map(x => x.srcHash)
2✔
1157
                    );
1158
                }
1159
            }
1160
        }
1161
    }
1162

1163
    public isTelnetAdapter(): this is TelnetAdapter {
1164
        return false;
×
1165
    }
1166

1167
    public isDebugProtocolAdapter(): this is DebugProtocolAdapter {
1168
        return true;
×
1169
    }
1170
}
1171

1172
export interface StackFrame {
1173
    frameId: number;
1174
    frameIndex: number;
1175
    threadIndex: number;
1176
    filePath: string;
1177
    lineNumber: number;
1178
    functionIdentifier: string;
1179
}
1180

1181
export enum EventName {
2✔
1182
    suspend = 'suspend'
2✔
1183
}
1184

1185
export interface EvaluateContainer {
1186
    name: string;
1187
    evaluateName: string;
1188
    type: string;
1189
    value?: any;
1190
    keyType?: KeyType;
1191
    namedVariables?: number;
1192
    indexedVariables?: number;
1193
    highLevelType?: HighLevelType;
1194
    children: EvaluateContainer[];
1195
    isCustom?: boolean;
1196
    evaluateNow?: boolean;
1197
    presentationHint?: DebugProtocol.VariablePresentationHint;
1198
}
1199

1200
export enum KeyType {
2✔
1201
    string = 'String',
2✔
1202
    integer = 'Integer',
2✔
1203
    legacy = 'Legacy'
2✔
1204
}
1205

1206
export interface Thread {
1207
    isSelected: boolean;
1208
    isDetached?: boolean;
1209
    /**
1210
     * The 1-based line number for the thread
1211
     */
1212
    lineNumber: number;
1213
    filePath: string;
1214
    functionName: string;
1215
    lineContents: string;
1216
    threadId: number;
1217
}
1218

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

© 2026 Coveralls, Inc