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

rokucommunity / roku-debug / 26120672946

19 May 2026 07:37PM UTC coverage: 70.727% (+0.7%) from 70.049%
26120672946

Pull #351

github

web-flow
Merge eb0b2e542 into 5bbd82240
Pull Request #351: 0.23.8

3328 of 5046 branches covered (65.95%)

Branch coverage included in aggregate %.

5834 of 7908 relevant lines covered (73.77%)

35.01 hits per line

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

58.16
/src/adapters/DebugProtocolAdapter.ts
1
import * as EventEmitter from 'events';
2
import { Socket } from 'net';
2✔
3
import { DiagnosticSeverity, util as bscUtil } from 'brighterscript';
2✔
4
import type { BSDebugDiagnostic } from '../CompileErrorProcessor';
2✔
5
import { CompileErrorProcessor } from '../CompileErrorProcessor';
2✔
6
import type { RendezvousHistory, RendezvousTracker } from '../RendezvousTracker';
2✔
7
import type { ChanperfData } from '../ChanperfTracker';
2✔
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';
2✔
13
import type { BreakpointManager } from '../managers/BreakpointManager';
2✔
14
import type { ProjectManager } from '../managers/ProjectManager';
2✔
15
import type { BreakpointsVerifiedEvent, ConstructorOptions, ProtocolVersionDetails } from '../debugProtocol/client/DebugProtocolClient';
2✔
16
import { DebugProtocolClient } from '../debugProtocol/client/DebugProtocolClient';
2✔
17
import { ProtocolCapabilities } from '../debugProtocol/client/ProtocolCapabilities';
18
import type { Variable } from '../debugProtocol/events/responses/VariablesResponse';
19
import { VariableType } from '../debugProtocol/events/responses/VariablesResponse';
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';
17✔
24
import { insertCustomVariables, overrideKeyTypesForCustomVariables } from './customVariableUtils';
17✔
25
import type { DebugProtocol } from '@vscode/debugprotocol';
17✔
26
import { SocketConnectionInUseError } from '../Exceptions';
17✔
27

17✔
28
/**
17✔
29
 * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it.
17✔
30
 */
