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

rokucommunity / roku-debug / #2026

02 Nov 2022 02:41PM UTC coverage: 56.194% (+7.0%) from 49.18%
#2026

push

TwitchBronBron
0.17.0

997 of 1898 branches covered (52.53%)

Branch coverage included in aggregate %.

2074 of 3567 relevant lines covered (58.14%)

15.56 hits per line

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

26.79
/src/adapters/DebugProtocolAdapter.ts
1
import type { ConstructorOptions, ProtocolVersionDetails } from '../debugProtocol/Debugger';
2
import { Debugger } from '../debugProtocol/Debugger';
1✔
3
import * as EventEmitter from 'events';
1✔
4
import { Socket } from 'net';
1✔
5
import type { BSDebugDiagnostic } from '../CompileErrorProcessor';
6
import { CompileErrorProcessor } from '../CompileErrorProcessor';
1✔
7
import type { RendezvousHistory } from '../RendezvousTracker';
8
import { RendezvousTracker } from '../RendezvousTracker';
1✔
9
import type { ChanperfData } from '../ChanperfTracker';
10
import { ChanperfTracker } from '../ChanperfTracker';
1✔
11
import type { SourceLocation } from '../managers/LocationManager';
12
import { ERROR_CODES, PROTOCOL_ERROR_CODES, STOP_REASONS } from '../debugProtocol/Constants';
1✔
13
import { defer, util } from '../util';
1✔
14
import { logger } from '../logging';
1✔
15
import * as semver from 'semver';
1✔
16
import type { AdapterOptions, HighLevelType, RokuAdapterEvaluateResponse } from '../interfaces';
17
import type { BreakpointManager } from '../managers/BreakpointManager';
18
import type { ProjectManager } from '../managers/ProjectManager';
19
import { ActionQueue } from '../managers/ActionQueue';
1✔
20

21
/**
22
 * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it.
23
 */
24
export class DebugProtocolAdapter {
1✔
25
    constructor(
26
        private options: AdapterOptions & ConstructorOptions,
2✔
27
        private projectManager: ProjectManager,
2✔
28
        private breakpointManager: BreakpointManager
2✔
29
    ) {
30
        util.normalizeAdapterOptions(this.options);
2✔
31
        this.emitter = new EventEmitter();
2✔
32
        this.chanperfTracker = new ChanperfTracker();
2✔
33
        this.rendezvousTracker = new RendezvousTracker();
2✔
34
        this.compileErrorProcessor = new CompileErrorProcessor();
2✔
35
        this.connected = false;
2✔
36

37
        // watch for chanperf events
38
        this.chanperfTracker.on('chanperf', (output) => {
2✔
39
            this.emit('chanperf', output);
×
40
        });
41

42
        // watch for rendezvous events
43
        this.rendezvousTracker.on('rendezvous', (output) => {
2✔
44
            this.emit('rendezvous', output);
×
45
        });
46
    }
47

48
    private logger = logger.createLogger(`[${DebugProtocolAdapter.name}]`);
2✔
49

50
    /**
51
     * Indicates whether the adapter has successfully established a connection with the device
52
     */
53
    public connected: boolean;
54

55
    /**
56
     *  Due to casing issues with the variables request on some versions of the debug protocol, we first need to try the request in the supplied case.
57
     * If that fails we retry in lower case. This flag is used to drive that logic switching
58
     */
59
    private enableVariablesLowerCaseRetry = true;
2✔
60
    private supportsExecuteCommand: boolean;
61
    private compileClient: Socket;
62
    private compileErrorProcessor: CompileErrorProcessor;
63
    private emitter: EventEmitter;
64
    private chanperfTracker: ChanperfTracker;
65
    private rendezvousTracker: RendezvousTracker;
66
    private socketDebugger: Debugger;
67
    private nextFrameId = 1;
2✔
68

69
    private stackFramesCache: Record<number, StackFrame> = {};
2✔
70
    private cache = {};
2✔
71

72
    /**
73
     * Get the version of the protocol for the Roku device we're currently connected to.
74
     */
75
    public get activeProtocolVersion() {
76
        return this.socketDebugger?.protocolVersion;
×
77
    }
78

79
    public readonly supportsMultipleRuns = false;
2✔
80

81
    /**
82
     * Subscribe to various events
83
     * @param eventName
84
     * @param handler
85
     */
86
    public on(eventName: 'cannot-continue', handler: () => void);
87
    public on(eventname: 'chanperf', handler: (output: ChanperfData) => void);
88
    public on(eventName: 'close', handler: () => void);
89
    public on(eventName: 'app-exit', handler: () => void);
90
    public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void);
