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

rokucommunity / roku-debug / 27218968133

09 Jun 2026 04:00PM UTC coverage: 70.722%. First build
27218968133

Pull #303

github

web-flow
Merge 3599b905f into 7f9b69552
Pull Request #303: add support of ThreadsResponse id name type fields

3455 of 5168 branches covered (66.85%)

Branch coverage included in aggregate %.

44 of 45 new or added lines in 5 files covered. (97.78%)

5678 of 7746 relevant lines covered (73.3%)

45.02 hits per line

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

59.85
/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
    private emitter: EventEmitter;
79
    private chanperfTracker: ChanperfTracker;
80
    private client: DebugProtocolClient;
81
    private nextFrameId = 1;
21✔
82

83
    private stackFramesCache: Record<number, StackFrame> = {};
21✔
84
    private cache = {};
21✔
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) => {
16✔
114
            const disconnect = this.on(eventName as Parameters<DebugProtocolAdapter['on']>[0], (...args) => {
16✔
115
                disconnect();
16✔
116
                resolve(...args);
16✔
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);
33!
144
        return () => {
33✔
145
            this.emitter?.removeListener(eventName, handler);
16!
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(() => {
72✔
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) {
72!
159
                return;
×
160
            }
161
            //drop stale 'suspend'/'runtime-error' events when the debugger has already resumed.
162
            //emit() defers via setTimeout, so isStopped can flip false between queue and fire (e.g. auto-continue on entry breakpoint),
163
            //causing downstream handlers to call getThreads() against a running debugger.
164
            //See https://github.com/rokucommunity/vscode-brightscript-language/issues/798
165
            if ((eventName === 'suspend' || eventName === 'runtime-error') && !this.isAtDebuggerPrompt) {
72✔
166
                this.logger.warn(`Dropping stale "${eventName}" event because debugger is no longer paused`);
1✔
167
                return;
1✔
168
            }
169
            this.emitter.emit(eventName, data);
71✔
170
        }, 0);
171
    }
172

173
    /**
174
     * Does the current client support exception breakpoints? Resolved via the live client's
175
     * capabilities when connected, otherwise the device-info-seeded fallback.
176
     */
177
    public get supportsExceptionBreakpoints(): boolean {
178
        return this.capabilities.supportsExceptionBreakpoints;
×
179
    }
180

181
    /**
182
     * Does the current client support conditional breakpoints? Same fallback semantics as
183
     * `supportsExceptionBreakpoints`.
184
     */
185
    public get supportsConditionalBreakpoints(): boolean {
186
        return this.capabilities.supportsConditionalBreakpoints;
×
187
    }
188

189
    /**
190
     * Does the current client support hit-count breakpoints?
191
     */
192
    public get supportsHitConditionalBreakpoints(): boolean {
193
        return this.capabilities.supportsHitConditionalBreakpoints;
×
194
    }
195

196
    /**
197
     * The debugger needs to tell us when to be active (i.e. when the package was deployed)
198
     */
199
    public isActivated = false;
21✔
200

201
    /**
202
     * This will be set to true When the roku emits the [scrpt.ctx.run.enter] text,
203
     * which indicates that the app is running on the Roku
204
     */
205
    public isAppRunning = false;
21✔
206

207
    public activate() {
208
        this.isActivated = true;
×
209
        this.handleStartupIfReady();
×
210
    }
211

212
    public async sendErrors() {
213
        await this.compileErrorProcessor.sendErrors();
×
214
    }
215

216
    private handleStartupIfReady() {
217
        if (this.isActivated && this.isAppRunning) {
16!
218
            this.emit('start');
×
219

220
            //if we are already sitting at a debugger prompt, we need to emit the first suspend event.
221
            //If not, then there are probably still messages being received, so let the normal handler
222
            //emit the suspend event when it's ready
223
            if (this.isAtDebuggerPrompt === true) {
×
224
                this.emit('suspend');
×
225
            }
226
        }
227
    }
228

229
    /**
230
     * Wait until the client has stopped sending messages. This is used mainly during .connect so we can ignore all old messages from the server
231
     * @param client
232
     * @param maxWaitMilliseconds
233
     */
234
    private settleCompileClient(client: Socket, maxWaitMilliseconds = 400) {
×
235
        return new Promise<string>((resolve) => {
×
236
            let timeoutStarted = false;
×
237
            let callCount = -1;
×
238
            let logs = '';
×
239

240
            function handler(buffer) {
241
                callCount++;
×
242
                logs += buffer.toString();
×
243
                let myCallCount = callCount;
×
244
                timeoutStarted = true;
×
245
                setTimeout(() => {
×
246
                    if (myCallCount === callCount) {
×
247
                        // stop listening for data events
248
                        client.removeListener('data', handler);
×
249
                        resolve(logs);
×
250
                    }
251
                }, maxWaitMilliseconds);
252
            }
253

254
            const startTimeout = () => {
×
255
                if (timeoutStarted === false) {
×
256
                    handler(Buffer.from(''));
×
257
                }
258
            };
259

260
            // watch for data events
261
            client.on('data', handler);
×
262

263
            // watch for different connection related events to start the timeout logic
264
            client.on('ready', startTimeout);
×
265
            client.on('end', startTimeout);
×
266
            client.on('closed', startTimeout);
×
267
        });
268
    }
269

270
    public get isAtDebuggerPrompt() {
271
        return this.client?.isStopped ?? false;
81!
272
    }
273

274
    private firstConnectDeferred = defer<void>();
21✔
275

276
    /**
277
     * Resolves when the first connection to the client is established
278
     */
279
    public onReady() {
280
        return this.firstConnectDeferred.promise;
×
281
    }
282

283
    /**
284
     * Connect to the telnet session. This should be called before the channel is launched.
285
     */
286
    public async connect(): Promise<void> {
287
        //Start processing telnet output to look for compile errors or the debugger prompt
288
        await this.processTelnetOutput();
16✔
289

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

293
            //if this is the first time we are connecting, resolve the promise.
294
            //(future events fire for "reconnect" situations, we don't need to resolve again for those)
295
            if (!this.firstConnectDeferred.isCompleted) {
×
296
                this.firstConnectDeferred.resolve();
×
297
            }
298
        });
