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

rokucommunity / roku-debug / #2010

pending completion
#2010

push

web-flow
Merge bb51a5c85 into f30a7deaa

1834 of 2724 branches covered (67.33%)

Branch coverage included in aggregate %.

1866 of 1866 new or added lines in 41 files covered. (100.0%)

3396 of 4571 relevant lines covered (74.29%)

25.55 hits per line

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

77.58
/src/debugProtocol/client/DebugProtocolClient.ts
1
import * as Net from 'net';
1✔
2
import * as EventEmitter from 'eventemitter3';
1✔
3
import * as semver from 'semver';
1✔
4
import { PROTOCOL_ERROR_CODES, Command, StepType, ErrorCode, UpdateType, UpdateTypeCode, StopReason } from '../Constants';
1✔
5
import { logger } from '../../logging';
1✔
6
import { ExecuteV3Response } from '../events/responses/ExecuteV3Response';
1✔
7
import { ListBreakpointsResponse } from '../events/responses/ListBreakpointsResponse';
1✔
8
import { AddBreakpointsResponse } from '../events/responses/AddBreakpointsResponse';
1✔
9
import { RemoveBreakpointsResponse } from '../events/responses/RemoveBreakpointsResponse';
1✔
10
import { defer, util } from '../../util';
1✔
11
import { BreakpointErrorUpdate } from '../events/updates/BreakpointErrorUpdate';
1✔
12
import { ContinueRequest } from '../events/requests/ContinueRequest';
1✔
13
import { StopRequest } from '../events/requests/StopRequest';
1✔
14
import { ExitChannelRequest } from '../events/requests/ExitChannelRequest';
1✔
15
import { StepRequest } from '../events/requests/StepRequest';
1✔
16
import { RemoveBreakpointsRequest } from '../events/requests/RemoveBreakpointsRequest';
1✔
17
import { ListBreakpointsRequest } from '../events/requests/ListBreakpointsRequest';
1✔
18
import { VariablesRequest } from '../events/requests/VariablesRequest';
1✔
19
import { StackTraceRequest } from '../events/requests/StackTraceRequest';
1✔
20
import { ThreadsRequest } from '../events/requests/ThreadsRequest';
1✔
21
import { ExecuteRequest } from '../events/requests/ExecuteRequest';
1✔
22
import { AddBreakpointsRequest } from '../events/requests/AddBreakpointsRequest';
1✔
23
import { AddConditionalBreakpointsRequest } from '../events/requests/AddConditionalBreakpointsRequest';
1✔
24
import type { ProtocolRequest, ProtocolResponse, ProtocolUpdate } from '../events/ProtocolEvent';
25
import { HandshakeResponse } from '../events/responses/HandshakeResponse';
1✔
26
import { HandshakeV3Response } from '../events/responses/HandshakeV3Response';
1✔
27
import { HandshakeRequest } from '../events/requests/HandshakeRequest';
1✔
28
import { GenericV3Response } from '../events/responses/GenericV3Response';
1✔
29
import { AllThreadsStoppedUpdate } from '../events/updates/AllThreadsStoppedUpdate';
1✔
30
import { CompileErrorUpdate } from '../events/updates/CompileErrorUpdate';
1✔
31
import { GenericResponse } from '../events/responses/GenericResponse';
1✔
32
import type { StackTraceResponse } from '../events/responses/StackTraceResponse';
33
import { ThreadsResponse } from '../events/responses/ThreadsResponse';
1✔
34
import type { Variable } from '../events/responses/VariablesResponse';
35
import { VariablesResponse, VariablesResponseData, VariableType } from '../events/responses/VariablesResponse';
1✔
36
import { IOPortOpenedUpdate, isIOPortOpenedUpdate } from '../events/updates/IOPortOpenedUpdate';
1✔
37
import { ThreadAttachedUpdate } from '../events/updates/ThreadAttachedUpdate';
1✔
38
import { StackTraceV3Response } from '../events/responses/StackTraceV3Response';
1✔
39
import { ActionQueue } from '../../managers/ActionQueue';
1✔
40
import type { DebugProtocolClientPlugin } from './DebugProtocolClientPlugin';
41
import PluginInterface from '../PluginInterface';
1✔
42
import type { VerifiedBreakpoint } from '../events/updates/BreakpointVerifiedUpdate';
43
import { BreakpointVerifiedUpdate } from '../events/updates/BreakpointVerifiedUpdate';
1✔
44
import type { AddConditionalBreakpointsResponse } from '../events/responses/AddConditionalBreakpointsResponse';
45