91
    public on(eventName: 'connected', handler: (params: boolean) => void);
92
    public on(eventname: 'console-output', handler: (output: string) => void); // TODO: might be able to remove this at some point.
93
    public on(eventname: 'protocol-version', handler: (output: ProtocolVersionDetails) => void);
94
    public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void);
95
    public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void);
96
    public on(eventName: 'suspend', handler: () => void);
97
    public on(eventName: 'start', handler: () => void);
98
    public on(eventname: 'unhandled-console-output', handler: (output: string) => void);
99
    public on(eventName: string, handler: (payload: any) => void) {
100
        this.emitter.on(eventName, handler);
×
101
        return () => {
×
102
            if (this.emitter !== undefined) {
×
103
                this.emitter.removeListener(eventName, handler);
×
104
            }
105
        };
106
    }
107

108
    private emit(eventName: 'suspend');
109
    private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]);
110
    private emit(eventName: 'app-exit' | 'cannot-continue' | 'chanperf' | 'close' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output', data?);
111
    private emit(eventName: string, data?) {
112
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
113
        setTimeout(() => {
×
114
            //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists
115
            if (this.emitter) {
×
116
                this.emitter.emit(eventName, data);
×
117
            }
118
        }, 0);
119
    }
120

121
    /**
122
     * The debugger needs to tell us when to be active (i.e. when the package was deployed)
123
     */
124
    public isActivated = false;
2✔
125

126
    /**
127
     * This will be set to true When the roku emits the [scrpt.ctx.run.enter] text,
128
     * which indicates that the app is running on the Roku
129
     */
130
    public isAppRunning = false;
2✔
131

132
    public activate() {
133
        this.isActivated = true;
×
134
        this.handleStartupIfReady();
×
135
    }
136

137
    public async sendErrors() {
138
        await this.compileErrorProcessor.sendErrors();
×
139
    }
140

141
    private handleStartupIfReady() {
142
        if (this.isActivated && this.isAppRunning) {
×
143
            this.emit('start');
×
144

145
            //if we are already sitting at a debugger prompt, we need to emit the first suspend event.
146
            //If not, then there are probably still messages being received, so let the normal handler
147
            //emit the suspend event when it's ready
148
            if (this.isAtDebuggerPrompt === true) {
×
149
                this.emit('suspend');
×
150
            }
151
        }
152
    }
153

154
    /**
155
     * Wait until the client has stopped sending messages. This is used mainly during .connect so we can ignore all old messages from the server
156
     * @param client
157
     * @param name
158
     * @param maxWaitMilliseconds
159
     */
160
    private settle(client: Socket, name: string, maxWaitMilliseconds = 400) {
×
161
        return new Promise((resolve) => {
×
162
            let callCount = -1;
×
163

164
            function handler() {
165
                callCount++;
×
166
                let myCallCount = callCount;
×
167
                setTimeout(() => {
×
168
                    //if no other calls have been made since the timeout started, then the listener has settled
169
                    if (myCallCount === callCount) {
×
170
                        client.removeListener(name, handler);
×
171
                        resolve(callCount);
×
172
                    }
173
                }, maxWaitMilliseconds);
174
            }
175

176
            client.addListener(name, handler);
×
177
            //call the handler immediately so we have a timeout
178
            handler();
×
179
        });
180
    }
181

182
    public get isAtDebuggerPrompt() {
183
        return this.socketDebugger?.isStopped ?? false;
2!
184
    }
185

186
    /**
187
     * Connect to the telnet session. This should be called before the channel is launched.
188
     */
189
    public async connect() {
190
        let deferred = defer();
×
191
        this.socketDebugger = new Debugger(this.options);
×
192
        try {
×
193
            // Emit IO from the debugger.
194
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
195
            this.socketDebugger.on('io-output', async (responseText) => {
×
196
                if (typeof responseText === 'string') {
×
197
                    responseText = this.chanperfTracker.processLog(responseText);
×
198
                    responseText = await this.rendezvousTracker.processLog(responseText);
×
199
                    this.emit('unhandled-console-output', responseText);
×
200
                    this.emit('console-output', responseText);
×
201
                }
202
            });
203

204
            // Emit IO from the debugger.
205
            this.socketDebugger.on('protocol-version', (data: ProtocolVersionDetails) => {
×
206
                if (data.errorCode === PROTOCOL_ERROR_CODES.SUPPORTED) {
×
207
                    this.emit('console-output', data.message);
×
208
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_TESTED) {
×
209
                    this.emit('unhandled-console-output', data.message);
×
210
                    this.emit('console-output', data.message);
×
211
                } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_SUPPORTED) {
×
212
                    this.emit('unhandled-console-output', data.message);
×
213
                    this.emit('console-output', data.message);
×
214
                }
215

216
                // TODO: Update once we know the exact version of the debug protocol this issue was fixed in.
217
                // Due to casing issues with variables on protocol version <FUTURE_VERSION> and under we first need to try the request in the supplied case.
218
                // If that fails we retry in lower case.
219
                this.enableVariablesLowerCaseRetry = semver.satisfies(this.activeProtocolVersion, '<3.1.0');
×
220
                // While execute was added as a command in 2.1.0. It has shortcoming that prevented us for leveraging the command.
221
                // This was mostly addressed in the 3.0.0 release to the point where we were comfortable adding support for the command.
222
                this.supportsExecuteCommand = semver.satisfies(this.activeProtocolVersion, '>=3.0.0');
×
223
            });
224

225
            // Listen for the close event
226
            this.socketDebugger.on('close', () => {
×
227
                this.emit('close');
×
228
                this.beginAppExit();
×
229
            });
230

231
            // Listen for the app exit event
232
            this.socketDebugger.on('app-exit', () => {
×
233
                this.emit('app-exit');
×
234
            });
235

236
            this.socketDebugger.on('suspend', (data) => {
×
237
                this.clearCache();
×
238
                this.emit('suspend');
×
239
            });
240

241
            this.socketDebugger.on('runtime-error', (data) => {
×
242
                console.debug('hasRuntimeError!!', data);
×
243
                this.emit('runtime-error', <BrightScriptRuntimeError>{
×
244
                    message: data.data.stopReasonDetail,
245
                    errorCode: STOP_REASONS[data.data.stopReason]
246
                });
247
            });
248

249
            this.socketDebugger.on('cannot-continue', () => {
×
250
                this.emit('cannot-continue');
×
251
            });
252

253
            this.connected = await this.socketDebugger.connect();
×
254

255
            this.logger.log(`Closing telnet connection used for compile errors`);
×
256
            if (this.compileClient) {
×
257
                this.compileClient.removeAllListeners();
×
258
                this.compileClient.destroy();
×
259
                this.compileClient = undefined;
×
260
            }
261

262
            this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected });
×
263
            this.emit('connected', this.connected);
×
264

265
            //the adapter is connected and running smoothly. resolve the promise
266
            deferred.resolve();
×
267
        } catch (e) {
268
            deferred.reject(e);
×
269
        }
270
        return deferred.promise;
×
271
    }
272

273
    private beginAppExit() {
274
        this.compileErrorProcessor.compileErrorTimer = setTimeout(() => {
×
275
            this.isAppRunning = false;
×
276
            this.emit('app-exit');
×
277
        }, 200);
278
    }
279

280
    public async watchCompileOutput() {
281
        let deferred = defer();
×
282
        try {
×
283
            this.compileClient = new Socket();
×
284
            this.compileErrorProcessor.on('diagnostics', (errors) => {
×
285
                this.compileClient.end();
×
286
                this.emit('diagnostics', errors);
×
287
            });
288

289
            //if the connection fails, reject the connect promise
290
            this.compileClient.addListener('error', (err) => {
×
291
                deferred.reject(new Error(`Error with connection to: ${this.options.host}:${this.options.brightScriptConsolePort} \n\n ${err.message} `));
×
292
            });
293
            this.logger.info('Connecting via telnet to gather compile info', { host: this.options.host, port: this.options.brightScriptConsolePort });
×
294
            this.compileClient.connect(this.options.brightScriptConsolePort, this.options.host, () => {
×
295
                this.logger.log(`Connected via telnet to gather compile info`, { host: this.options.host, port: this.options.brightScriptConsolePort });
×
296
            });
297

298
            this.logger.debug('Waiting for the compile client to settle');
×
299
            await this.settle(this.compileClient, 'data');
×
300
            this.logger.debug('Compile client has settled');
×
301

302
            let lastPartialLine = '';
×
303
            this.compileClient.on('data', (buffer) => {
×
304
                let responseText = buffer.toString();
×
305
                this.logger.info('CompileClient received data', { responseText });
×
306
                if (!responseText.endsWith('\n')) {
×
307
                    this.logger.debug('Buffer was split');
×
308
                    // buffer was split, save the partial line
309
                    lastPartialLine += responseText;
×
310
                } else {
311
                    if (lastPartialLine) {
×
312
                        this.logger.debug('Previous response was split, so merging last response with this one', { lastPartialLine, responseText });
×
313
                        // there was leftover lines, join the partial lines back together
314
                        responseText = lastPartialLine + responseText;
×
315
                        lastPartialLine = '';
×
316
                    }
317
                    // Emit the completed io string.
318
                    this.compileErrorProcessor.processUnhandledLines(responseText.trim());
×
319
                    this.emit('unhandled-console-output', responseText.trim());
×
320
                }
321
            });
322

323
            // connected to telnet. resolve the promise
324
            deferred.resolve();
×
325
        } catch (e) {
326
            deferred.reject(e);
×
327
        }
328
        return deferred.promise;
×
329
    }