299
    }
300

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

327
            // Emit IO from the debugger.
328
            this.client.on('protocol-version', (data: ProtocolVersionDetails) => {
16✔
329
                if (data.errorCode === PROTOCOL_ERROR_CODES.SUPPORTED) {
16!
330
                    this.emit('console-output', data.message);
16✔
331
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_TESTED) {
×
332
                    this.emit('unhandled-console-output', data.message);
×
333
                    this.emit('console-output', data.message);
×
334
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_SUPPORTED) {
×
335
                    this.emit('unhandled-console-output', data.message);
×
336
                    this.emit('console-output', data.message);
×
337
                }
338

339
            });
340

341
            // Listen for the close event
342
            this.client.on('close', () => {
16✔
343
                this.emit('close');
1✔
344
                this.beginAppExit();
1✔
345
                void this.client?.destroy();
1!
346
                this.client = undefined;
1✔
347
                //the protocol client is gone — keep `connected` in sync so any subsequent
348
                //syncBreakpoints / setExceptionBreakpoints calls during the close→app-exit
349
                //window early-return instead of dereferencing the undefined client.
350
                //See https://github.com/rokucommunity/vscode-brightscript-language/issues/811
351
                this.connected = false;
1✔
352
            });
353

354
            // Listen for the app exit event
355
            this.client.on('app-exit', () => {
16✔
356
                this.emit('app-exit');
×
357
                void this.client?.destroy();
×
358
                this.client = undefined;
×
359
            });
360

361
            this.client.on('suspend', (data) => {
16✔
362
                this.clearCache();
17✔
363
                this.emit('suspend');
17✔
364
            });
365

366
            this.client.on('runtime-error', (data) => {
16✔
367
                console.debug('hasRuntimeError!!', data);
×
368
                this.emit('runtime-error', <BrightScriptRuntimeError>{
×
369
                    message: data.data.stopReasonDetail,
370
                    errorCode: data.data.stopReason
371
                });
372
            });
373

374
            this.client.on('cannot-continue', () => {
16✔
375
                this.emit('cannot-continue');
×
376
            });
377

378
            //handle when the device verifies breakpoints
379
            this.client.on('breakpoints-verified', (event) => {
16✔
380
                let unverifiableDeviceIds = [] as number[];
6✔
381

382
                //mark the breakpoints as verified
383
                for (let breakpoint of event?.breakpoints ?? []) {
6!
384
                    const success = this.breakpointManager.verifyBreakpoint(breakpoint.id, true);
5✔
385
                    if (!success) {
5✔
386
                        unverifiableDeviceIds.push(breakpoint.id);
3✔
387
                    }
388
                }
389
                //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
390
                if (unverifiableDeviceIds.length > 0) {
6✔
391
                    this.logger.warn('Could not find breakpoints to verify. Removing from device:', { deviceBreakpointIds: unverifiableDeviceIds });
3✔
392
                    void this.client.removeBreakpoints(unverifiableDeviceIds);
3✔
393
                }
394
                this.emit('breakpoints-verified', event);
5✔
395
            });