17✔
31
export class DebugProtocolAdapter {
17✔
32
    constructor(
33
        private options: AdapterOptions & ConstructorOptions,
34
        private projectManager: ProjectManager,
35
        private breakpointManager: BreakpointManager,
17✔
36
        private rendezvousTracker: RendezvousTracker,
37
        private deviceInfo: DeviceInfo
38
    ) {
39
        util.normalizeAdapterOptions(this.options);
40
        this.emitter = new EventEmitter();
17✔
41
        this.chanperfTracker = new ChanperfTracker();
17✔
42
        this.compileErrorProcessor = new CompileErrorProcessor();
17✔
43
        this.connected = false;
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);
47

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

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

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

63
    /**
17✔
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;
69
    }
70

71
    /**
72
     * Indicates whether the adapter has successfully established a connection with the device
73
     */
38!
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;
82

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

12✔
86
    /**
12✔
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

24!
93
    /**
24✔
94
     * Subscribe to an event exactly once
95
     * @param eventName
12!
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>;
53✔
101
    public once(eventName: 'app-ready'): Promise<void>;
102
    public once(eventName: 'diagnostics'): Promise<BSDebugDiagnostic>;
53!
103
    public once(eventName: 'connected'): Promise<boolean>;
53✔
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) => {
114
            const disconnect = this.on(eventName as Parameters<DebugProtocolAdapter['on']>[0], (...args) => {
115
                disconnect();
116
                resolve(...args);
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);
12!
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);
144
        return () => {
145
            this.emitter?.removeListener(eventName, handler);
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(() => {
×
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) {
×
159
                this.emitter.emit(eventName, data);
×
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 {
50!
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;
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;
12✔
197

12✔
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() {
12✔
208
        if (this.isActivated && this.isAppRunning) {
12!
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');
12✔
216
            }
12✔
217
        }
218
    }
219

12✔
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;
12✔
229
            let logs = '';
12!
230

12✔
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);
12✔
243
            }
244

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

12✔
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);
12✔
257
            client.on('closed', startTimeout);
12✔
258
        });
12✔
259
    }
260

12✔
261
    public get isAtDebuggerPrompt() {
×
262
        return this.client?.isStopped ?? false;
×
263
    }
264

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

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

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

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

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

292
    public async createDebugProtocolClient() {
293
        let deferred = defer();
294
        if (this.client) {
295
            await Promise.race([
296
                util.sleep(2000),
297
                await this.client.destroy()
×
298
            ]);
299
            this.client = undefined;
12✔
300
        }
12✔
301
        this.client = new DebugProtocolClient(this.options);
12✔
302
        try {
12✔
303
            // Emit IO from the debugger.
12✔
304
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
12✔
305
            this.client.on('io-output', async (responseText) => {
12✔
306
                if (typeof responseText === 'string') {
307
                    responseText = this.chanperfTracker.processLog(responseText);
308
                    responseText = await this.rendezvousTracker.processLog(responseText);
309
                    this.emit('unhandled-console-output', responseText);
12✔
310
                    this.emit('console-output', responseText);
311
                }
12!
312
            });
×
313

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

×
326
            });
×
327

×
328
            // Listen for the close event
329
            this.client.on('close', () => {
330
                this.emit('close');
331
                this.beginAppExit();
332
                void this.client?.destroy();
333
                this.client = undefined;
334
            });
×
335

336
            // Listen for the app exit event
337
            this.client.on('app-exit', () => {
338
                this.emit('app-exit');
339
                void this.client?.destroy();
340
                this.client = undefined;
1✔
341
            });
342

343
            this.client.on('suspend', (data) => {
1!
344
                this.clearCache();
×
345
                this.emit('suspend');
346
            });
1✔
347

1✔
348
            this.client.on('runtime-error', (data) => {
1✔
349
                console.debug('hasRuntimeError!!', data);
1✔
350
                this.emit('runtime-error', <BrightScriptRuntimeError>{
1✔
351
                    message: data.data.stopReasonDetail,
1✔
352
                    errorCode: data.data.stopReason
×
353
                });
×
354
            });
355

1✔
356
            this.client.on('cannot-continue', () => {
×
357
                this.emit('cannot-continue');
358
            });
359

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

1✔
364
                //mark the breakpoints as verified
365
                for (let breakpoint of event?.breakpoints ?? []) {
1✔
366
                    const success = this.breakpointManager.verifyBreakpoint(breakpoint.id, true);
1✔
367
                    if (!success) {
×
368
                        unverifiableDeviceIds.push(breakpoint.id);
369
                    }
1✔
370
                }
1✔
371
                //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
1✔
372
                if (unverifiableDeviceIds.length > 0) {
1✔
373
                    this.logger.warn('Could not find breakpoints to verify. Removing from device:', { deviceBreakpointIds: unverifiableDeviceIds });
1!
374
                    void this.client.removeBreakpoints(unverifiableDeviceIds);
×
375
                }
376
                this.emit('breakpoints-verified', event);
377
            });
378

379
            this.client.on('compile-error', (update) => {
1✔
380
                let diagnostics: BSDebugDiagnostic[] = [];
1✔
381
                diagnostics.push({
×
382
                    path: update.data.filePath,
×
383
                    range: bscUtil.createRange(update.data.lineNumber - 1, 0, update.data.lineNumber - 1, 999),
×
384
                    message: update.data.errorMessage,
385
                    severity: DiagnosticSeverity.Error,
×
386
                    code: undefined
×
387
                });
388
                this.emit('diagnostics', diagnostics);
×
389
            });
×
390

×
391
            await this.client.connect();
×
392

393
            this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected });
394
            this.connected = true;
×
395
            this.isAppRunning = true;
396
            this.handleStartupIfReady();
397
            this.emit('connected', this.connected);
1✔
398
            this.emit('app-ready');
1✔
399
            //flush any breakpoints that were queued while we were waiting for the client to connect.
1✔
400
            //setBreakpointsRequest is called by VS Code before the channel is uploaded, so the initial
401
            //sync bails out (no client yet); this re-sync pushes those queued breakpoints to the device.
402
            void this.syncBreakpoints();
1✔
403
            //also replay any queued exception breakpoint filters
404
            if (this.pendingExceptionBreakpointFilters) {
405
                const queuedFilters = this.pendingExceptionBreakpointFilters;
×
406
                this.pendingExceptionBreakpointFilters = undefined;
407
                void this.client.setExceptionBreakpoints(queuedFilters);
1✔
408
            }
409

410
            //the adapter is connected and running smoothly. resolve the promise
×
411
            deferred.resolve();
×
412
        } catch (e) {
×
413
            deferred.reject(e);
×
414
        }
415
        return deferred.promise;
416
    }
417

418
    private beginAppExit() {
419
        this.compileErrorProcessor.compileErrorTimer = setTimeout(() => {
420
            this.isAppRunning = false;
421
            this.emit('app-exit');
×
422
        }, 200);
×
423
    }
424

425
    /**
×
426
     * Determines if the current version of the debug protocol supports emitting compile error updates.
×
427
     */
428
    public get supportsCompileErrorReporting() {
429
        return this.capabilities.supportsCompileErrorReporting;
×
430
    }
×
431

432
    /**
433
     * Indicate if virtual variables should be auto resolved when they are encountered.
434
     */
435
    public get autoResolveVirtualVariables() {
436
        return this.options.autoResolveVirtualVariables;
×
437
    }
×
438

439
    private processingTelnetOutput = false;
440
    public async processTelnetOutput() {
441
        if (this.processingTelnetOutput) {
442
            return;
443
        }
×
444
        this.processingTelnetOutput = true;
445

×
446
        let deferred = defer();
447
        try {
448
            this.compileClient = new Socket({ allowHalfOpen: false });
449
            util.registerSocketLogging(this.compileClient, this.logger, 'CompileClient');
450

451
            this.compileErrorProcessor.on('diagnostics', (errors) => {
12✔
452
                this.compileClient.end();
12✔
453
                this.emit('diagnostics', errors);
454
            });
455

456
            this.compileErrorProcessor.on('launch-status', (message) => {
457
                this.emit('launch-status', message);
458
            });
459

×
460
            //if the connection fails, reject the connect promise.
461
            //Use tryReject (not reject) because this handler persists for the socket's lifetime.
×
462
            //After a successful connection the deferred is already resolved, so a post-connection
×
463
            //socket error (e.g. ECONNRESET on device disconnect) must not crash the process.
×
464
            this.compileClient.on('error', (err) => {
465
                deferred.tryReject(new Error(`Error with connection to: ${this.options.host}:${this.options.brightScriptConsolePort} \n\n ${err.message} `));
×
466
            });
×
467
            this.logger.info('Connecting via telnet to gather compile info', { host: this.options.host, port: this.options.brightScriptConsolePort });
×
468
            this.compileClient.connect(this.options.brightScriptConsolePort, this.options.host, () => {
469
                this.logger.log(`CONNECTED via telnet to gather compile info`, { host: this.options.host, port: this.options.brightScriptConsolePort });
×
470
            });
×
471

×
472
            this.logger.debug('Waiting for the compile client to settle');
×
473
            const settledLogs = await this.settleCompileClient(this.compileClient);
×
474
            this.logger.debug('Compile client has settled');
475
            this.logger.trace('Settled logs:', settledLogs);
476

477
            if (settledLogs.trim().startsWith('Console connection is already in use.')) {
478
                throw new SocketConnectionInUseError(`Telnet connection ${this.options.host}:${this.options.brightScriptConsolePort} already is use`, {
479
                    port: this.options.brightScriptConsolePort,
×
480
                    host: this.options.host
×
481
                });
×
482
            }
×
483

484
            let lastPartialLine = '';
×
485
            this.compileClient.on('data', (buffer) => {
×
486
                let responseText = buffer.toString();
487
                this.logger.info('CompileClient received data', { responseText });
488

489
                let logResult = util.handleLogFragments(lastPartialLine, buffer.toString());
490

491
                // Save any remaining partial line for the next event
×
492
                lastPartialLine = logResult.remaining;
493
                if (logResult.completed) {
494
                    // Emit the completed io string.
495
                    this.findWaitForDebuggerPrompt(logResult.completed);
496
                    this.compileErrorProcessor.processUnhandledLines(logResult.completed);
497
                    this.logger.debug('CompileClient data:', logResult.completed);
×
498
                    this.emit('unhandled-console-output', logResult.completed);
16!
499
                } else {
×
500
                    this.logger.debug('CompileClient buffer was split:', lastPartialLine);
501
                }
16✔
502
            });
503

504
            this.compileClient.on('close', () => {
15✔
505
                this.logger.log('compileClient socket closed');
15✔
506
                this.compileClientClosed.tryResolve();
15✔
507
            });
508

15✔
509
            // connected to telnet. resolve the promise
2✔
510
            deferred.resolve();
2✔
511
        } catch (e) {
512
            deferred.reject(e);
13✔
513
        }
12✔
514
        return deferred.promise;
12✔
515
    }
516

517
    private findWaitForDebuggerPrompt(responseText: string) {
518
        let lines = responseText.split(/\r?\n/g);
519
        for (const line of lines) {
520
            if (/Waiting for debugger on \d+\.\d+\.\d+\.\d+:8081/g.exec(line)) {
521
                this.emit('waiting-for-debugger');
522
            }
24!
523
        }
524
    }
12✔
525

12✔
526
    /**
527
     * Send command to step over
528
     */
13✔
529
    public async stepOver(threadId: number) {
12!
530
        this.clearCache();
12!
531
        return this.client.stepOver(threadId);
532
    }
13✔
533

534
    public async stepInto(threadId: number) {
535
        this.clearCache();
536
        return this.client.stepIn(threadId);
2✔
537
    }
538

539
    public async stepOut(threadId: number) {
12✔
540
        this.clearCache();
541
        return this.client.stepOut(threadId);
542
    }
543

544
    /**
545
     * Tell the brightscript program to continue (i.e. resume program)
546
     */
2✔
547
    public async continue() {
2✔
548
        this.clearCache();
2!
549
        return this.client.continue();
×
550
    }
551

2✔
552
    /**
2!
553
     * Tell the brightscript program to pause (fall into debug mode)
×
554
     */
555
    public async pause() {
2✔
556
        this.clearCache();
2✔
557
        //send the kill signal, which breaks into debugger mode
558
        return this.client.pause();
2!
559
    }
×
560

561
    /**
2✔
562
     * Clears the state, which means that everything will be retrieved fresh next time it is requested
2!
563
     */
564
    public clearCache() {
×
565
        this.cache = {};
×
566
        this.stackFramesCache = {};
×
567
    }
568

2✔
569
    /**
570
     * Execute a command directly on the roku. Returns the output of the command
571
     * @param command
572
     * @returns the output of the command (if possible)
573
     */
574
    public async evaluate(command: string, frameId: number = this.client.primaryThread): Promise<RokuAdapterEvaluateResponse> {
575
        if (this.capabilities.supportsExecuteCommand) {
1✔
576
            if (!this.isAtDebuggerPrompt) {
1!
577
                throw new Error('Cannot run evaluate: debugger is not paused');
1✔
578
            }
579

580
            let stackFrame = this.getStackFrameById(frameId);
581
            if (!stackFrame) {
582
                throw new Error('Cannot execute command without a corresponding frame');
1✔
583
            }
1✔
584
            this.logger.log('evaluate ', { command, frameId });
1✔
585

586
            const response = await this.client.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex);
587
            this.logger.info('evaluate response', { command, response });
588
            if (response.data.executeSuccess) {
589
                return {
590
                    message: undefined,
591
                    type: 'message'
592
                };
1✔
593
            } else {
1!
594
                const messages = [
595
                    ...response?.data?.compileErrors ?? [],
1✔
596
                    ...response?.data?.runtimeErrors ?? [],
597
                    ...response?.data?.otherErrors ?? []
598
                ];
599
                return {
600
                    message: messages[0] ?? 'Unknown error executing command',
601
                    type: 'error'
602
                };
603
            }
604
        } else {
605
            return {
606
                message: `Execute commands are not supported on debug protocol: ${this.activeProtocolVersion}, v3.0.0 or greater is required.`,
607
                type: 'error'
608
            };
609
        }
610
    }