330

331
    /**
332
     * Send command to step over
333
     */
334
    public async stepOver(threadId: number) {
335
        this.clearCache();
×
336
        return this.socketDebugger.stepOver(threadId);
×
337
    }
338

339
    public async stepInto(threadId: number) {
340
        this.clearCache();
×
341
        return this.socketDebugger.stepIn(threadId);
×
342
    }
343

344
    public async stepOut(threadId: number) {
345
        this.clearCache();
×
346
        return this.socketDebugger.stepOut(threadId);
×
347
    }
348

349
    /**
350
     * Tell the brightscript program to continue (i.e. resume program)
351
     */
352
    public async continue() {
353
        this.clearCache();
×
354
        return this.socketDebugger.continue();
×
355
    }
356

357
    /**
358
     * Tell the brightscript program to pause (fall into debug mode)
359
     */
360
    public async pause() {
361
        this.clearCache();
×
362
        //send the kill signal, which breaks into debugger mode
363
        return this.socketDebugger.pause();
×
364
    }
365

366
    /**
367
     * Clears the state, which means that everything will be retrieved fresh next time it is requested
368
     */
369
    public clearCache() {
370
        this.cache = {};
×
371
        this.stackFramesCache = {};
×
372
    }
373

374
    /**
375
     * Execute a command directly on the roku. Returns the output of the command
376
     * @param command
377
     * @returns the output of the command (if possible)
378
     */