396

397
            this.client.on('compile-error', (update) => {
16✔
398
                let diagnostics: BSDebugDiagnostic[] = [];
×
399
                diagnostics.push({
×
400
                    path: update.data.filePath,
401
                    range: bscUtil.createRange(update.data.lineNumber - 1, 0, update.data.lineNumber - 1, 999),
402
                    message: update.data.errorMessage,
403
                    severity: DiagnosticSeverity.Error,
404
                    code: undefined
405
                });
406
                this.emit('diagnostics', diagnostics);
×
407
            });
408

409
            await this.client.connect();
16✔
410

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

428
            //the adapter is connected and running smoothly. resolve the promise
429
            deferred.resolve();
16✔
430
        } catch (e) {
431
            deferred.reject(e);
×
432
        }
433
        return deferred.promise;
16✔
434
    }
435

436
    private beginAppExit() {
437
        this.compileErrorProcessor.compileErrorTimer = setTimeout(() => {
1✔
438
            this.isAppRunning = false;
1✔
439
            this.emit('app-exit');
1✔
440
        }, 200);
441
    }
442

443
    /**
444
     * Determines if the current version of the debug protocol supports emitting compile error updates.
445
     */
446
    public get supportsCompileErrorReporting() {
447
        return this.capabilities.supportsCompileErrorReporting;
×
448
    }
449

450
    /**
451
     * Indicate if virtual variables should be auto resolved when they are encountered.
452
     */
453
    public get autoResolveVirtualVariables() {
454
        return this.options.autoResolveVirtualVariables;
1✔
455
    }
456

457
    private processingTelnetOutput = false;
21✔
458
    public async processTelnetOutput() {
459
        if (this.processingTelnetOutput) {
1!
460
            return;
×
461
        }
462
        this.processingTelnetOutput = true;
1✔
463

464
        let deferred = defer();
1✔
465
        try {
1✔
466
            this.compileClient = new Socket({ allowHalfOpen: false });
1✔
467
            util.registerSocketLogging(this.compileClient, this.logger, 'CompileClient');
1✔
468

469
            this.compileErrorProcessor.on('diagnostics', (errors) => {
1✔
470
                this.compileClient.end();
×
471
                this.emit('diagnostics', errors);
×
472
            });
473

474
            this.compileErrorProcessor.on('launch-status', (message) => {
1✔
475
                this.emit('launch-status', message);
×
476
            });
477

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

490
            this.logger.debug('Waiting for the compile client to settle');
1✔
491
            const settledLogs = await this.settleCompileClient(this.compileClient);
1✔
492
            this.logger.debug('Compile client has settled');
1✔
493
            this.logger.trace('Settled logs:', settledLogs);
1✔
494

495
            if (settledLogs.trim().startsWith('Console connection is already in use.')) {
1!
496
                throw new SocketConnectionInUseError(`Telnet connection ${this.options.host}:${this.options.brightScriptConsolePort} already is use`, {
×
497
                    port: this.options.brightScriptConsolePort,
498
                    host: this.options.host
499
                });
500
            }
501

502
            let lastPartialLine = '';
1✔
503
            this.compileClient.on('data', (buffer) => {
1✔
504
                let responseText = buffer.toString();
×
505
                this.logger.info('CompileClient received data', { responseText });
×
506

507
                let logResult = util.handleLogFragments(lastPartialLine, buffer.toString());
×
508

509
                // Save any remaining partial line for the next event
510
                lastPartialLine = logResult.remaining;
×
511
                if (logResult.completed) {
×
512
                    // Emit the completed io string.
513
                    this.findWaitForDebuggerPrompt(logResult.completed);
×
514
                    this.compileErrorProcessor.processUnhandledLines(logResult.completed);
×
515
                    this.logger.debug('CompileClient data:', logResult.completed);
×
516
                    this.emit('unhandled-console-output', logResult.completed);
×
517
                } else {
518
                    this.logger.debug('CompileClient buffer was split:', lastPartialLine);
×
519
                }
520
            });
521

522
            this.compileClient.on('close', () => {
1✔
523
                this.logger.log('compileClient socket closed');
1✔
524
                this.compileClientClosed.tryResolve();
1✔
525
            });
526

527
            // connected to telnet. resolve the promise
528
            deferred.resolve();
1✔
529
        } catch (e) {
530
            deferred.reject(e);
×
531
        }
532
        return deferred.promise;
1✔
533
    }
534