1✔
611

1✔
612
    public async getStackTrace(threadIndex: number = this.client.primaryThread) {
1✔
613
        if (!this.isAtDebuggerPrompt) {
614
            throw new Error('Cannot get stack trace: debugger is not paused');
615
        }
616
        return this.resolve(`stack trace for thread ${threadIndex}`, async () => {
617
            let thread = await this.getThreadByThreadId(threadIndex);
618
            let frames: StackFrame[] = [];
619
            let stackTraceData = await this.client.getStackTrace(threadIndex);
620

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

×
647
            return frames;
648
        });
10✔
649
    }
650

7✔
651
    public getStackFrameById(frameId: number): StackFrame {
652
        return this.stackFramesCache[frameId];
653
    }
654

10✔
655
    private cleanUpFunctionName(functionName): string {
6✔
656
        return functionName.substring(functionName.lastIndexOf('@') + 1);
657
    }
4!
658

×
659
    /**
660
     * Get info about the specified variable.
4✔
661
     * @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
3✔
662
     */
663
    private async getVariablesResponse(expression: string, frameId: number) {
1!
664
        const logger = this.logger.createLogger('[getVariable]');
1✔
665
        logger.info('begin', { expression });
666
        if (!this.isAtDebuggerPrompt) {
10✔
667
            throw new Error('Cannot resolve variable: debugger is not paused');
60✔
668
        }
30✔
669

30!
670
        let frame = this.getStackFrameById(frameId);
30!
671
        if (!frame) {
672
            throw new Error('Cannot request variable without a corresponding frame');
673
        }
674

675
        logger.info(`Expression:`, JSON.stringify(expression));
676
        let variablePath = expression === '' ? [] : util.getVariablePath(expression);
677

678
        // Temporary workaround related to casing issues over the protocol
679
        if (this.capabilities.enableVariablesLowerCaseRetry && variablePath?.length > 0) {
680
            variablePath[0] = variablePath[0].toLowerCase();
681
        }
10✔
682

10✔
683
        let response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
2!
684

685
        if (this.capabilities.enableVariablesLowerCaseRetry && response.data.errorCode !== ErrorCode.OK) {
2✔
686
            // Temporary workaround related to casing issues over the protocol
687
            logger.log(`Retrying expression as lower case:`, expression);
8✔
688
            variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase());
689
            response = await this.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex);