379
    public async evaluate(command: string, frameId: number = this.socketDebugger.primaryThread): Promise<RokuAdapterEvaluateResponse> {
×
380
        if (this.supportsExecuteCommand) {
×
381
            if (!this.isAtDebuggerPrompt) {
×
382
                throw new Error('Cannot run evaluate: debugger is not paused');
×
383
            }
384

385
            let stackFrame = this.getStackFrameById(frameId);
×
386
            if (!stackFrame) {
×
387
                throw new Error('Cannot execute command without a corresponding frame');
×
388
            }
389
            this.logger.log('evaluate ', { command, frameId });
×
390

391
            const response = await this.socketDebugger.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex);
×
392
            this.logger.info('evaluate response', { command, response });
×
393
            if (response.executeSuccess) {
×
394
                return {
×
395
                    message: undefined,
396
                    type: 'message'
397
                };
398
            } else {
399
                return {
×
400
                    message: response.compileErrors.messages[0] ?? response.runtimeErrors.messages[0] ?? response.otherErrors.messages[0] ?? 'Unknown error executing command',
×
401
                    type: 'error'
402
                };
403
            }
404
        } else {
405
            return {
×
406
                message: `Execute commands are not supported on debug protocol: ${this.activeProtocolVersion}, v3.0.0 or greater is required.`,
407
                type: 'error'
408
            };
409
        }
410
    }
411

412
    public async getStackTrace(threadId: number = this.socketDebugger.primaryThread) {
×
413
        if (!this.isAtDebuggerPrompt) {
×
414
            throw new Error('Cannot get stack trace: debugger is not paused');
×
415
        }
416
        return this.resolve(`stack trace for thread ${threadId}`, async () => {
×
417
            let thread = await this.getThreadByThreadId(threadId);
×
418
            let frames: StackFrame[] = [];
×
419
            let stackTraceData = await this.socketDebugger.stackTrace(threadId);
×
420
            for (let i = 0; i < stackTraceData.stackSize; i++) {
×
421
                let frameData = stackTraceData.entries[i];
×
422
                let stackFrame: StackFrame = {
×
423
                    frameId: this.nextFrameId++,
424
                    // frame index is the reverse of the returned order.
425
                    frameIndex: stackTraceData.stackSize - i - 1,
426
                    threadIndex: threadId,
427
                    filePath: frameData.fileName,
428
                    lineNumber: frameData.lineNumber,
429
                    // eslint-disable-next-line no-nested-ternary
430
                    functionIdentifier: this.cleanUpFunctionName(i === 0 ? (frameData.functionName) ? frameData.functionName : thread.functionName : frameData.functionName)
×
431
                };
432
                this.stackFramesCache[stackFrame.frameId] = stackFrame;
×
433
                frames.push(stackFrame);
×
434
            }
435
            //if the first frame is missing any data, suppliment with thread information
436
            if (frames[0]) {
×
437
                frames[0].filePath ??= thread.filePath;
×
438
                frames[0].lineNumber ??= thread.lineNumber;
×
439
            }
440

441
            return frames;
×
442
        });
443
    }
444

445
    private getStackFrameById(frameId: number): StackFrame {
446
        return this.stackFramesCache[frameId];
×
447
    }
448

449
    private cleanUpFunctionName(functionName): string {
450
        return functionName.substring(functionName.lastIndexOf('@') + 1);
×
451
    }
452

453
    /**
454
     * Given an expression, evaluate that statement ON the roku
455
     * @param expression
456
     */