535
    private findWaitForDebuggerPrompt(responseText: string) {
536
        let lines = responseText.split(/\r?\n/g);
×
537
        for (const line of lines) {
×
538
            if (/Waiting for debugger on \d+\.\d+\.\d+\.\d+:8081/g.exec(line)) {
×
539
                this.emit('waiting-for-debugger');
×
540
            }
541
        }
542
    }
543

544
    /**
545
     * Send command to step over
546
     */
547
    public async stepOver(threadId: number) {
548
        this.clearCache();
×
549
        return this.client.stepOver(threadId);
×
550
    }
551

552
    public async stepInto(threadId: number) {
553
        this.clearCache();
×
554
        return this.client.stepIn(threadId);
×
555
    }
556

557
    public async stepOut(threadId: number) {
558
        this.clearCache();
×
559
        return this.client.stepOut(threadId);
×
560
    }
561

562
    /**
563
     * Tell the brightscript program to continue (i.e. resume program)
564
     */
565
    public async continue() {
566
        this.clearCache();
×
567
        return this.client.continue();
×
568
    }
569

570
    /**
571
     * Tell the brightscript program to pause (fall into debug mode)
572
     */
573
    public async pause() {
574
        this.clearCache();
×
575
        //send the kill signal, which breaks into debugger mode
576
        return this.client.pause();
×
577
    }
578

579
    /**
580
     * Clears the state, which means that everything will be retrieved fresh next time it is requested
581
     */
582
    public clearCache() {
583
        this.cache = {};
17✔
584
        this.stackFramesCache = {};
17✔
585
    }
586

587
    /**
588
     * Execute a command directly on the roku. Returns the output of the command
589
     * @param command
590
     * @returns the output of the command (if possible)
591
     */