690
        }
5✔
691
        return response;
692
    }
693

10✔
694
    /**
4✔
695
     * Get the variable for the specified expression.
4✔
696
     */
4✔
697
    public async getVariable(expression: string, frameId: number): Promise<EvaluateContainer> {
4✔
698
        const response = await this.getVariablesResponse(expression, frameId);
6✔
699

6!
700
        if (Array.isArray(response?.data?.variables)) {
×
701
            const container = this.createEvaluateContainer(
702
                response.data.variables[0],
6✔
703
                //the name of the top container is the expression itself
6✔
704
                expression,
705
                //this is the top-level container, so there are no parent keys to this entry
706
                undefined
707
            );
10!
708
            await insertCustomVariables(this, expression, container);
×
709
            container.namedVariables = container.children.length - container.indexedVariables;
×
710
            return container;
711
        }
×
712
    }
713

10✔
714
    /**
715
     * Get the list of local variables
716
     */
717
    public async getLocalVariables(frameId: number) {
718
        const response = await this.getVariablesResponse('', frameId);
719

720
        if (response?.data?.errorCode === ErrorCode.OK && Array.isArray(response?.data?.variables)) {
721
            //create a top-level container to hold all the local vars
31✔
722
            const container = this.createEvaluateContainer(
4✔
723
                //dummy data
4✔
724
                {
725
                    isConst: false,
27✔
726
                    isContainer: true,
27✔
727
                    keyType: VariableType.String,
728
                    refCount: undefined,
729
                    type: VariableType.AssociativeArray,
730
                    value: undefined,
731
                    children: response.data.variables
732
                },
15!
733
                //no name, this is a dummy container
×
734
                undefined,
735
                //there's no parent path
15✔
736
                undefined
737
            );
12✔
738
            container.indexedVariables = 0;
739
            container.namedVariables = container.children.length;
740
            return container;
741
        }
742
    }