457
    public async getVariable(expression: string, frameId: number, withChildren = true) {
×
458
        const logger = this.logger.createLogger(' getVariable');
2✔
459
        logger.info('begin', { expression });
2✔
460
        if (!this.isAtDebuggerPrompt) {
2!
461
            throw new Error('Cannot resolve variable: debugger is not paused');
×
462
        }
463

464
        let frame = this.getStackFrameById(frameId);
2✔
465
        if (!frame) {
2!
466
            throw new Error('Cannot request variable without a corresponding frame');
×
467
        }
468

469
        logger.log(`Expression:`, expression);
2✔
470
        let variablePath = expression === '' ? [] : util.getVariablePath(expression);
2✔
471

472
        // Temporary workaround related to casing issues over the protocol
473
        if (this.enableVariablesLowerCaseRetry && variablePath?.length > 0) {
2!
474
            variablePath[0] = variablePath[0].toLowerCase();
1✔
475
        }
476

477
        let response = await this.socketDebugger.getVariables(variablePath, withChildren, frame.frameIndex, frame.threadIndex);
2✔
478

479
        if (this.enableVariablesLowerCaseRetry && response.errorCode !== ERROR_CODES.OK) {
2!
480
            // Temporary workaround related to casing issues over the protocol
481
            logger.log(`Retrying expression as lower case:`, expression);
×
482
            variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase());
×
483
            response = await this.socketDebugger.getVariables(variablePath, withChildren, frame.frameIndex, frame.threadIndex);
×
484
        }
485

486

487
        if (response.errorCode === ERROR_CODES.OK) {
2!
488
            let mainContainer: EvaluateContainer;
489
            let children: EvaluateContainer[] = [];
2✔
490
            let firstHandled = false;
2✔
491
            for (let variable of response.variables) {
2✔
492
                let value;
493
                let variableType = variable.variableType;
6✔
494
                if (variable.value === null) {
6!
495
                    value = 'roInvalid';
×
496
                } else if (variableType === 'String') {
6!
497
                    value = `\"${variable.value}\"`;
×
498
                } else {
499
                    value = variable.value;
6✔
500
                }
501

502
                if (variableType === 'Subtyped_Object') {
6!
503
                    //subtyped objects can only have string values
504
                    let parts = (variable.value as string).split('; ');
×
505
                    variableType = `${parts[0]} (${parts[1]})`;
×
506
                } else if (variableType === 'AA') {
6✔
507
                    variableType = 'AssociativeArray';
1✔
508
                }
509

510
                let container = <EvaluateContainer>{
6✔
511
                    name: expression,
512
                    evaluateName: expression,
513
                    variablePath: variablePath,
514
                    type: variableType,
515
                    value: value,
516
                    keyType: variable.keyType,
517
                    elementCount: variable.elementCount
518
                };
519

520
                if (!firstHandled && variablePath.length > 0) {
6✔
521
                    firstHandled = true;
1✔
522
                    mainContainer = container;
1✔
523
                } else {
524
                    if (!firstHandled && variablePath.length === 0) {
5✔
525
                        // If this is a scope request there will be no entries in the variable path
526
                        // We will need to create a fake mainContainer
527
                        firstHandled = true;
1✔
528
                        mainContainer = <EvaluateContainer>{
1✔
529
                            name: expression,
530
                            evaluateName: expression,
531
                            variablePath: variablePath,
532
                            type: '',
533
                            value: null,
534
                            keyType: 'String',
535
                            elementCount: response.numVariables
536
                        };
537
                    }
538

539
                    let pathAddition = mainContainer.keyType === 'Integer' ? children.length : variable.name;
5!
540
                    container.name = pathAddition.toString();
5✔
541
                    if (mainContainer.evaluateName) {
5✔
542
                        container.evaluateName = `${mainContainer.evaluateName}["${pathAddition}"]`;
2✔
543
                    } else {
544
                        container.evaluateName = pathAddition.toString();
3✔
545
                    }
546
                    container.variablePath = [].concat(container.variablePath, [pathAddition.toString()]);
5✔
547
                    if (container.keyType) {
5!
548
                        container.children = [];
×
549
                    }
550
                    children.push(container);
5✔
551
                }
552
            }
553
            mainContainer.children = children;
2✔
554
            return mainContainer;
2✔
555
        }