592
    public async evaluate(command: string, frameId: number = this.client.primaryThread): Promise<RokuAdapterEvaluateResponse> {
×
593
        if (this.capabilities.supportsExecuteCommand) {
×
594
            if (!this.isAtDebuggerPrompt) {
×
595
                throw new Error('Cannot run evaluate: debugger is not paused');
×
596
            }
597

598
            let stackFrame = this.getStackFrameById(frameId);
×
599
            if (!stackFrame) {
×
600
                throw new Error('Cannot execute command without a corresponding frame');
×
601
            }
602
            this.logger.log('evaluate ', { command, frameId });
×
603

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

630
    public async getStackTrace(threadIndex: number = this.client.primaryThread) {
×
631
        if (!this.isAtDebuggerPrompt) {
20!
632
            throw new Error('Cannot get stack trace: debugger is not paused');
×
633
        }
634
        return this.resolve(`stack trace for thread ${threadIndex}`, async () => {
20✔
635
            let thread = await this.getThreadByThreadId(threadIndex);
19✔
636
            let frames: StackFrame[] = [];
19✔
637
            let stackTraceData = await this.client.getStackTrace(threadIndex);
19✔
638

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

665
            return frames;
17✔
666
        });
667
    }
668

669
    public getStackFrameById(frameId: number): StackFrame {
670
        return this.stackFramesCache[frameId];
2✔
671
    }
672

673
    private cleanUpFunctionName(functionName): string {
674
        return functionName.substring(functionName.lastIndexOf('@') + 1);
16✔
675
    }
676

677
    /**
678
     * Get info about the specified variable.
679
     * @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
680
     */
681
    private async getVariablesResponse(expression: string, frameId: number) {
682
        const logger = this.logger.createLogger('[getVariable]');
2✔
683
        logger.info('begin', { expression });
2✔
684
        if (!this.isAtDebuggerPrompt) {
2!
685
            throw new Error('Cannot resolve variable: debugger is not paused');
×
686
        }
687

688
        let frame = this.getStackFrameById(frameId);
2✔
689
        if (!frame) {
2!
690
            throw new Error('Cannot request variable without a corresponding frame');
×
691
        }
692

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

696
        // Temporary workaround related to casing issues over the protocol
697
        if (this.capabilities.enableVariablesLowerCaseRetry && variablePath?.length > 0) {
2!
698
            variablePath[0] = variablePath[0].toLowerCase();
×
699
        }
700

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

703
        if (this.capabilities.enableVariablesLowerCaseRetry && response.data.errorCode !== ErrorCode.OK) {
2!
704
            // Temporary workaround related to casing issues over the protocol
705
            logger.log(`Retrying expression as lower case:`, expression);
×
706
            variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase());
×
707
            response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
×
708
        }
709
        return response;
2✔
710
    }
711

712
    /**
713
     * Get the variable for the specified expression.
714
     */
715
    public async getVariable(expression: string, frameId: number): Promise<EvaluateContainer> {
716
        const response = await this.getVariablesResponse(expression, frameId);
1✔
717

718
        if (Array.isArray(response?.data?.variables)) {
1!
719
            const container = this.createEvaluateContainer(
1✔
720
                response.data.variables[0],
721
                //the name of the top container is the expression itself
722
                expression,
723
                //this is the top-level container, so there are no parent keys to this entry
724
                undefined
725
            );
726
            await insertCustomVariables(this, expression, container);
1✔
727
            container.namedVariables = container.children.length - container.indexedVariables;
1✔
728
            return container;
1✔
729
        }
730
    }
731

732
    /**
733
     * Get the list of local variables
734
     */
735
    public async getLocalVariables(frameId: number) {
736
        const response = await this.getVariablesResponse('', frameId);
1✔
737

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

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

779
        if (variableType === VariableType.SubtypedObject) {
10!
780
            //subtyped objects can only have string values
781
            let parts = (variable.value as string).split('; ');
×
782
            // Pull the primary type from the value.
783
            (variableType as string) = parts[0];
×
784

785
            // Format the value to be more readable in the UI.
786
            // Example: `roSGNode; Group` = `roSGNode (Group)`
787
            (value as string) = `${parts[0]}(${parts[1]})`;
×
788
        } else if (variableType === VariableType.Object || variableType === VariableType.Interface) {
10!
789
            // We want the type to reflect `roAppInfo` or `roDeviceInfo` for example in the UI
790
            // so set the type to be the value from the device
791
            variableType = value;
×
792
        } else if (variableType === VariableType.AssociativeArray) {
10✔
793
            // We want the type to reflect `function` in the UI
794
            value = VariableType.AssociativeArray;
7✔
795
        }
796

797
        //build full evaluate name for this var. (i.e. `alpha["beta"]` + ["charlie"]` === `alpha["beta"]["charlie"]`)
798
        let evaluateName: string;
799
        if (!parentEvaluateName?.trim()) {
10✔
800
            evaluateName = name?.toString();
6✔
801
        } else if (variable.isVirtual) {
4!
802
            evaluateName = `${parentEvaluateName}.${name}`;
×
803
        } else if (typeof name === 'string') {
4✔
804
            evaluateName = `${parentEvaluateName}["${name}"]`;
3✔
805
        } else if (typeof name === 'number') {
1!
806
            evaluateName = `${parentEvaluateName}[${name}]`;
1✔
807
        }
808

809
        let container: EvaluateContainer = {
10✔
810
            name: name?.toString() ?? '',
60✔
811
            evaluateName: evaluateName ?? '',
30✔
812
            type: variableType ?? '',
30!
813
            value: value ?? null,
30!
814
            highLevelType: undefined,
815
            //non object/array variables don't have a key type
816
            keyType: variable.keyType as unknown as KeyType,
817
            namedVariables: 0,
818
            indexedVariables: 0,
819
            //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
820
            children: []
821
        };
822

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

827
        if (container.keyType === KeyType.integer) {
10✔
828
            container.indexedVariables = variable.childCount ?? variable.children?.length ?? undefined;
2!
829
            // We do not know how many named variables there are, if any, so we will always tell the DAP client to ask for them
830
            container.namedVariables = 1;
2✔
831
        } else if (container.keyType === KeyType.string) {
8✔
832
            // container.namedVariables = variable.childCount ?? variable.children?.length ?? undefined;
833
            // Force one so DAP client always asks for all named vars
834
            container.namedVariables = 1;
5✔
835
        }
836

837
        //recursively generate children containers
838
        if ([KeyType.integer, KeyType.string].includes(container.keyType) && Array.isArray(variable.children)) {
10✔
839
            container.children = [];
4✔
840

841
            container.namedVariables = 0;
4✔
842
            container.indexedVariables = 0;
4✔
843

844
            for (let i = 0; i < variable.children.length; i++) {
4✔
845
                const childVariable = variable.children[i];
6✔
846
                if (childVariable.name === undefined) {
6!
847
                    container.indexedVariables++;
×
848
                }
849
                const childContainer = this.createEvaluateContainer(
6✔
850
                    childVariable,
851
                    container.keyType === KeyType.integer && !childVariable.isVirtual ? i : childVariable.name,
13✔
852
                    container.evaluateName
853
                );
854
                container.children.push(childContainer);
6✔
855
            }
856
        }
857

858
        //show virtual variables in the UI
859
        if (variable.isVirtual) {
10!
860
            if (!container.presentationHint) {
×
861
                container.presentationHint = {};
×
862
            }
863
            container.presentationHint.kind = 'virtual';
×
864
        }
865

866
        return container;
10✔
867
    }
868

869
    /**
870
     * Cache items by a unique key
871
     * @param expression
872
     * @param factory
873
     */
874
    private resolve<T>(key: string, factory: () => T | Thenable<T>): Promise<T> {
875
        if (this.cache[key]) {
39✔
876
            this.logger.log('return cashed response', key, this.cache[key]);
4✔
877
            return this.cache[key];
4✔
878
        }
879
        this.cache[key] = Promise.resolve<T>(factory());
35✔
880
        return this.cache[key];
35✔
881
    }
882

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

911
            for (let i = 0; i < (threadsResponse.data?.threads?.length ?? 0); i++) {
16!
912
                let threadInfo = threadsResponse.data.threads[i];
16✔
913
                let thread = <Thread>{
16✔
914
                    // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
915
                    // NOTE: Rely on the thead index from the threads update event.
916
                    isSelected: this.client.primaryThread === i,
917
                    // isSelected: threadInfo.isPrimary,
918
                    isDetached: threadInfo.isDetached,
919
                    filePath: threadInfo.filePath,
920
                    functionName: threadInfo.functionName,
921
                    lineNumber: threadInfo.lineNumber, //threadInfo.lineNumber is 1-based. Thread requires 1-based line numbers
922
                    lineContents: threadInfo.codeSnippet,
923
                    threadId: i,
924
                    osThreadId: threadInfo.osThreadId,
925
                    name: threadInfo.name,
926
                    type: threadInfo.type
927
                };
928
                threads.push(thread);
16✔
929
            }
930
            //make sure the selected thread is at the top
931
            threads.sort((a, b) => {
16✔
932
                return a.isSelected ? -1 : 1;
×
933
            });
934

935
            return threads;
16✔
936
        });