12✔
743

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

761
        if (variableType === VariableType.SubtypedObject) {
762
            //subtyped objects can only have string values
763
            let parts = (variable.value as string).split('; ');
764
            // Pull the primary type from the value.
765
            (variableType as string) = parts[0];
766

767
            // Format the value to be more readable in the UI.
768
            // Example: `roSGNode; Group` = `roSGNode (Group)`
769
            (value as string) = `${parts[0]}(${parts[1]})`;
770
        } else if (variableType === VariableType.Object || variableType === VariableType.Interface) {
771
            // We want the type to reflect `roAppInfo` or `roDeviceInfo` for example in the UI
12✔
772
            // so set the type to be the value from the device
773
            variableType = value;
774
        } else if (variableType === VariableType.AssociativeArray) {
12✔
775
            // We want the type to reflect `function` in the UI
×
776
            value = VariableType.AssociativeArray;
777
        }
12✔
778

779
        //build full evaluate name for this var. (i.e. `alpha["beta"]` + ["charlie"]` === `alpha["beta"]["charlie"]`)
780
        let evaluateName: string;
781
        if (!parentEvaluateName?.trim()) {
15✔
782
            evaluateName = name?.toString();
15✔
783
        } else if (variable.isVirtual) {
15✔
784
            evaluateName = `${parentEvaluateName}.${name}`;
14✔
785
        } else if (typeof name === 'string') {
786
            evaluateName = `${parentEvaluateName}["${name}"]`;
787
        } else if (typeof name === 'number') {
788
            evaluateName = `${parentEvaluateName}[${name}]`;
789
        }
×
790

×
791
        let container: EvaluateContainer = {
792
            name: name?.toString() ?? '',
793
            evaluateName: evaluateName ?? '',
794
            type: variableType ?? '',
795
            value: value ?? null,
796
            highLevelType: undefined,
797
            //non object/array variables don't have a key type
798
            keyType: variable.keyType as unknown as KeyType,
×
799
            namedVariables: 0,
800
            indexedVariables: 0,
×
801
            //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
×
802
            children: []
×
803
        };
804

805
        // In preparation for adding custom variables some variables need to be marked
×
806
        // as keyable/container like even thought they are not on device.
807
        overrideKeyTypesForCustomVariables(this, container);
808