46
export class DebugProtocolClient {
1✔
47

48
    public logger = logger.createLogger(`[${DebugProtocolClient.name}]`);
64✔
49

50
    // The highest tested version of the protocol we support.
51
    public supportedVersionRange = '<=3.0.0';
64✔
52

53
    constructor(
54
        options?: ConstructorOptions
55
    ) {
56
        this.options = {
64✔
57
            controlPort: 8081,
58
            host: undefined,
59
            //override the defaults with the options from parameters
60
            ...options ?? {}
192✔
61
        };
62

63
        //add the internal plugin last, so it's the final plugin to handle the events
64
        this.addCorePlugin();
64✔
65
    }
66

67
    private addCorePlugin() {
68
        this.plugins.add({
64✔
69
            onUpdate: (event) => {
70
                return this.handleUpdate(event.update);
56✔
71
            }
72
        }, 999);
73
    }
74

75
    public static DEBUGGER_MAGIC = 'bsdebug'; // 64-bit = [b'bsdebug\0' little-endian]
1✔
76

77
    public scriptTitle: string;
78
    public isHandshakeComplete = false;
64✔
79
    public connectedToIoPort = false;
64✔
80
    /**
81
     * Debug protocol version 3.0.0 introduced a packet_length to all responses. Prior to that, most responses had no packet length at all.
82
     * This field indicates whether we should be looking for packet_length or not in the responses we get from the device
83
     */
84
    public watchPacketLength = false;
64✔
85
    public protocolVersion: string;
86
    public primaryThread: number;
87
    public stackFrameIndex: number;
88

89
    /**
90
     * A collection of plugins that can interact with the client at lifecycle points
91
     */
92
    public plugins = new PluginInterface<DebugProtocolClientPlugin>();
64✔
93

94
    private emitter = new EventEmitter();
64✔
95
    /**
96
     * The primary socket for this session. It's used to communicate with the debugger by sending commands and receives responses or updates
97
     */
98
    private controlSocket: Net.Socket;
99
    /**
100
     * A socket where the debug server will send stdio
101
     */
102
    private ioSocket: Net.Socket;
103
    /**
104
     * The buffer where all unhandled data will be stored until successfully consumed
105
     */
106
    private buffer = Buffer.alloc(0);
64✔
107
    /**
108
     * Is the debugger currently stopped at a line of code in the program
109
     */
110
    public isStopped = false;
64✔
111
    private requestIdSequence = 1;
64✔
112
    private activeRequests = new Map<number, ProtocolRequest>();
64✔
113
    private options: ConstructorOptions;
114

115
    /**
116
     * Prior to protocol v3.1.0, the Roku device would regularly set the wrong thread as "active",
117
     * so this flag lets us know if we should use our better-than-nothing workaround
118
     */
119
    private get enableThreadHoppingWorkaround() {
120
        return semver.satisfies(this.protocolVersion, '<3.1.0');
12✔
121
    }
122

123
    /**
124
     * Starting in protocol v3.1.0, component libary breakpoints must be added in the format `lib:/<library_name>/<filepath>`, but prior they didn't require this.
125
     * So this flag tells us which format to support
126
     */
127
    private get enableComponentLibrarySpecificBreakpoints() {
128
        return semver.satisfies(this.protocolVersion, '>=3.1.0');
13✔
129
    }
130

131
    /**
132
     * Starting in protocol v3.1.0, breakpoints can support conditional expressions. This flag indicates whether the current sessuion supports that functionality.
133
     */
134
    private get supportsConditionalBreakpoints() {
135
        return semver.satisfies(this.protocolVersion, '>=3.1.0');
11✔
136
    }
137

138
    public get supportsBreakpointRegistrationWhileRunning() {
139
        return semver.satisfies(this.protocolVersion, '>=3.2.0');
3✔
140
    }
141

142
    public get supportsBreakpointVerification() {
143
        return semver.satisfies(this.protocolVersion, '>=3.2.0');
7✔
144
    }
145

146
    /**
147
     * Get a promise that resolves after an event occurs exactly once
148
     */
149
    public once(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start'): Promise<void>;
150
    public once(eventName: 'breakpoints-verified'): Promise<BreakpointsVerifiedEvent>;
151
    public once<T = AllThreadsStoppedUpdate | ThreadAttachedUpdate>(eventName: 'runtime-error' | 'suspend'): Promise<T>;
152
    public once(eventName: 'io-output'): Promise<string>;
153
    public once(eventName: 'data'): Promise<Buffer>;
154
    public once(eventName: 'response'): Promise<ProtocolResponse>;
155
    public once(eventName: 'update'): Promise<ProtocolUpdate>;
156
    public once(eventName: 'protocol-version'): Promise<ProtocolVersionDetails>;
157
    public once(eventName: 'handshake-verified'): Promise<HandshakeResponse>;
158
    public once(eventName: string) {
159
        return new Promise((resolve) => {
50✔
160
            const disconnect = this.on(eventName as Parameters<DebugProtocolClient['on']>[0], (...args) => {
50✔
161
                disconnect();
50✔
162
                resolve(...args);
50✔
163
            });
164
        });
165
    }
166

167
    public on(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start', handler: () => void);
168
    public on(eventName: 'breakpoints-verified', handler: (event: BreakpointsVerifiedEvent) => void);
169
    public on(eventName: 'response', handler: (response: ProtocolResponse) => void);
170
    public on(eventName: 'update', handler: (update: ProtocolUpdate) => void);
171
    /**
172
     * The raw data from the server socket. You probably don't need this...
173
     */
174
    public on(eventName: 'data', handler: (data: Buffer) => void);
175
    public on<T = AllThreadsStoppedUpdate | ThreadAttachedUpdate>(eventName: 'runtime-error' | 'suspend', handler: (data: T) => void);
176
    public on(eventName: 'io-output', handler: (output: string) => void);
177
    public on(eventName: 'protocol-version', handler: (data: ProtocolVersionDetails) => void);
178
    public on(eventName: 'handshake-verified', handler: (data: HandshakeResponse) => void);
179
    // public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void);
180
    // public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void);
181
    public on(eventName: string, handler: (payload: any) => void) {
182
        this.emitter.on(eventName, handler);
204✔
183
        return () => {
204✔
184
            this.emitter.removeListener(eventName, handler);
155✔
185
        };
186
    }
187

188
    private emit(eventName: 'response', response: ProtocolResponse);
189
    private emit(eventName: 'update', update: ProtocolUpdate);
190
    private emit(eventName: 'data', update: Buffer);
191
    private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent);
192
    private emit(eventName: 'suspend' | 'runtime-error', data: AllThreadsStoppedUpdate | ThreadAttachedUpdate);
193
    private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'start', data?);
194
    private emit(eventName: string, data?) {
195
        //emit these events on next tick, otherwise they will be processed immediately which could cause issues
196
        setTimeout(() => {
558✔
197
            //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists
198
            this.emitter.emit(eventName, data);
558✔
199
        }, 0);
200
    }
201

202
    private async establishControlConnection() {
203
        const pendingSockets = new Set<Net.Socket>();
52✔
204
        const connection = await new Promise<Net.Socket>((resolve) => {
52✔
205
            util.setInterval((cancelInterval) => {
52✔
206
                const socket = new Net.Socket();
52✔
207
                pendingSockets.add(socket);
52✔
208
                socket.on('error', (error) => {
52✔
209
                    console.debug(Date.now(), 'Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error);
×
210
                });
211
                socket.connect({ port: this.options.controlPort, host: this.options.host }, () => {
52✔
212
                    cancelInterval();
52✔
213

214
                    this.logger.debug(`Connected to debug protocol control port. Socket ${[...pendingSockets].indexOf(socket)} of ${pendingSockets.size} was the winner`);
52✔
215
                    //clean up all remaining pending sockets
216
                    for (const pendingSocket of pendingSockets) {
52✔
217
                        pendingSocket.removeAllListeners();
52✔
218
                        //cleanup and destroy all other sockets
219
                        if (pendingSocket !== socket) {
52!
220
                            pendingSocket.end();
×
221
                            pendingSocket?.destroy();
×
222
                        }
223
                    }
224
                    pendingSockets.clear();
52✔
225
                    resolve(socket);
52✔
226
                });
227
            }, this.options.controlConnectInterval ?? 250);
156!
228
        });
229
        await this.plugins.emit('onServerConnected', {
52✔
230
            client: this,
231
            server: connection
232
        });
233
        return connection;
52✔
234
    }
235

236
    /**
237
     * A queue for processing the incoming buffer, every transmission at a time
238
     */
239
    private bufferQueue = new ActionQueue();
64✔
240

241
    /**
242
     * Connect to the debug server.
243
     * @param sendHandshake should the handshake be sent as part of this connect process. If false, `.sendHandshake()` will need to be called before a session can begin
244
     */
245
    public async connect(sendHandshake = true): Promise<boolean> {
52✔
246
        this.logger.log('connect', this.options);
52✔
247

248
        // If there is no error, the server has accepted the request and created a new dedicated control socket
249
        this.controlSocket = await this.establishControlConnection();
52✔
250

251
        this.controlSocket.on('data', (data) => {
52✔
252
            this.emit('data', data);
160✔
253
            this.logger.info('received server-to-client data\n', { type: 'server-to-client', data: data.toJSON() });
160✔
254
            //queue up processing the new data, chunk by chunk
255
            void this.bufferQueue.run(async () => {
160✔
256
                this.buffer = Buffer.concat([this.buffer, data]);
160✔
257
                while (this.buffer.length > 0 && await this.process()) {
160✔
258
                    //the loop condition is the actual work
259
                }
260
                return true;
160✔
261
            });
262
        });
263

264
        this.controlSocket.on('end', () => {
52✔
265
            this.logger.log('TCP connection closed');
7✔
266
            this.shutdown('app-exit');
7✔
267
        });
268

269
        // Don't forget to catch error, for your own sake.
270
        this.controlSocket.once('error', (error) => {
52✔
271
            //the Roku closed the connection for some unknown reason...
272
            console.error(`TCP connection error on control port`, error);
×
273
            this.shutdown('close');
×
274
        });
275

276
        if (sendHandshake) {
52!
277
            await this.sendHandshake();
52✔
278
        }
279
        return true;
52✔
280
    }
281

282
    /**
283
     * Send the initial handshake request, and wait for the handshake response
284
     */
285
    public async sendHandshake() {
286
        return this.processHandshakeRequest(
52✔
287
            HandshakeRequest.fromJson({
288
                magic: DebugProtocolClient.DEBUGGER_MAGIC
289
            })
290
        );
291
    }
292

293
    private async processHandshakeRequest(request: HandshakeRequest) {
294
        //send the magic, which triggers the debug session
295
        this.logger.log('Sending magic to server');
52✔
296

297
        //send the handshake request, and wait for the handshake response from the device
298
        return this.sendRequest<HandshakeV3Response | HandshakeResponse>(request);
52✔
299
    }
300

301
    public continue() {
302
        return this.processContinueRequest(
2✔
303
            ContinueRequest.fromJson({
304
                requestId: this.requestIdSequence++
305
            })
306
        );
307
    }
308

309
    private async processContinueRequest(request: ContinueRequest) {
310
        if (this.isStopped) {
2✔
311
            this.isStopped = false;
1✔
312
            return this.sendRequest<GenericResponse>(request);
1✔
313
        }
314
    }
315

316
    public pause() {
317
        return this.processStopRequest(
2✔
318
            StopRequest.fromJson({
319
                requestId: this.requestIdSequence++
320
            })
321
        );
322
    }
323

324
    private async processStopRequest(request: StopRequest) {
325
        if (this.isStopped === false) {
2✔
326
            return this.sendRequest<GenericResponse>(request);
1✔
327
        }
328
    }
329

330
    public async exitChannel() {
331
        return this.sendRequest<GenericResponse>(
1✔
332
            ExitChannelRequest.fromJson({
333
                requestId: this.requestIdSequence++
334
            })
335
        );
336
    }
337

338
    public async stepIn(threadIndex: number = this.primaryThread) {
1✔
339
        return this.step(StepType.Line, threadIndex);
2✔
340
    }
341

342
    public async stepOver(threadIndex: number = this.primaryThread) {
1✔
343
        return this.step(StepType.Over, threadIndex);
2✔
344
    }
345

346
    public async stepOut(threadIndex: number = this.primaryThread) {
3✔
347
        return this.step(StepType.Out, threadIndex);
4✔
348
    }
349

350
    private async step(stepType: StepType, threadIndex: number): Promise<GenericResponse> {
351
        return this.processStepRequest(
8✔
352
            StepRequest.fromJson({
353
                requestId: this.requestIdSequence++,
354
                stepType: stepType,
355
                threadIndex: threadIndex
356
            })
357
        );
358
    }
359

360
    private async processStepRequest(request: StepRequest) {
361
        if (this.isStopped) {
8✔
362
            this.isStopped = false;
7✔
363
            let stepResult = await this.sendRequest<GenericResponse>(request);
7✔
364
            if (stepResult.data.errorCode === ErrorCode.OK) {
7✔
365
                this.isStopped = true;
6✔
366
                //TODO this is not correct. Do we get a new threads event after a step? Perhaps that should be what triggers the event instead of us?
367
                this.emit('suspend', stepResult as AllThreadsStoppedUpdate);
6✔
368
            } else {
369
                // there is a CANT_CONTINUE error code but we can likely treat all errors like a CANT_CONTINUE
370
                this.emit('cannot-continue');
1✔
371
            }
372
            return stepResult;
7✔
373
        } else {
374
            this.logger.log('[processStepRequest] skipped because debugger is not paused');
1✔
375
        }
376
    }
377

378
    public async threads() {
379
        return this.processThreadsRequest(
10✔
380
            ThreadsRequest.fromJson({
381
                requestId: this.requestIdSequence++
382
            })
383
        );
384
    }
385
    public async processThreadsRequest(request: ThreadsRequest) {
386
        if (this.isStopped) {
10✔
387
            let result = await this.sendRequest<ThreadsResponse>(request);
9✔
388

389
            if (result.data.errorCode === ErrorCode.OK) {
9✔
390
                //older versions of the debug protocol had issues with maintaining the active thread, so our workaround is to keep track of it elsewhere
391
                if (this.enableThreadHoppingWorkaround) {
8✔
392
                    //ignore the `isPrimary` flag on threads
393
                    this.logger.debug(`Ignoring the 'isPrimary' flag from threads because protocol version 3.0.0 and lower has a bug`);
1✔
394
                } else {
395
                    //trust the debug protocol's `isPrimary` flag on threads
396
                    for (let i = 0; i < result.data.threads.length; i++) {
7✔
397
                        let thread = result.data.threads[i];
8✔
398
                        if (thread.isPrimary) {
8✔
399
                            this.primaryThread = i;
7✔
400
                            break;
7✔
401
                        }
402
                    }
403
                }
404
            }
405
            return result;
9✔
406
        } else {
407
            this.logger.log('[processThreadsRequest] skipped because not stopped');
1✔
408
        }
409
    }
410

411
    /**
412
     * Get the stackTrace from the device IF currently stopped
413
     */
414
    public async getStackTrace(threadIndex: number = this.primaryThread) {
2✔
415
        return this.processStackTraceRequest(
9✔
416
            StackTraceRequest.fromJson({
417
                requestId: this.requestIdSequence++,
418
                threadIndex: threadIndex
419
            })
420
        );
421
    }
422

423
    private async processStackTraceRequest(request: StackTraceRequest) {
424
        if (!this.isStopped) {
9✔
425
            this.logger.log('[getStackTrace] skipped because debugger is not paused');
1✔
426
        } else if (request?.data?.threadIndex > -1) {
8!
427
            return this.sendRequest<StackTraceResponse>(request);
7✔
428
        } else {
429
            this.logger.log(`[getStackTrace] skipped because ${request?.data?.threadIndex} is not valid threadIndex`);
1!
430
        }
431
    }
432

433
    /**
434
     * @param variablePathEntries One or more path entries to the variable to be inspected. E.g., m.top.myObj["someKey"] can be accessed with ["m","top","myobj","\"someKey\""].
435
     *
436
     *                            If no path is specified, the variables accessible from the specified stack frame are returned.
437
     *
438
     *                            Starting in protocol v3.1.0, The keys for indexed gets (i.e. obj["key"]) should be wrapped in quotes so they can be handled in a case-sensitive fashion (if applicable on device).
439
     *                            All non-quoted keys (i.e. strings without leading and trailing quotes inside them) will be treated as case-insensitive).
440
     * @param getChildKeys  If set, VARIABLES response include the child keys for container types like lists and associative arrays
441
     * @param stackFrameIndex 0 = first function called, nframes-1 = last function. This indexing does not match the order of the frames returned from the STACKTRACE command
442
     * @param threadIndex the index (or perhaps ID?) of the thread to get variables for
443
     */
444
    public async getVariables(variablePathEntries: Array<string> = [], stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) {
17✔
445
        const response = await this.processVariablesRequest(
16✔
446
            VariablesRequest.fromJson({
447
                requestId: this.requestIdSequence++,
448
                threadIndex: threadIndex,
449
                stackFrameIndex: stackFrameIndex,
450
                getChildKeys: true,
451
                variablePathEntries: variablePathEntries.map(x => ({
26✔
452
                    //remove leading and trailing quotes
453
                    name: x.replace(/^"/, '').replace(/"$/, ''),
454
                    forceCaseInsensitive: !x.startsWith('"') && !x.endsWith('"')
50✔
455
                })),
456
                //starting in protocol v3.1.0, it supports marking certain path items as case-insensitive (i.e. parts of DottedGet expressions)
457
                enableForceCaseInsensitivity: semver.satisfies(this.protocolVersion, '>=3.1.0') && variablePathEntries.length > 0
31✔
458
            })
459
        );
460

461
        //if there was an issue, build a "fake" variables response for several known situationsm or throw nicer errors
462
        if (util.hasNonNullishProperty(response?.data.errorData)) {
16✔
463
            let variable = {
7✔
464
                value: null,
465
                isContainer: false,
466
                isConst: false,
467
                refCount: 0,
468
                childCount: 0
469
            } as Variable;
470
            const simulatedResponse = VariablesResponse.fromJson({
7✔
471
                ...response.data,
472
                variables: [variable]
473
            });
474

475
            let parentVarType: VariableType;
476
            let parentVarTypeText: string;
477
            const loadParentVarInfo = async (index: number) => {
7✔
478
                //fetch the variable one level back from the bad one to get its type
479
                const parentVar = await this.getVariables(
4✔
480
                    variablePathEntries.slice(0, index),
481
                    stackFrameIndex,
482
                    threadIndex
483
                );
484
                parentVarType = parentVar?.data?.variables?.[0]?.type;
4!
485
                parentVarTypeText = parentVarType;
4✔
486
                //convert `roSGNode; Node` to `roSGNode (Node)`
487
                if (parentVarType === VariableType.SubtypedObject) {
4!
488
                    const chunks = parentVar?.data?.variables?.[0]?.value?.toString().split(';').map(x => x.trim());
×
489
                    parentVarTypeText = `${chunks[0]} (${chunks[1]})`;
×
490
                }
491
            };
492

493
            if (!util.isNullish(response.data.errorData.missingKeyIndex)) {
7✔
494
                const { missingKeyIndex } = response.data.errorData;
5✔
495
                //leftmost var is uninitialized, and we tried to read it
496
                //ex: variablePathEntries = [`notThere`]
497
                if (variablePathEntries.length === 1 && missingKeyIndex === 0) {
5✔
498
                    variable.name = variablePathEntries[0];
1✔
499
                    variable.type = VariableType.Uninitialized;
1✔
500
                    return simulatedResponse;
1✔
501
                }
502

503
                //leftmost var was uninitialized, and tried to read a prop on it
504
                //ex: variablePathEntries = ["notThere", "definitelyNotThere"]
505
                if (missingKeyIndex === 0 && variablePathEntries.length > 1) {
4✔
506
                    throw new Error(`Cannot read '${variablePathEntries[missingKeyIndex + 1]}' on type 'Uninitialized'`);
1✔
507
                }
508

509
                if (variablePathEntries.length > 1 && missingKeyIndex > 0) {
3!
510
                    await loadParentVarInfo(missingKeyIndex);
3✔
511

512
                    // prop at the end of Node or AA doesn't exist. Treat like `invalid`.
513
                    // ex: variablePathEntries = ['there', 'notThere']
514
                    if (
3✔
515
                        missingKeyIndex === variablePathEntries.length - 1 &&
4✔
516
                        [VariableType.AssociativeArray, VariableType.SubtypedObject].includes(parentVarType)
517
                    ) {
518
                        variable.name = variablePathEntries[variablePathEntries.length - 1];
1✔
519
                        variable.type = VariableType.Invalid;
1✔
520
                        variable.value = 'Invalid (not defined)';
1✔
521
                        return simulatedResponse;
1✔
522
                    }
523
                }
524
                //prop in the middle is missing, tried reading a prop on it
525
                // ex: variablePathEntries = ["there", "notThere", "definitelyNotThere"]
526
                throw new Error(`Cannot read '${variablePathEntries[missingKeyIndex]}'${parentVarType ? ` on type '${parentVarTypeText}'` : ''}`);
2!
527
            }
528

529
            if (!util.isNullish(response.data.errorData.invalidPathIndex)) {
2!
530
                const { invalidPathIndex } = response.data.errorData;
2✔
531

532
                //leftmost var is literal `invalid`, tried to read it
533
                if (variablePathEntries.length === 1 && invalidPathIndex === 0) {
2!
534
                    variable.name = variablePathEntries[variablePathEntries.length - 1];
×
535
                    variable.type = VariableType.Invalid;
×
536
                    return simulatedResponse;
×
537
                }
538

539
                if (variablePathEntries.length > 1 && invalidPathIndex > 0) {
2✔
540
                    await loadParentVarInfo(invalidPathIndex);
1✔
541

542
                    //leftmost var is set to literal `invalid`, tried to read prop
543
                    if (invalidPathIndex === 0 && variablePathEntries.length > 1) {
1!
544
                        throw new Error(`Cannot read '${variablePathEntries[invalidPathIndex + 1]}' on type '${parentVarTypeText}'`);
×
545
                    }
546

547
                    // prop at the end doesn't exist. Treat like `invalid`.
548
                    // ex: variablePathEntries = ['there', 'notThere']
549
                    if (
1!
550
                        invalidPathIndex === variablePathEntries.length - 1 &&
1!
551
                        [VariableType.AssociativeArray, VariableType.SubtypedObject].includes(parentVarType)
552
                    ) {
553
                        variable.name = variablePathEntries[variablePathEntries.length - 1];
×
554
                        variable.type = VariableType.Invalid;
×
555
                        variable.value = 'Invalid (not defined)';
×
556
                        return simulatedResponse;
×
557
                    }
558
                }
559
                //prop in the middle is missing, tried reading a prop on it
560
                // ex: variablePathEntries = ["there", "notThere", "definitelyNotThere"]
561
                throw new Error(`Cannot read '${variablePathEntries[invalidPathIndex]}'${parentVarType ? ` on type '${parentVarTypeText}'` : ''}`);
2✔
562

563
            }
564
        }
565
        return response;
9✔
566
    }
567

568
    private async processVariablesRequest(request: VariablesRequest) {
569
        if (this.isStopped && request.data.threadIndex > -1) {
16✔
570
            return this.sendRequest<VariablesResponse>(request);
15✔
571
        }
572
    }
573

574
    public async executeCommand(sourceCode: string, stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) {
2✔
575
        return this.processExecuteRequest(
2✔
576
            ExecuteRequest.fromJson({
577
                requestId: this.requestIdSequence++,
578
                threadIndex: threadIndex,
579
                stackFrameIndex: stackFrameIndex,
580
                sourceCode: sourceCode
581
            })
582
        );
583
    }
584

585
    private async processExecuteRequest(request: ExecuteRequest) {
586
        if (this.isStopped && request.data.threadIndex > -1) {
2✔
587
            return this.sendRequest<ExecuteV3Response>(request);
1✔
588
        }
589
    }
590

591
    public async addBreakpoints(breakpoints: Array<BreakpointSpec & { componentLibraryName?: string }>): Promise<AddBreakpointsResponse> {
592
        const { enableComponentLibrarySpecificBreakpoints } = this;
9✔
593
        if (breakpoints?.length > 0) {
9!
594
            const json = {
7✔
595
                requestId: this.requestIdSequence++,
596
                breakpoints: breakpoints.map(x => {
597
                    let breakpoint = {
8✔
598
                        ...x,
599
                        ignoreCount: x.hitCount
600
                    };
601
                    if (enableComponentLibrarySpecificBreakpoints && breakpoint.componentLibraryName) {
8✔
602
                        breakpoint.filePath = breakpoint.filePath.replace(/^pkg:\//i, `lib:/${breakpoint.componentLibraryName}/`);
1✔
603
                    }
604
                    return breakpoint;
8✔
605
                })
606
            };
607

608
            const useConditionalBreakpoints = (
609
                //does this protocol version support conditional breakpoints?
610
                this.supportsConditionalBreakpoints &&
7✔
611
                //is there at least one conditional breakpoint present?
612
                !!breakpoints.find(x => !!x?.conditionalExpression?.trim())
6!
613
            );
614

615
            let response: AddBreakpointsResponse | AddConditionalBreakpointsResponse;
616
            if (useConditionalBreakpoints) {
7✔
617
                response = await this.sendRequest<AddBreakpointsResponse>(
2✔
618
                    AddConditionalBreakpointsRequest.fromJson(json)
619
                );
620
            } else {
621
                response = await this.sendRequest<AddBreakpointsResponse>(
5✔
622
                    AddBreakpointsRequest.fromJson(json)
623
                );
624
            }
625

626
            //if the device does not support breakpoint verification, then auto-mark all of these as verified
627
            if (!this.supportsBreakpointVerification) {
7!
628
                this.emit('breakpoints-verified', {
7✔
629
                    breakpoints: response.data.breakpoints
630
                });
631
            }
632
            return response;
7✔
633
        }
634
        return AddBreakpointsResponse.fromBuffer(null);
2✔
635
    }
636

637
    public async listBreakpoints(): Promise<ListBreakpointsResponse> {
638
        return this.processRequest<ListBreakpointsResponse>(
4✔
639
            ListBreakpointsRequest.fromJson({
640
                requestId: this.requestIdSequence++
641
            })
642
        );
643
    }
644

645
    /**
646
     * Remove breakpoints having the specified IDs
647
     */
648
    public async removeBreakpoints(breakpointIds: number[]) {
649
        return this.processRemoveBreakpointsRequest(
2✔
650
            RemoveBreakpointsRequest.fromJson({
651
                requestId: this.requestIdSequence++,
652
                breakpointIds: breakpointIds
653
            })
654
        );
655
    }
656

657
    private async processRemoveBreakpointsRequest(request: RemoveBreakpointsRequest) {
658
        if (request.data.breakpointIds?.length > 0) {
2✔
659
            return this.sendRequest<RemoveBreakpointsResponse>(request);
1✔
660
        }
661
        return RemoveBreakpointsResponse.fromJson(null);
1✔
662
    }
663

664
    /**
665
     * Given a request, process it in the proper fashion. This is mostly used for external mocking/testing of
666
     * this client, but it should force the client to flow in the same fashion as a live debug session
667
     */
668
    public async processRequest<TResponse extends ProtocolResponse>(request: ProtocolRequest): Promise<TResponse> {
669
        switch (request?.constructor.name) {
4!
670
            case ContinueRequest.name:
671
                return this.processContinueRequest(request as ContinueRequest) as any;
×
672

673
            case ExecuteRequest.name:
674
                return this.processExecuteRequest(request as ExecuteRequest) as any;
×
675

676
            case HandshakeRequest.name:
677
                return this.processHandshakeRequest(request as HandshakeRequest) as any;
×
678

679
            case RemoveBreakpointsRequest.name:
680
                return this.processRemoveBreakpointsRequest(request as RemoveBreakpointsRequest) as any;
×
681

682
            case StackTraceRequest.name:
683
                return this.processStackTraceRequest(request as StackTraceRequest) as any;
×
684

685
            case StepRequest.name:
686
                return this.processStepRequest(request as StepRequest) as any;
×
687

688
            case StopRequest.name:
689
                return this.processStopRequest(request as StopRequest) as any;
×
690

691
            case ThreadsRequest.name:
692
                return this.processThreadsRequest(request as ThreadsRequest) as any;
×
693

694
            case VariablesRequest.name:
695
                return this.processVariablesRequest(request as VariablesRequest) as any;
×
696

697
            //for all other request types, there's no custom business logic, so just pipe them through manually
698
            case AddBreakpointsRequest.name:
699
            case AddConditionalBreakpointsRequest.name:
700
            case ExitChannelRequest.name:
701
            case ListBreakpointsRequest.name:
702
                return this.sendRequest(request);
4✔
703
            default:
704
                this.logger.log('Unknown request type. Sending anyway...', request);
×
705
                //unknown request type. try sending it as-is
706
                return this.sendRequest(request);
×
707
        }
708
    }
709

710
    /**
711
     * Send a request to the roku device, and get a promise that resolves once we have received the response
712
     */
713
    private async sendRequest<T extends ProtocolResponse | ProtocolUpdate>(request: ProtocolRequest) {
714
        request = (await this.plugins.emit('beforeSendRequest', {
106✔
715
            client: this,
716
            request: request
717
        })).request;
718

719
        this.activeRequests.set(request.data.requestId, request);
106✔
720

721
        return new Promise<T>((resolve) => {
106✔
722
            let unsubscribe = this.on('response', (response) => {
106✔
723
                if (response.data.requestId === request.data.requestId) {
106✔
724
                    unsubscribe();
105✔
725
                    this.activeRequests.delete(request.data.requestId);
105✔
726
                    resolve(response as T);
105✔
727
                }
728
            });
729

730
            this.logger.log(`Request ${request?.data?.requestId}`, request);
106!
731
            if (this.controlSocket) {
106✔
732
                const buffer = request.toBuffer();
105✔
733
                this.logger.info('received client-to-server data\n', { type: 'client-to-server', data: buffer.toJSON() });
105✔
734
                this.controlSocket.write(buffer);
105✔
735
                void this.plugins.emit('afterSendRequest', {
105✔
736
                    client: this,
737
                    request: request
738
                });
739
            } else {
740
                throw new Error(`Control socket was closed - Command: ${Command[request.data.command]}`);
1✔
741
            }
742
        });
743
    }
744

745
    /**
746
     * Sometimes a request arrives that we don't understand. If that's the case, this function can be used
747
     * to discard that entire response by discarding `packet_length` number of bytes
748
     */
749
    private discardNextResponseOrUpdate() {
750
        const response = GenericV3Response.fromBuffer(this.buffer);
1✔
751
        if (response.success && response.data.packetLength > 0) {
1!
752
            this.logger.warn(`Unsupported response or updated encountered. Discarding ${response.data.packetLength} bytes:`, JSON.stringify(
×
753
                this.buffer.slice(0, response.data.packetLength + 1).toJSON().data
754
            ));
755
            //we have a valid event. Clear the buffer of this data
756
            this.buffer = this.buffer.slice(response.data.packetLength);
×
757
        }
758
    }
759

760
    private async process(): Promise<boolean> {
761
        try {
162✔
762
            this.logger.info('[process()]: buffer=', this.buffer.toJSON());
162✔
763

764
            let { responseOrUpdate } = await this.plugins.emit('provideResponseOrUpdate', {
162✔
765
                client: this,
766
                activeRequests: this.activeRequests,
767
                buffer: this.buffer
768
            });
769

770
            if (!responseOrUpdate) {
162!
771
                responseOrUpdate = this.getResponseOrUpdate(this.buffer);
162✔
772
            }
773

774
            //if the event failed to parse, or the buffer doesn't have enough bytes to satisfy the packetLength, exit here (new data will re-trigger this function)
775
            if (!responseOrUpdate) {
162✔
776
                this.logger.info('Unable to convert buffer into anything meaningful', this.buffer);
1✔
777
                //if we have packet length, and we have at least that many bytes, throw out this message so we can hopefully recover
778
                this.discardNextResponseOrUpdate();
1✔
779
                return false;
1✔
780
            }
781
            if (!responseOrUpdate.success || responseOrUpdate.data.packetLength > this.buffer.length) {
161!
782
                this.logger.log(`event parse failed. ${responseOrUpdate?.data?.packetLength} bytes required, ${this.buffer.length} bytes available`);
×
783
                return false;
×
784
            }
785

786
            //we have a valid event. Remove this data from the buffer
787
            this.buffer = this.buffer.slice(responseOrUpdate.readOffset);
161✔
788

789
            if (responseOrUpdate.data.errorCode !== ErrorCode.OK) {
161✔
790
                this.logger.error(responseOrUpdate.data.errorCode, responseOrUpdate);
9✔
791
            }
792

793
            //we got a result
794
            if (responseOrUpdate) {
161!
795
                //emit the corresponding event
796
                if (isProtocolUpdate(responseOrUpdate)) {
161✔
797
                    this.logger.log(`Update:`, responseOrUpdate);
56✔
798
                    this.emit('update', responseOrUpdate);
56✔
799
                    await this.plugins.emit('onUpdate', {
56✔
800
                        client: this,
801
                        update: responseOrUpdate
802
                    });
803
                } else {
804
                    this.logger.log(`Response ${responseOrUpdate?.data?.requestId}:`, responseOrUpdate);
105!
805
                    this.emit('response', responseOrUpdate);
105✔
806
                    await this.plugins.emit('onResponse', {
105✔
807
                        client: this,
808
                        response: responseOrUpdate as any
809
                    });
810
                }
811
                return true;
161✔
812
            }
813
        } catch (e) {
814
            this.logger.error(`process() failed:`, e);
×
815
        }
816
    }
817

818
    /**
819
     * Given a buffer, try to parse into a specific ProtocolResponse or ProtocolUpdate
820
     */
821
    public getResponseOrUpdate(buffer: Buffer): ProtocolResponse | ProtocolUpdate {
822
        //if we haven't seen a handshake yet, try to convert the buffer into a handshake
823
        if (!this.isHandshakeComplete) {
162✔
824
            let handshake: HandshakeV3Response | HandshakeResponse;
825
            //try building the v3 handshake response first
826
            handshake = HandshakeV3Response.fromBuffer(buffer);
52✔
827
            //we didn't get a v3 handshake. try building an older handshake response
828
            if (!handshake.success) {
52✔
829
                handshake = HandshakeResponse.fromBuffer(buffer);
1✔
830
            }
831
            if (handshake.success) {
52!
832
                this.verifyHandshake(handshake);
52✔
833
                return handshake;
52✔
834
            }
835
            return;
×
836
        }
837

838
        let genericResponse = this.watchPacketLength ? GenericV3Response.fromBuffer(buffer) : GenericResponse.fromBuffer(buffer);
110!
839

840
        //if the response has a non-OK error code, we won't receive the expected response type,
841
        //so return the generic response
842
        if (genericResponse.success && genericResponse.data.errorCode !== ErrorCode.OK) {
110✔
843
            return genericResponse;
9✔
844
        }
845
        // a nonzero requestId means this is a response to a request that we sent
846
        if (genericResponse.data.requestId !== 0) {
101✔
847
            //requestId 0 means this is an update
848
            const request = this.activeRequests.get(genericResponse.data.requestId);
45✔
849
            if (request) {
45✔
850
                return DebugProtocolClient.getResponse(this.buffer, request.data.command);
44✔
851
            }
852
        } else {
853
            return this.getUpdate(this.buffer);
56✔
854
        }
855
    }
856

857
    public static getResponse(buffer: Buffer, command: Command) {
858
        switch (command) {
44!
859
            case Command.Stop:
860
            case Command.Continue:
861
            case Command.Step:
862
            case Command.ExitChannel:
863
                return GenericV3Response.fromBuffer(buffer);
9✔
864
            case Command.Execute:
865
                return ExecuteV3Response.fromBuffer(buffer);
1✔
866
            case Command.AddBreakpoints:
867
            case Command.AddConditionalBreakpoints:
868
                return AddBreakpointsResponse.fromBuffer(buffer);
7✔
869
            case Command.ListBreakpoints:
870
                return ListBreakpointsResponse.fromBuffer(buffer);
3✔
871
            case Command.RemoveBreakpoints:
872
                return RemoveBreakpointsResponse.fromBuffer(buffer);
1✔
873
            case Command.Variables:
874
                return VariablesResponse.fromBuffer(buffer);
8✔
875
            case Command.StackTrace:
876
                return StackTraceV3Response.fromBuffer(buffer);
7✔
877
            case Command.Threads:
878
                return ThreadsResponse.fromBuffer(buffer);
8✔
879
            default:
880
                return undefined;
×
881
        }
882
    }
883

884
    public getUpdate(buffer: Buffer): ProtocolUpdate {
885
        //read the update_type from the buffer (save some buffer parsing time by narrowing to the exact update type)
886
        const updateTypeCode = buffer.readUInt32LE(
56✔
887
            // if the protocol supports packet length, then update_type is bytes 12-16. Otherwise, it's bytes 8-12
888
            this.watchPacketLength ? 12 : 8
56!
889
        );
890
        const updateType = UpdateTypeCode[updateTypeCode] as UpdateType;
56✔
891

892
        this.logger?.log('getUpdate(): update Type:', updateType);
56!
893
        switch (updateType) {
56!
894
            case UpdateType.IOPortOpened:
895
                //TODO handle this
896
                return IOPortOpenedUpdate.fromBuffer(buffer);
3✔
897
            case UpdateType.AllThreadsStopped:
898
                return AllThreadsStoppedUpdate.fromBuffer(buffer);
50✔
899
            case UpdateType.ThreadAttached:
900
                return ThreadAttachedUpdate.fromBuffer(buffer);
3✔
901
            case UpdateType.BreakpointError:
902
                //we do nothing with breakpoint errors at this time.
903
                return BreakpointErrorUpdate.fromBuffer(buffer);
×
904
            case UpdateType.CompileError:
905
                return CompileErrorUpdate.fromBuffer(buffer);
×
906
            case UpdateType.BreakpointVerified:
907
                let response = BreakpointVerifiedUpdate.fromBuffer(buffer);
×
908
                if (response?.data?.breakpoints?.length > 0) {
×
909
                    this.emit('breakpoints-verified', response.data);
×
910
                }
911
                return response;
×
912
            default:
913
                return undefined;
×
914
        }
915
    }
916

917
    private handleUpdateQueue = new ActionQueue();
64✔
918

919
    /**
920
     * Handle/process any received updates from the debug protocol
921
     */
922
    private async handleUpdate(update: ProtocolUpdate) {
923
        return this.handleUpdateQueue.run(async () => {
56✔
924
            update = (await this.plugins.emit('beforeHandleUpdate', {
56✔
925
                client: this,
926
                update: update
927
            })).update;
928

929
            if (update instanceof AllThreadsStoppedUpdate || update instanceof ThreadAttachedUpdate) {
56✔
930
                this.isStopped = true;
53✔
931

932
                let eventName: 'runtime-error' | 'suspend';
933
                if (update.data.stopReason === StopReason.RuntimeError) {
53!
934
                    eventName = 'runtime-error';
×
935
                } else {
936
                    eventName = 'suspend';
53✔
937
                }
938

939
                const isValidStopReason = [StopReason.RuntimeError, StopReason.Break, StopReason.StopStatement].includes(update.data.stopReason);
53✔
940

941
                if (update instanceof AllThreadsStoppedUpdate && isValidStopReason) {
53✔
942
                    this.primaryThread = update.data.threadIndex;
50✔
943
                    this.stackFrameIndex = 0;
50✔
944
                    this.emit(eventName, update);
50✔
945
                } else if (update instanceof ThreadAttachedUpdate && isValidStopReason) {
3!
946
                    this.primaryThread = update.data.threadIndex;
3✔
947
                    this.emit(eventName, update);
3✔
948
                }
949

950
            } else if (isIOPortOpenedUpdate(update)) {
3!
951
                this.connectToIoPort(update);
3✔
952
            }
953
            return true;
56✔
954
        });
955
    }
956

957
    /**
958
     * Verify all the handshake data
959
     */
960
    private verifyHandshake(response: HandshakeResponse | HandshakeV3Response): boolean {
961
        if (DebugProtocolClient.DEBUGGER_MAGIC === response.data.magic) {
52✔
962
            this.logger.log('Magic is valid.');
51✔
963

964
            this.protocolVersion = response.data.protocolVersion;
51✔
965
            this.logger.log('Protocol Version:', this.protocolVersion);
51✔
966

967
            this.watchPacketLength = semver.satisfies(this.protocolVersion, '>=3.0.0');
51✔
968
            this.isHandshakeComplete = true;
51✔
969

970
            let handshakeVerified = true;
51✔
971

972
            if (semver.satisfies(this.protocolVersion, this.supportedVersionRange)) {
51✔
973
                this.logger.log('supported');
1✔
974
                this.emit('protocol-version', {
1✔
975
                    message: `Protocol Version ${this.protocolVersion} is supported!`,
976
                    errorCode: PROTOCOL_ERROR_CODES.SUPPORTED
977
                });
978
            } else if (semver.gtr(this.protocolVersion, this.supportedVersionRange)) {
50!
979
                this.logger.log('roku-debug has not been tested against protocol version', this.protocolVersion);
50✔
980
                this.emit('protocol-version', {
50✔
981
                    message: `Protocol Version ${this.protocolVersion} has not been tested and my not work as intended.\nPlease open any issues you have with this version to https://github.com/rokucommunity/roku-debug/issues`,
982
                    errorCode: PROTOCOL_ERROR_CODES.NOT_TESTED
983
                });
984
            } else {
985
                this.logger.log('not supported');
×
986
                this.emit('protocol-version', {
×
987
                    message: `Protocol Version ${this.protocolVersion} is not supported.\nIf you believe this is an error please open an issue at https://github.com/rokucommunity/roku-debug/issues`,
988
                    errorCode: PROTOCOL_ERROR_CODES.NOT_SUPPORTED
989
                });
990
                this.shutdown('close');
×
991
                handshakeVerified = false;
×
992
            }
993

994
            this.emit('handshake-verified', handshakeVerified);
51✔
995
            return handshakeVerified;
51✔
996
        } else {
997
            this.logger.log('Closing connection due to bad debugger magic', response.data.magic);
1✔
998
            this.emit('handshake-verified', false);
1✔
999
            this.shutdown('close');
1✔
1000
            return false;
1✔
1001
        }
1002
    }
1003

1004
    /**
1005
     * When the debugger emits the IOPortOpenedUpdate, we need to immediately connect to the IO port to start receiving that data
1006
     */
1007
    private connectToIoPort(update: IOPortOpenedUpdate) {
1008
        if (update.success) {
4✔
1009
            // Create a new TCP client.
1010
            this.ioSocket = new Net.Socket();
3✔
1011
            // Send a connection request to the server.
1012
            this.logger.log(`Connect to IO Port ${this.options.host}:${update.data.port}`);
3✔
1013
            this.ioSocket.connect({
3✔
1014
                port: update.data.port,
1015
                host: this.options.host
1016
            }, () => {
1017
                // If there is no error, the server has accepted the request
1018
                this.logger.log('TCP connection established with the IO Port.');
3✔
1019
                this.connectedToIoPort = true;
3✔
1020

1021
                let lastPartialLine = '';
3✔
1022
                this.ioSocket.on('data', (buffer) => {
3✔
1023
                    this.logger.info('received IO data\n', { type: 'io', data: buffer.toJSON() });
3✔
1024
                    let responseText = buffer.toString();
3✔
1025
                    if (!responseText.endsWith('\n')) {
3✔
1026
                        // buffer was split, save the partial line
1027
                        lastPartialLine += responseText;
1✔
1028
                    } else {
1029
                        if (lastPartialLine) {
2✔
1030
                            // there was leftover lines, join the partial lines back together
1031
                            responseText = lastPartialLine + responseText;
1✔
1032
                            lastPartialLine = '';
1✔
1033
                        }
1034
                        // Emit the completed io string.
1035
                        this.emit('io-output', responseText.trim());
2✔
1036
                    }
1037
                });
1038

1039
                this.ioSocket.on('end', () => {
3✔
1040
                    this.ioSocket.end();
3✔
1041
                    this.logger.log('Requested an end to the IO connection');
3✔
1042
                });
1043

1044
                // Don't forget to catch error, for your own sake.
1045
                this.ioSocket.once('error', (err) => {
3✔
1046
                    this.ioSocket.end();
×
1047
                    this.logger.error(err);
×
1048
                });
1049
            });
1050
            return true;
3✔
1051
        }
1052
        return false;
1✔
1053
    }
1054

1055
    public destroy() {
1056
        this.shutdown('close');
57✔
1057
    }
1058

1059
    private shutdown(eventName: 'app-exit' | 'close') {
1060
        if (this.controlSocket) {
65✔
1061
            this.controlSocket.removeAllListeners();
51✔
1062
            this.controlSocket.destroy();
51✔
1063
            this.controlSocket = undefined;
51✔
1064
        }
1065

1066
        if (this.ioSocket) {
65✔
1067
            this.ioSocket.removeAllListeners();
3✔
1068
            this.ioSocket.destroy();
3✔
1069
            this.ioSocket = undefined;
3✔
1070
        }
1071

1072
        this.emit(eventName);
65✔
1073
    }
1074
}
1075

1076
export interface ProtocolVersionDetails {
1077
    message: string;
1078
    errorCode: PROTOCOL_ERROR_CODES;
1079
}
1080

1081
export interface BreakpointSpec {
1082
    /**
1083
     * The path of the source file where the breakpoint is to be inserted.
1084
     */
1085
    filePath: string;
1086
    /**
1087
     * The (1-based) line number in the channel application code where the breakpoint is to be executed.
1088
     */
1089
    lineNumber: number;
1090
    /**
1091
     * The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint.
1092
     */
1093
    hitCount?: number;
1094
    /**
1095
     * BrightScript code that evaluates to a boolean value. The expression is compiled and executed in
1096
     * the context where the breakpoint is located. If specified, the hitCount is only be
1097
     * updated if this evaluates to true.
1098
     * @avaiable since protocol version 3.1.0
1099
     */
1100
    conditionalExpression?: string;
1101
}
1102

1103
export interface ConstructorOptions {
1104
    /**
1105
     * The host/ip address of the Roku
1106
     */
1107
    host: string;
1108
    /**
1109
     * The port number used to send all debugger commands. This is static/unchanging for Roku devices,
1110
     * but is configurable here to support unit testing or alternate runtimes (i.e. https://www.npmjs.com/package/brs)
1111
     */
1112
    controlPort?: number;
1113
    /**
1114
     * The interval (in milliseconds) for how frequently the `connect`
1115
     * call should retry connecting to the control port. At the start of a debug session,
1116
     * the protocol debugger will start trying to connect the moment the channel is sideloaded,
1117
     * and keep trying until a successful connection is established or the debug session is terminated
1118
     * @default 250
1119
     */
1120
    controlConnectInterval?: number;
1121
    /**
1122
     * The maximum time (in milliseconds) the debugger will keep retrying connections.
1123
     * This is here to prevent infinitely pinging the Roku device.
1124
     */
1125
    controlConnectMaxTime?: number;
1126
}
1127

1128
/**
1129
 * Is the event a ProtocolUpdate update
1130
 */
1131
export function isProtocolUpdate(event: ProtocolUpdate | ProtocolResponse): event is ProtocolUpdate {
1✔
1132
    return event?.constructor?.name.endsWith('Update') && event?.data?.requestId === 0;
266!
1133
}
1134
/**
1135
 * Is the event a ProtocolResponse
1136
 */
1137
export function isProtocolResponse(event: ProtocolUpdate | ProtocolResponse): event is ProtocolResponse {
1✔
1138
    return event?.constructor?.name.endsWith('Response') && event?.data?.requestId !== 0;
×
1139
}
1140

1141
export interface BreakpointsVerifiedEvent {
1142
    breakpoints: VerifiedBreakpoint[];
1143
}
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