937
    }
938

939
    private async getThreadByThreadId(threadId: number) {
940
        let threads = await this.getThreads();
19✔
941
        for (let thread of threads) {
19✔
942
            if (thread.threadId === threadId) {
19✔
943
                return thread;
18✔
944
            }
945
        }
946
    }
947

948
    public removeAllListeners() {
949
        if (this.emitter) {
×
950
            this.emitter.removeAllListeners();
×
951
        }
952
    }
953

954
    /**
955
     * Indicates whether this class had `.destroy()` called at least once. Mostly used for checking externally to see if
956
     * the whole debug session has been terminated or is in a bad state.
957
     */
958
    public isDestroyed = false;
21✔
959
    /**
960
     * Disconnect from the telnet session and unset all objects
961
     */
962
    public async destroy() {
963
        this.isDestroyed = true;
×
964

965
        // destroy the debug client if it's defined
966
        if (this.client) {
×
967
            try {
×
968
                await this.client.destroy();
×
969
            } catch (e) {
970
                this.logger.error(e);
×
971
            }
972
        }
973

974
        try {
×
975
            let shutdownTimeMax = this.options?.shutdownTimeout ?? 10_000;
×
976
            await this.destroyCompileClient(shutdownTimeMax);
×
977
        } catch (e) {
978
            this.logger.error(e);
×
979
        }
980

981
        this.cache = undefined;
×
982
        this.removeAllListeners();
×
983
        this.emitter = undefined;
×
984
    }
985

986
    /**
987
     * Promise that is resolved when the compile client socket is closed
988
     */
989
    private compileClientClosed = defer<void>();
21✔
990
    private isDestroyingCompileClient = false;
21✔
991

992
    private async destroyCompileClient(timeout: number) {
993
        if (this.compileClient && !this.isDestroyingCompileClient) {
×
994
            this.isDestroyingCompileClient = true;
×
995
            this.compileClient?.end();
×
996

997
            //wait for the compileClient to be closed
998
            await Promise.race([
×
999
                this.compileClientClosed.promise,
1000
                util.sleep(timeout)
1001
            ]);
1002

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

1005
            //destroy the compileClient
1006
            this.compileClient?.removeAllListeners();
×
1007
            this.compileClient?.destroy();
×
1008
            this.compileClient = undefined;
×
1009
            this.isDestroyingCompileClient = false;
×
1010
        }
1011
    }
1012

1013
    /**
1014
     * Passes the log level down to the RendezvousTracker and ChanperfTracker
1015
     * @param outputLevel the consoleOutput from the launch config
1016
     */
1017
    public setConsoleOutput(outputLevel: string) {
1018
        this.chanperfTracker.setConsoleOutput(outputLevel);
×
1019
        this.rendezvousTracker.setConsoleOutput(outputLevel);
×
1020
    }