×
809
        if (container.keyType === KeyType.integer) {
×
810
            container.indexedVariables = variable.childCount ?? variable.children?.length ?? undefined;
×
811
            // We do not know how many named variables there are, if any, so we will always tell the DAP client to ask for them
812
            container.namedVariables = 1;
813
        } else if (container.keyType === KeyType.string) {
×
814
            // container.namedVariables = variable.childCount ?? variable.children?.length ?? undefined;
815
            // Force one so DAP client always asks for all named vars
×
816
            container.namedVariables = 1;
×
817
        }
×
818

819
        //recursively generate children containers
820
        if ([KeyType.integer, KeyType.string].includes(container.keyType) && Array.isArray(variable.children)) {
821
            container.children = [];
×
822

×
823
            container.namedVariables = 0;
×
824
            container.indexedVariables = 0;
825

×
826
            for (let i = 0; i < variable.children.length; i++) {
827
                const childVariable = variable.children[i];
828
                if (childVariable.name === undefined) {
829
                    container.indexedVariables++;
×
830
                }
831
                const childContainer = this.createEvaluateContainer(
×
832
                    childVariable,
×
833
                    container.keyType === KeyType.integer && !childVariable.isVirtual ? i : childVariable.name,
×
834
                    container.evaluateName
×
835
                );
836
                container.children.push(childContainer);
837
            }
838
        }
839

840
        //show virtual variables in the UI
841
        if (variable.isVirtual) {
842
            if (!container.presentationHint) {
×
843
                container.presentationHint = {};
×
844
            }
845
            container.presentationHint.kind = 'virtual';
846
        }
847

848
        return container;
849
    }
×
850

851
    /**
852
     * Cache items by a unique key
853
     * @param expression
854
     * @param factory
855
     */
×
856
    private resolve<T>(key: string, factory: () => T | Thenable<T>): Promise<T> {
857
        if (this.cache[key]) {
858
            this.logger.log('return cashed response', key, this.cache[key]);
×
859
            return this.cache[key];
×
860
        }
861
        this.cache[key] = Promise.resolve<T>(factory());
862
        return this.cache[key];
×
863
    }
×
864

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

893
            for (let i = 0; i < (threadsResponse.data?.threads?.length ?? 0); i++) {
894
                let threadInfo = threadsResponse.data.threads[i];
10✔
895
                let thread = <Thread>{
10✔
896
                    // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
10✔
897
                    // NOTE: Rely on the thead index from the threads update event.
2✔
898
                    isSelected: this.client.primaryThread === i,
2✔
899
                    // isSelected: threadInfo.isPrimary,
900
                    isDetached: threadInfo.isDetached,
901
                    filePath: threadInfo.filePath,
8✔
902
                    functionName: threadInfo.functionName,
3✔
903
                    lineNumber: threadInfo.lineNumber, //threadInfo.lineNumber is 1-based. Thread requires 1-based line numbers
904
                    lineContents: threadInfo.codeSnippet,
5✔
905
                    threadId: i
3!
906
                };
1✔
907
                threads.push(thread);
908
            }
909
            //make sure the selected thread is at the top
8✔
910
            threads.sort((a, b) => {
6✔
911
                return a.isSelected ? -1 : 1;
8✔
912
            });
8✔
913

914
            return threads;
915
        });
8!
916
    }
917

918
    private async getThreadByThreadId(threadId: number) {
919
        let threads = await this.getThreads();
920
        for (let thread of threads) {
921
            if (thread.threadId === threadId) {
922
                return thread;
923
            }
924
        }
6✔
925
    }
6✔
926

6✔
927
    public removeAllListeners() {
8!
928
        if (this.emitter) {
1✔
929
            this.emitter.removeAllListeners();
930
        }
931
    }
7✔
932

933
    /**
934
     * Indicates whether this class had `.destroy()` called at least once. Mostly used for checking externally to see if
6✔
935
     * the whole debug session has been terminated or is in a bad state.
12✔
936
     */
937
    public isDestroyed = false;
12!
938
    /**
10!
939
     * Disconnect from the telnet session and unset all objects
6✔
940
     */
6!
941
    public async destroy() {
942
        this.isDestroyed = true;
6✔
943

944
        // destroy the debug client if it's defined
945
        if (this.client) {
6✔
946
            try {
1✔
947
                await this.client.destroy();
948
            } catch (e) {
949
                this.logger.error(e);
950
            }
951
        }
952

2✔
953
        try {
954
            let shutdownTimeMax = this.options?.shutdownTimeout ?? 10_000;
955
            await this.destroyCompileClient(shutdownTimeMax);
956
        } catch (e) {
957
            this.logger.error(e);
958
        }
×
959

960
        this.cache = undefined;
961
        this.removeAllListeners();
×
962
        this.emitter = undefined;
963
    }
964

2✔
965
    /**
966
     * Promise that is resolved when the compile client socket is closed
2✔
967
     */