556
    }
557

558
    /**
559
     * Cache items by a unique key
560
     * @param expression
561
     * @param factory
562
     */
563
    private resolve<T>(key: string, factory: () => T | Thenable<T>): Promise<T> {
564
        if (this.cache[key]) {
×
565
            this.logger.log('return cashed response', key, this.cache[key]);
×
566
            return this.cache[key];
×
567
        }
568
        this.cache[key] = Promise.resolve<T>(factory());
×
569
        return this.cache[key];
×
570
    }
571

572
    /**
573
     * Get a list of threads. The active thread will always be first in the list.
574
     */
575
    public async getThreads() {
576
        if (!this.isAtDebuggerPrompt) {
×
577
            throw new Error('Cannot get threads: debugger is not paused');
×
578
        }
579
        return this.resolve('threads', async () => {
×
580
            let threads: Thread[] = [];
×
581
            let threadsData = await this.socketDebugger.threads();
×
582

583
            for (let i = 0; i < threadsData.threadsCount; i++) {
×
584
                let threadInfo = threadsData.threads[i];
×
585
                let thread = <Thread>{
×
586
                    // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary.
587
                    // NOTE: Rely on the thead index from the threads update event.
588
                    isSelected: this.socketDebugger.primaryThread === i,
589
                    // isSelected: threadInfo.isPrimary,
590
                    filePath: threadInfo.fileName,
591
                    functionName: threadInfo.functionName,
592
                    lineNumber: threadInfo.lineNumber + 1, //protocol is 0-based but 1-based is expected
593
                    lineContents: threadInfo.codeSnippet,
594
                    threadId: i
595
                };
596
                threads.push(thread);
×
597
            }
598
            //make sure the selected thread is at the top
599
            threads.sort((a, b) => {
×
600
                return a.isSelected ? -1 : 1;
×
601
            });
602

603
            return threads;
×
604
        });
605
    }
606

607
    private async getThreadByThreadId(threadId: number) {
608
        let threads = await this.getThreads();
×
609
        for (let thread of threads) {
×
610
            if (thread.threadId === threadId) {
×
611
                return thread;
×
612
            }
613
        }
614
    }
615

616
    public removeAllListeners() {
617
        this.emitter?.removeAllListeners();
×
618
    }
619

620
    /**
621
     * Disconnect from the telnet session and unset all objects
622
     */
623
    public async destroy() {
624
        if (this.socketDebugger) {
×
625
            // destroy might be called due to a compile error so the socket debugger might not exist yet
626
            await this.socketDebugger.exitChannel();
×
627
        }
628

629
        this.cache = undefined;
×
630
        if (this.emitter) {
×
631
            this.emitter.removeAllListeners();
×
632
        }
633
        this.emitter = undefined;
×
634
    }
635

636
    // #region Rendezvous Tracker pass though functions
637
    /**
638
     * Passes the debug functions used to locate the client files and lines to the RendezvousTracker
639
     */
640
    public registerSourceLocator(sourceLocator: (debuggerPath: string, lineNumber: number) => Promise<SourceLocation>) {
641
        this.rendezvousTracker.registerSourceLocator(sourceLocator);
×
642
    }
643

644
    /**
645
     * Passes the log level down to the RendezvousTracker and ChanperfTracker
646
     * @param outputLevel the consoleOutput from the launch config
647
     */
648
    public setConsoleOutput(outputLevel: string) {
649
        this.chanperfTracker.setConsoleOutput(outputLevel);
×
650
        this.rendezvousTracker.setConsoleOutput(outputLevel);
×
651
    }
652

653
    /**
654
     * Sends a call to the RendezvousTracker to clear the current rendezvous history
655
     */
656
    public clearRendezvousHistory() {
657
        this.rendezvousTracker.clearHistory();
×
658
    }
659

660
    /**
661
     * Sends a call to the ChanperfTracker to clear the current chanperf history
662
     */