1021

1022
    /**
1023
     * Sends a call to the RendezvousTracker to clear the current rendezvous history
1024
     */
1025
    public clearRendezvousHistory() {
1026
        this.rendezvousTracker.clearHistory();
×
1027
    }
1028

1029
    /**
1030
     * Sends a call to the ChanperfTracker to clear the current chanperf history
1031
     */
1032
    public clearChanperfHistory() {
1033
        this.chanperfTracker.clearHistory();
×
1034
    }
1035

1036
    /**
1037
     * The most recently requested exception breakpoint filters. Stored so we can replay them
1038
     * to the debug protocol client once it connects (the session can send these before the
1039
     * device has launched and the client has finished its handshake).
1040
     */
1041
    private pendingExceptionBreakpointFilters: ExceptionBreakpoint[] | undefined;
1042

1043
    public async setExceptionBreakpoints(filters: ExceptionBreakpoint[]) {
1044
        if (!this.capabilities.supportsExceptionBreakpoints) {
×
1045
            return undefined;
×
1046
        }
1047
        //if the client isn't connected yet, queue the filters for replay on connect
1048
        if (!this.connected) {
×
1049
            this.pendingExceptionBreakpointFilters = filters;
×
1050
            return undefined;
×
1051
        }
1052
        return this.client.setExceptionBreakpoints(filters);
×
1053
    }
1054

1055
    private syncBreakpointsPromise = Promise.resolve();
21✔
1056
    public async syncBreakpoints() {
1057
        this.logger.log('syncBreakpoints()');
42✔
1058
        //wait for the previous sync to finish
1059
        this.syncBreakpointsPromise = this.syncBreakpointsPromise
42✔
1060
            //ignore any errors
1061
            .catch(() => { })
1062
            //run the next sync
1063
            .then(() => this._syncBreakpoints());
42✔
1064

1065
        //return the new promise, which will resolve once our latest `syncBreakpoints()` call is finished
1066
        return this.syncBreakpointsPromise;
42✔
1067
    }
1068

1069
    public async _syncBreakpoints() {
1070
        //we need to actually be connected to the device before we can push breakpoints. We'll get
1071
        //called again once the debug protocol client has connected.
1072
        if (!this.connected) {
32✔
1073
            this.logger.info('Cannot sync breakpoints because the debug protocol client has not connected yet');
2✔
1074
            return;
2✔
1075
        }
1076
        //we can't send breakpoints unless we're stopped (or in a protocol version that supports sending them while running).
1077
        //So...if we're not stopped, quit now. (we'll get called again when the stop event happens)
1078
        if (!this.capabilities.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) {
30✔
1079
            this.logger.info('Cannot sync breakpoints because the debugger', this.capabilities.supportsBreakpointRegistrationWhileRunning ? 'does not support sending breakpoints while running' : 'is not paused');
16!
1080
            return;
16✔
1081
        }
1082

1083
        //compute breakpoint changes since last sync
1084
        const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects());
14✔
1085
        this.logger.log('Syncing breakpoints', diff);
14✔
1086

1087
        if (diff.added.length === 0 && diff.removed.length === 0) {
14✔
1088
            this.logger.debug('No breakpoints to sync');
2✔
1089
            return;
2✔
1090
        }
1091

1092
        //getDiff above can yield to other microtasks. If the protocol client closed while we were
1093
        //awaiting (mid-sync TOCTOU), bail out before dereferencing this.client. The app-exit handler
1094
        //resets the breakpoint baseline so the next reconnect re-pushes any pending changes.
1095
        //See https://github.com/rokucommunity/vscode-brightscript-language/issues/811
1096
        if (!this.client) {
12✔
1097
            this.logger.info('Skipping breakpoint sync because the protocol client closed mid-sync');
2✔
1098
            return;
2✔
1099
        }
1100

1101
        // REMOVE breakpoints (delete these breakpoints from the device)
1102
        if (diff.removed.length > 0) {
10✔
1103
            const response = await this.client.removeBreakpoints(
3✔
1104
                //TODO handle retrying to remove breakpoints that don't have deviceIds yet but might get one in the future
1105
                diff.removed.map(x => x.deviceId).filter(x => typeof x === 'number')
5✔
1106
            );
1107

1108
            if (response.data?.errorCode === ErrorCode.NOT_STOPPED) {
3!
1109
                this.breakpointManager.failedDeletions.push(...diff.removed);
1✔
1110
            }
1111
        }
1112