2✔
968
    private compileClientClosed = defer<void>();
4✔
969
    private isDestroyingCompileClient = false;
970

2✔
971
    private async destroyCompileClient(timeout: number) {
2✔
972
        if (this.compileClient && !this.isDestroyingCompileClient) {
2✔
973
            this.isDestroyingCompileClient = true;
2✔
974
            this.compileClient?.end();
4✔
975

976
            //wait for the compileClient to be closed
977
            await Promise.race([
978
                this.compileClientClosed.promise,
979
                util.sleep(timeout)
980
            ]);
981

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

984
            //destroy the compileClient
985
            this.compileClient?.removeAllListeners();
986
            this.compileClient?.destroy();
987
            this.compileClient = undefined;
988
            this.isDestroyingCompileClient = false;
989
        }
990
    }
991

992
    /**
993
     * Passes the log level down to the RendezvousTracker and ChanperfTracker
994
     * @param outputLevel the consoleOutput from the launch config
995
     */
996
    public setConsoleOutput(outputLevel: string) {
997
        this.chanperfTracker.setConsoleOutput(outputLevel);
998
        this.rendezvousTracker.setConsoleOutput(outputLevel);
999
    }
1000

1001
    /**
1002
     * Sends a call to the RendezvousTracker to clear the current rendezvous history
1003
     */
1004
    public clearRendezvousHistory() {
1005
        this.rendezvousTracker.clearHistory();
1006
    }
1007

1008
    /**
1009
     * Sends a call to the ChanperfTracker to clear the current chanperf history
1010
     */
1011
    public clearChanperfHistory() {
1012
        this.chanperfTracker.clearHistory();
1013
    }
1014

1015
    /**
1016
     * The most recently requested exception breakpoint filters. Stored so we can replay them
1017
     * to the debug protocol client once it connects (the session can send these before the
1018
     * device has launched and the client has finished its handshake).
1019
     */
1020
    private pendingExceptionBreakpointFilters: ExceptionBreakpoint[] | undefined;
1021

1022
    public async setExceptionBreakpoints(filters: ExceptionBreakpoint[]) {
1023
        if (!this.capabilities.supportsExceptionBreakpoints) {
1024
            return undefined;
1025
        }
1026
        //if the client isn't connected yet, queue the filters for replay on connect
1027
        if (!this.connected) {
1028
            this.pendingExceptionBreakpointFilters = filters;
1029
            return undefined;
1030
        }
1031
        return this.client.setExceptionBreakpoints(filters);
1032
    }
1033

1034
    private syncBreakpointsPromise = Promise.resolve();
1035
    public async syncBreakpoints() {
1036
        this.logger.log('syncBreakpoints()');
1037
        //wait for the previous sync to finish
1038
        this.syncBreakpointsPromise = this.syncBreakpointsPromise
1039
            //ignore any errors
1040
            .catch(() => { })
1041
            //run the next sync
1042
            .then(() => this._syncBreakpoints());
1043

1044
        //return the new promise, which will resolve once our latest `syncBreakpoints()` call is finished
1045
        return this.syncBreakpointsPromise;
1046
    }
1047

1048
    public async _syncBreakpoints() {
1049
        //we need to actually be connected to the device before we can push breakpoints. We'll get
1050
        //called again once the debug protocol client has connected.
1051
        if (!this.connected) {
1052
            this.logger.info('Cannot sync breakpoints because the debug protocol client has not connected yet');
1053
            return;
1054
        }
1055
        //we can't send breakpoints unless we're stopped (or in a protocol version that supports sending them while running).
1056
        //So...if we're not stopped, quit now. (we'll get called again when the stop event happens)
1057
        if (!this.capabilities.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) {
1058
            this.logger.info('Cannot sync breakpoints because the debugger', this.capabilities.supportsBreakpointRegistrationWhileRunning ? 'does not support sending breakpoints while running' : 'is not paused');
1059
            return;
1060
        }
1061

1062
        //compute breakpoint changes since last sync
1063
        const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects());
1064
        this.logger.log('Syncing breakpoints', diff);
1065

1066
        if (diff.added.length === 0 && diff.removed.length === 0) {
1067
            this.logger.debug('No breakpoints to sync');
1068
            return;
1069
        }
1070

1071
        // REMOVE breakpoints (delete these breakpoints from the device)
1072
        if (diff.removed.length > 0) {
1073
            const response = await this.client.removeBreakpoints(
1074
                //TODO handle retrying to remove breakpoints that don't have deviceIds yet but might get one in the future
1075
                diff.removed.map(x => x.deviceId).filter(x => typeof x === 'number')
1076
            );
1077

1078
            if (response.data?.errorCode === ErrorCode.NOT_STOPPED) {
1079
                this.breakpointManager.failedDeletions.push(...diff.removed);
1080
            }
1081
        }