663
    public clearChanperfHistory() {
664
        this.chanperfTracker.clearHistory();
×
665
    }
666
    // #endregion
667

668
    public async syncBreakpoints() {
669
        //we can't send breakpoints unless we're stopped. So...if we're not stopped, quit now. (we'll get called again when the stop event happens)
670
        if (!this.isAtDebuggerPrompt) {
×
671
            return;
×
672
        }
673
        //compute breakpoint changes since last sync
674
        const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects());
×
675

676
        //delete these breakpoints
677
        if (diff.removed.length > 0) {
×
678
            await this.actionQueue.run(async () => {
×
679
                const response = await this.socketDebugger.removeBreakpoints(
×
680
                    diff.removed.map(x => x.deviceId)
×
681
                );
682
                //return true to mark this action as complete, or false to retry the task again in the future
683
                return response.success && response.errorCode === ERROR_CODES.OK;
×
684
            });
685
        }
686

687
        if (diff.added.length > 0) {
×
688
            const breakpointsToSendToDevice = diff.added.map(breakpoint => {
×
689
                const hitCount = parseInt(breakpoint.hitCondition);
×
690
                return {
×
691
                    filePath: breakpoint.pkgPath,
692
                    lineNumber: breakpoint.line,
693
                    hitCount: !isNaN(hitCount) ? hitCount : undefined,
×
694
                    conditionalExpression: breakpoint.condition,
695
                    key: breakpoint.hash,
696
                    componentLibraryName: breakpoint.componentLibraryName
697
                };
698
            });
699

700
            //send these new breakpoints to the device
701
            await this.actionQueue.run(async () => {
×
702
                const response = await this.socketDebugger.addBreakpoints(breakpointsToSendToDevice);
×
703
                if (response.errorCode === ERROR_CODES.OK) {
×
704
                    //mark the breakpoints as verified
705
                    for (let i = 0; i < response.breakpoints.length; i++) {
×
706
                        const deviceBreakpoint = response.breakpoints[i];
×
707
                        if (deviceBreakpoint.isVerified) {
×
708
                            this.breakpointManager.verifyBreakpoint(
×
709
                                breakpointsToSendToDevice[i].key,
710
                                deviceBreakpoint.breakpointId
711
                            );
712
                        }
713
                    }
714
                    //return true to mark this action as complete
715
                    return true;
×
716
                } else {
717
                    //this action is not yet complete. it should be retried
718
                    return false;
×
719
                }
720
            });
721
        }
722
    }
723

724
    private actionQueue = new ActionQueue();
2✔
725
}
726

727
export interface StackFrame {
728
    frameId: number;
729
    frameIndex: number;
730
    threadIndex: number;
731
    filePath: string;
732
    lineNumber: number;
733
    functionIdentifier: string;
734
}
735

736
export enum EventName {
1✔
737
    suspend = 'suspend'
1✔
738
}
739

740
export interface EvaluateContainer {
741
    name: string;
742
    evaluateName: string;
743
    variablePath: string[];
744
    type: string;
745
    value: string;
746
    keyType: KeyType;
747
    elementCount: number;
748
    highLevelType: HighLevelType;
749
    children: EvaluateContainer[];
750
    presentationHint?: 'property' | 'method' | 'class' | 'data' | 'event' | 'baseClass' | 'innerClass' | 'interface' | 'mostDerivedClass' | 'virtual' | 'dataBreakpoint';
751
}
752

753
export enum KeyType {
1✔
754
    string = 'String',
1✔
755
    integer = 'Integer',
1✔
756
    legacy = 'Legacy'
1✔
757
}
758

759
export interface Thread {
760
    isSelected: boolean;
761
    lineNumber: number;
762
    filePath: string;
763
    functionName: string;
764
    lineContents: string;
765
    threadId: number;
766
}
767

768
interface BrightScriptRuntimeError {
769
    message: string;
770
    errorCode: string;
771
}
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

© 2025 Coveralls, Inc