1113
        if (diff.added.length > 0) {
10✔
1114
            //the removeBreakpoints await above can also yield; re-check before attempting the add
1115
            if (!this.client) {
8!
1116
                this.logger.info('Skipping breakpoint add because the protocol client closed mid-sync');
×
1117
                return;
×
1118
            }
1119
            const breakpointsToSendToDevice = diff.added.map(breakpoint => {
8✔
1120
                const hitCount = parseInt(breakpoint.hitCondition);
11✔
1121
                return {
11✔
1122
                    filePath: breakpoint.pkgPath,
1123
                    lineNumber: breakpoint.line,
1124
                    hitCount: !isNaN(hitCount) ? hitCount : undefined,
11!
1125
                    conditionalExpression: breakpoint.condition,
1126
                    srcHash: breakpoint.srcHash,
1127
                    destHash: breakpoint.destHash,
1128
                    componentLibraryName: breakpoint.componentLibraryName
1129
                };
1130
            });
1131

1132
            //split the list into conditional and non-conditional breakpoints.
1133
            //(TODO we can eliminate this splitting logic once the conditional breakpoints "continue" bug in protocol is fixed)
1134
            const standardBreakpoints: typeof breakpointsToSendToDevice = [];
8✔
1135
            const conditionalBreakpoints: typeof breakpointsToSendToDevice = [];
8✔
1136
            for (const breakpoint of breakpointsToSendToDevice) {
8✔
1137
                if (breakpoint?.conditionalExpression?.trim()) {
11!
1138
                    conditionalBreakpoints.push(breakpoint);
1✔
1139
                } else {
1140
                    standardBreakpoints.push(breakpoint);
10✔
1141
                }
1142
            }
1143
            for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) {
8✔
1144
                const response = await this.client.addBreakpoints(breakpoints);
16✔
1145

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

1151
                        if (typeof deviceBreakpoint?.id === 'number') {
9!
1152
                            //sync this breakpoint's deviceId with the roku-assigned breakpoint ID
1153
                            this.breakpointManager.setBreakpointDeviceId(
9✔
1154
                                breakpoints[i].srcHash,
1155
                                breakpoints[i].destHash,
1156
                                deviceBreakpoint.id
1157
                            );
1158
                        }
1159

1160
                        //this breakpoint had an issue. remove it from the client
1161
                        if (deviceBreakpoint.errorCode !== ErrorCode.OK) {
9✔
1162
                            this.breakpointManager.deleteBreakpoint(breakpoints[i].srcHash);
1✔
1163
                        }
1164
                    }
1165
                    //the entire response was bad. delete these breakpoints from the client
1166
                } else {
1167
                    this.breakpointManager.deleteBreakpoints(
2✔
1168
                        breakpoints.map(x => x.srcHash)
2✔
1169
                    );
1170
                }
1171
            }
1172
        }
1173
    }
1174

1175
    public isTelnetAdapter(): this is TelnetAdapter {
1176
        return false;
×
1177
    }
1178

1179
    public isDebugProtocolAdapter(): this is DebugProtocolAdapter {
1180
        return true;
×
1181
    }
1182
}
1183

1184
export interface StackFrame {
1185
    frameId: number;
1186
    frameIndex: number;
1187
    threadIndex: number;
1188
    filePath: string;
1189
    lineNumber: number;
1190
    functionIdentifier: string;
1191
}
1192

1193
export enum EventName {
2✔
1194
    suspend = 'suspend'
2✔
1195
}
1196

1197
export interface EvaluateContainer {
1198
    name: string;
1199
    evaluateName: string;
1200
    type: string;
1201
    value?: any;
1202
    keyType?: KeyType;
1203
    namedVariables?: number;
1204
    indexedVariables?: number;
1205
    highLevelType?: HighLevelType;
1206
    children: EvaluateContainer[];
1207
    isCustom?: boolean;
1208
    evaluateNow?: boolean;
1209
    presentationHint?: DebugProtocol.VariablePresentationHint;
1210
}
1211

1212
export enum KeyType {
2✔
1213
    string = 'String',
2✔
1214
    integer = 'Integer',
2✔
1215
    legacy = 'Legacy'
2✔
1216
}
1217

1218
export interface Thread {
1219
    isSelected: boolean;
1220
    isDetached?: boolean;
1221
    /**
1222
     * The 1-based line number for the thread
1223
     */
1224
    lineNumber: number;
1225
    filePath: string;
1226
    functionName: string;
1227
    lineContents: string;
1228
    threadId: number;
1229
    osThreadId?: string;
1230
    name?: string;
1231
    type?: string;
1232
}
1233

1234
interface BrightScriptRuntimeError {
1235
    message: string;
1236
    errorCode: string;
1237
}
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