1082

1083
        if (diff.added.length > 0) {
1084
            const breakpointsToSendToDevice = diff.added.map(breakpoint => {
1085
                const hitCount = parseInt(breakpoint.hitCondition);
1086
                return {
1087
                    filePath: breakpoint.pkgPath,
1088
                    lineNumber: breakpoint.line,
1089
                    hitCount: !isNaN(hitCount) ? hitCount : undefined,
1090
                    conditionalExpression: breakpoint.condition,
1091
                    srcHash: breakpoint.srcHash,
1092
                    destHash: breakpoint.destHash,
1093
                    componentLibraryName: breakpoint.componentLibraryName
1094
                };
1095
            });
1096

1097
            //split the list into conditional and non-conditional breakpoints.
1098
            //(TODO we can eliminate this splitting logic once the conditional breakpoints "continue" bug in protocol is fixed)
1099
            const standardBreakpoints: typeof breakpointsToSendToDevice = [];
1100
            const conditionalBreakpoints: typeof breakpointsToSendToDevice = [];
1101
            for (const breakpoint of breakpointsToSendToDevice) {
1102
                if (breakpoint?.conditionalExpression?.trim()) {
1103
                    conditionalBreakpoints.push(breakpoint);
1104
                } else {
1105
                    standardBreakpoints.push(breakpoint);
1106
                }
1107
            }
1108
            for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) {
1109
                const response = await this.client.addBreakpoints(breakpoints);
1110

1111
                //if the response was successful, and we have the correct number of breakpoints in the response
1112
                if (response.data.errorCode === ErrorCode.OK && response?.data?.breakpoints?.length === breakpoints.length) {
1113
                    for (let i = 0; i < (response?.data?.breakpoints?.length ?? 0); i++) {
1114
                        const deviceBreakpoint = response.data.breakpoints[i];
1115

1116
                        if (typeof deviceBreakpoint?.id === 'number') {
1117
                            //sync this breakpoint's deviceId with the roku-assigned breakpoint ID
1118
                            this.breakpointManager.setBreakpointDeviceId(
1119
                                breakpoints[i].srcHash,
1120
                                breakpoints[i].destHash,
1121
                                deviceBreakpoint.id
1122
                            );
1123
                        }
1124

1125
                        //this breakpoint had an issue. remove it from the client
1126
                        if (deviceBreakpoint.errorCode !== ErrorCode.OK) {
1127
                            this.breakpointManager.deleteBreakpoint(breakpoints[i].srcHash);
1128
                        }
1129
                    }
1130
                    //the entire response was bad. delete these breakpoints from the client
1131
                } else {
1132
                    this.breakpointManager.deleteBreakpoints(
1133
                        breakpoints.map(x => x.srcHash)
1134
                    );
1135
                }
1136
            }
1137
        }
1138
    }
1139

1140
    public isTelnetAdapter(): this is TelnetAdapter {
1141
        return false;
1142
    }
1143

1144
    public isDebugProtocolAdapter(): this is DebugProtocolAdapter {
1145
        return true;
1146
    }
1147
}
1148

1149
export interface StackFrame {
1150
    frameId: number;
1151
    frameIndex: number;
1152
    threadIndex: number;
1153
    filePath: string;
1154
    lineNumber: number;
1155
    functionIdentifier: string;
1156
}
1157

1158
export enum EventName {
1159
    suspend = 'suspend'
1160
}
1161

1162
export interface EvaluateContainer {
1163
    name: string;
1164
    evaluateName: string;
1165
    type: string;
1166
    value?: any;
1167
    keyType?: KeyType;
1168
    namedVariables?: number;
1169
    indexedVariables?: number;
1170
    highLevelType?: HighLevelType;
1171
    children: EvaluateContainer[];
1172
    isCustom?: boolean;
1173
    evaluateNow?: boolean;
1174
    presentationHint?: DebugProtocol.VariablePresentationHint;
1175
}
1176

1177
export enum KeyType {
1178
    string = 'String',
1179
    integer = 'Integer',
1180
    legacy = 'Legacy'
1181
}
1182

1183
export interface Thread {
1184
    isSelected: boolean;
1185
    isDetached?: boolean;
1186
    /**
1187
     * The 1-based line number for the thread
1188
     */
1189
    lineNumber: number;
1190
    filePath: string;
1191
    functionName: string;
1192
    lineContents: string;
1193
    threadId: number;
1194
}
1195

1196
interface BrightScriptRuntimeError {
1197
    message: string;
1198
    errorCode: string;
1199
}
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