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

rokucommunity / roku-debug / 26120672946

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

Pull #351

github

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

3328 of 5046 branches covered (65.95%)

Branch coverage included in aggregate %.

5834 of 7908 relevant lines covered (73.77%)

35.01 hits per line

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

54.82
/src/debugSession/BrightScriptDebugSession.ts
1
import * as fsExtra from 'fs-extra';
2
import { orderBy } from 'natural-orderby';
2✔
3
import * as path from 'path';
2✔
4
import * as semver from 'semver';
2✔
5
import { rokuDeploy, CompileError, isUpdateCheckRequiredError, isConnectionResetError, EcpNetworkAccessModeDisabledError } from 'roku-deploy';
2✔
6
import type { DeviceInfo, RokuDeploy, RokuDeployOptions } from 'roku-deploy';
2✔
7
import {
2✔
8
    BreakpointEvent,
2✔
9
    LoggingDebugSession,
2✔
10
    Logger as DapLogger,
2✔
11
    logger as dapLogger,
2✔
12
    CapabilitiesEvent,
2✔
13
    InitializedEvent,
2✔
14
    InvalidatedEvent,
2✔
15
    OutputEvent,
2✔
16
    ProgressEndEvent,
2✔
17
    ProgressStartEvent,
2✔
18
    ProgressUpdateEvent,
2✔
19
    Source,
2✔
20
    StackFrame,
2✔
21
    StoppedEvent,
2✔
22
    TerminatedEvent,
2✔
23
    Thread,
2✔
24
    Variable
2✔
25
} from '@vscode/debugadapter';
2✔
26
import type { SceneGraphCommandResponse } from '../SceneGraphDebugCommandController';
2✔
27
import { SceneGraphDebugCommandController } from '../SceneGraphDebugCommandController';
2✔
28
import type { DebugProtocol } from '@vscode/debugprotocol';
2✔
29
import { defer, util } from '../util';
2✔
30
import { fileUtils, standardizePath as s } from '../FileUtils';
2✔
31
import { ComponentLibraryServer } from '../ComponentLibraryServer';
2✔
32
import { ProjectManager, Project, ComponentLibraryProject } from '../managers/ProjectManager';
2✔
33
import type { EvaluateContainer } from '../adapters/DebugProtocolAdapter';
2✔
34
import { DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter';
2✔
35
import { TelnetAdapter } from '../adapters/TelnetAdapter';
36
import type { BSDebugDiagnostic } from '../CompileErrorProcessor';
37
import { RendezvousTracker } from '../RendezvousTracker';
144✔
38
import {
144✔
39
    LaunchStartEvent,
144✔
40
    LogOutputEvent,
41
    RendezvousEvent,
42
    DiagnosticsEvent,
43
    StoppedEventReason,
144✔
44
    ChanperfEvent,
144✔
45
    DebugServerLogOutputEvent,
144✔
46
    ChannelPublishedEvent,
47
    CustomRequestEvent,
144✔
48
    ClientToServerCustomEventName,
144✔
49
    ProfilingErrorEvent,
144✔
50
    ProfilingStartEvent,
51
    ProfilingStopEvent,
52
    ProfilingEnabledEvent as ProfilingEnableEvent,
53
    ProcessCrashEvent
144✔
54
} from './Events';
55
import type { ProcessCrashEventData } from './Events';
56
import type { LaunchConfiguration, ComponentLibraryConfiguration } from '../LaunchConfiguration';
57
import { FileManager } from '../managers/FileManager';
144✔
58
import { SourceMapManager } from '../managers/SourceMapManager';
144✔
59
import { LocationManager } from '../managers/LocationManager';
144✔
60
import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager';
144✔
61
import { BreakpointManager } from '../managers/BreakpointManager';
144✔
62
import type { LogMessage } from '../logging';
63
import { PerfettoManager } from '../PerfettoManager';
64
import { logger, FileLoggingManager, debugServerLogOutputEventTransport, LogLevelPriority } from '../logging';
65
import { VariableType } from '../debugProtocol/events/responses/VariablesResponse';
144✔
66
import { DiagnosticSeverity } from 'brighterscript';
144✔
67
import type { ExceptionBreakpoint } from '../debugProtocol/events/requests/SetExceptionBreakpointsRequest';
144✔
68
import { debounce } from 'debounce';
144✔
69
import { interfaces, components, events } from 'brighterscript/dist/roku-types';
144✔
70
import { globalCallables } from 'brighterscript/dist/globalCallables';
×
71
import { bscProjectWorkerPool } from '../bsc/threading/BscProjectWorkerPool';
72
import { populateVariableFromRegistryEcp } from './ecpRegistryUtils';
144✔
73
import { AppState, rokuECP } from '../RokuECP';
144✔
74
import { SocketConnectionInUseError } from '../Exceptions';
144✔
75

76
const diagnosticSource = 'roku-debug';
77

78
export class BrightScriptDebugSession extends LoggingDebugSession {
144✔
79
    public constructor() {
144✔
80
        super();
81

144✔
82
        // this debugger uses one-based lines and columns
144✔
83
        this.setDebuggerLinesStartAt1(false);
84
        this.setDebuggerColumnsStartAt1(false);
144✔
85

144✔
86
        //give util a reference to this session to assist in logging across the entire module
144✔
87
        util._debugSession = this;
144✔
88

144✔
89
        this.fileManager = new FileManager();
90
        this.sourceMapManager = new SourceMapManager();
144✔
91
        this.locationManager = new LocationManager(this.sourceMapManager);
144✔
92
        this.breakpointManager = new BreakpointManager(this.sourceMapManager, this.locationManager);
93
        //send newly-verified breakpoints to vscode
94
        this.breakpointManager.on('breakpoints-verified', (data) => this.onDeviceBreakpointsChanged('changed', data));
95
        this.projectManager = new ProjectManager({
144✔
96
            breakpointManager: this.breakpointManager,
97
            locationManager: this.locationManager
98
        });
2✔
99
        this.fileLoggingManager = new FileLoggingManager();
100
    }
101

102
    public start(inStream: NodeJS.ReadableStream, outStream: NodeJS.WritableStream): void {
103
        super.start(inStream, outStream);
104
        // Set up DAP protocol logging as early as possible — immediately after start() so we capture
2✔
105
        // the initialize request and all early DAP traffic before launchRequest config is available.
2✔
106
        // The log file path is injected as ROKU_DAP_LOG_FILE by the extension's DebugAdapterDescriptorFactory,
107
        // which resolves the path from the `brightscript.debug.debugAdapterProtocolLogging` workspace setting
108
        // (or the equivalent launch.json property) before the debug adapter process is spawned.
109
        const dapLogFile = process.env.ROKU_DAP_LOG_FILE;
110
        if (dapLogFile) {
111
            // Use LogLevel.Error (not Verbose) as the console threshold so DAP messages are written
1✔
112
            // to the log file but are NOT forwarded to VS Code as OutputEvents, which would flood
113
            // the debug console and break the extension's output parsing.
114
            // Note: InternalLogger always writes ALL messages to the file stream regardless of level,
115
            // so the log file will still contain everything.
16✔
116
            dapLogger.setup(DapLogger.LogLevel.Error, dapLogFile);
1✔
117
        }
118
    }
15✔
119

15✔
120
    public setupProcessErrorHandlers() {
121
        if (this.processErrorHandlersRegistered) {
14✔
122
            return;
14✔
123
        }
14✔
124
        this.processErrorHandlersRegistered = true;
14✔
125

126
        const handleError = (type: 'uncaughtException' | 'unhandledRejection', error: unknown) => {
127
            const logger = this.logger.createLogger(`${type}`);
128
            const message = error instanceof Error ? error.message : String(error);
14✔
129
            const stack = error instanceof Error ? error.stack : undefined;
14✔
130
            logger.error(message, stack);
13✔
131

13✔
132
            let output: string;
133
            let debuggerVersion: string;
134
            let additionalInfo: ProcessCrashEventData['additionalInfo'];
39!
135
            try {
39!
136
                debuggerVersion = (fsExtra.readJsonSync(path.resolve(__dirname, '../../package.json')) as { version: string }).version;
13!
137

39!
138
                const clientName = this.initRequestArgs?.clientName ?? 'unknown';
139

140
                additionalInfo = {
13✔
141
                    clientName: clientName,
142
                    rokuDebugVersion: debuggerVersion,
91✔
143
                    ecpMode: this.deviceInfo?.ecpSettingMode,
91✔
144
                    developerMode: this.deviceInfo?.developerEnabled,
91✔
145
                    firmware: this.deviceInfo ? `${this.deviceInfo?.softwareVersion}.${this.deviceInfo?.softwareBuild}` : undefined,
146
                    protocolVersion: this.deviceInfo?.brightscriptDebuggerVersion,
13✔
147
                    protocolEnabled: this.enableDebugProtocol
148
                };
149

150
                const lines = Object.entries(additionalInfo as Record<string, unknown>).map(([key, value]) => {
151
                    // Insert a space before all uppercase letters preceded by a lowercase letter, then uppercase the first char
152
                    const spacedString = key.replace(/([a-z])([A-Z])/g, '$1 $2');
153
                    const formattedKey = spacedString.charAt(0).toUpperCase() + spacedString.slice(1);
154
                    return `**${formattedKey}:** ${JSON.stringify(value)}`;
155
                });
156

157
                const issueBodyPrefix = [
158
                    `**Error type:** ${type}`,
13✔
159
                    `**Message:** ${message}`,
13✔
160
                    ...lines,
13✔
161
                    '',
13✔
162
                    `**Steps to reproduce:**`,
13✔
163
                    `<!-- Please describe what you were doing when this crash occurred -->`,
13✔
164
                    '',
13✔
165
                    '**Stack trace:**',
13✔
166
                    '```',
13✔
167
                    ''
168
                ].join('\n');
13✔
169
                const issueBodySuffix = '\n```';
2✔
170

171
                const issueTitle = encodeURIComponent(`[crash] ${type}: ${message}`);
11✔
172
                const baseUrl = 'https://github.com/RokuCommunity/roku-debug/issues/new';
3✔
173
                const maxUrlLength = 2000;
174
                const urlOverhead = `${baseUrl}?title=${issueTitle}&body=`.length;
175
                const bodyBudget = maxUrlLength - urlOverhead;
8✔
176
                const encodedPrefix = encodeURIComponent(issueBodyPrefix);
177
                const encodedSuffix = encodeURIComponent(issueBodySuffix);
9✔
178
                const stackBudget = bodyBudget - encodedPrefix.length - encodedSuffix.length;
9✔
179
                let truncatedStack: string;
180
                if (!stack) {
181
                    truncatedStack = '(no stack trace)';
182
                } else if (encodeURIComponent(stack).length <= stackBudget) {
183
                    truncatedStack = stack;
184
                } else {
185
                    truncatedStack = decodeURIComponent(encodeURIComponent(stack).slice(0, stackBudget)) + '\n...(truncated)';
186
                }
63✔
187
                const issueUrl = `${baseUrl}?title=${issueTitle}&body=${encodedPrefix}${encodeURIComponent(truncatedStack)}${encodedSuffix}`;
188

189
                output = [
57✔
190
                    '',
191
                    '================================================================',
192
                    '\tBRIGHTSCRIPT DEBUGGER INTERNAL ERROR',
193
                    '\tThis is a crash in the debug adapter, not in your application.',
194
                    '================================================================',
195
                    `\tError type: ${type}`,
196
                    `\tMessage: ${message}`,
197
                    ...lines.map(l => `\t${l}`),
198
                    '',
5✔
199
                    '\tStack trace:',
200
                    ...(stack ?? '(no stack trace)').split('\n').map(l => `\t${l}`),
201
                    '',
202
                    '\tPlease report this at:',
203
                    `\t${issueUrl}`,
204
                    '================================================================',
14✔
205
                    ''
14✔
206
                ].join('\n');
14✔
207
            } catch (e) {
14✔
208
                output = JSON.stringify({
209
                    name: e.name,
15✔
210
                    message: e.message,
15✔
211
                    stack: e.stack
15✔
212
                });
15✔
213
            }
214

215
            void this.sendLogOutput(output).catch(() => { /** best-effort */ });
30✔
216
            this.isCrashed = true;
15✔
217
            this.sendEvent(new ProcessCrashEvent({ type, message, stack, additionalInfo: additionalInfo ?? {} }));
15✔
218
            setTimeout(() => void this.shutdown(), 5000);
219
        };
30✔
220

15✔
221
        this._uncaughtExceptionHandler = (error) => handleError('uncaughtException', error);
15✔
222
        this._unhandledRejectionHandler = (reason) => handleError('unhandledRejection', reason);
223

30✔
224
        process.on('uncaughtException', this._uncaughtExceptionHandler);
225
        process.on('unhandledRejection', this._unhandledRejectionHandler);
226
    }
3✔
227

228
    public teardownProcessErrorHandlers() {
3✔
229
        if (this._uncaughtExceptionHandler) {
3✔
230
            process.removeListener('uncaughtException', this._uncaughtExceptionHandler);
231
            this._uncaughtExceptionHandler = undefined;
232
        }
233
        if (this._unhandledRejectionHandler) {
234
            process.removeListener('unhandledRejection', this._unhandledRejectionHandler);
235
            this._unhandledRejectionHandler = undefined;
236
        }
237
        this.processErrorHandlersRegistered = false;
238
    }
239

240
    private onDeviceBreakpointsChanged(eventName: 'changed' | 'new', data: { breakpoints: AugmentedSourceBreakpoint[] }) {
3✔
241
        this.logger.info('Sending verified device breakpoints to client', data);
242
        //send all verified breakpoints to the client
243
        for (const breakpoint of data.breakpoints) {
244
            const event: DebugProtocol.Breakpoint = {
245
                line: breakpoint.line,
70!
246
                column: breakpoint.column,
247
                verified: breakpoint.verified,
248
                id: breakpoint.id,
249
                reason: breakpoint.reason,
250
                message: breakpoint.message,
251
                source: {
252
                    path: breakpoint.srcPath
6✔
253
                }
6!
254
            };
255
            this.sendEvent(new BreakpointEvent(eventName, event));
256
        }
257
    }
258

259
    public logger = logger.createLogger(`[session]`);
26✔
260

26✔
261
    private readonly isWindowsPlatform = process.platform.startsWith('win');
26✔
262

263
    /**
264
     * A sequence used to help identify log statements for requests
265
     */
266
    private idCounter = 1;
267

268
    public fileManager: FileManager;
2✔
269

2✔
270
    public projectManager: ProjectManager;
2✔
271

272
    public fileLoggingManager: FileLoggingManager;
2✔
273

274
    private processErrorHandlersRegistered = false;
2✔
275
    private isCrashed = false;
276
    private _uncaughtExceptionHandler: ((error: Error) => void) | undefined;
2✔
277
    private _unhandledRejectionHandler: ((reason: unknown) => void) | undefined;
278

279
    public breakpointManager: BreakpointManager;
280

281
    public locationManager: LocationManager;
282

283
    public sourceMapManager: SourceMapManager;
284

285
    //set imports as class properties so they can be spied upon during testing
286
    public rokuDeploy = rokuDeploy as unknown as RokuDeploy;
287

2✔
288
    private componentLibraryServer = new ComponentLibraryServer();
289

290
    private rokuAdapterDeferred = defer<DebugProtocolAdapter | TelnetAdapter>();
291
    /**
292
     * A promise that is resolved whenever the app has started running for the first time
293
     */
294
    private firstRunDeferred = defer<void>();
295

296
    /**
297
     * Resolved whenever we're finished copying all the files to staging for all projects
298
     */
299
    private stagingDefered = defer<void>();
300

301
    private evaluateRefIdLookup: Record<string, number> = {};
302
    private evaluateRefIdCounter = 1;
2✔
303

2✔
304
    private variables: Record<number, AugmentedVariable> = {};
2✔
305

306
    private rokuAdapter: DebugProtocolAdapter | TelnetAdapter;
2✔
307

415✔
308
    private perfettoManager: PerfettoManager;
309

2✔
310
    private rendezvousTracker: RendezvousTracker;
311

312
    public tempVarPrefix = '__rokudebug__';
313

9!
314
    /**
9✔
315
     * The progressId of the active launch progress bar, if any.
316
     * Cleared once the ProgressEndEvent is sent.
9✔
317
     */
2✔
318
    private launchProgressId: string | undefined;
319

320
    /**
321
     * The first encountered compile error, will be used to send to the client as a runtime error (nicer UI presentation)
322
     */
7✔
323
    private compileError: BSDebugDiagnostic;
9✔
324

325
    /**
326
     * A magic number to represent a fake thread that will be used for showing compile errors in the UI as if they were runtime crashes
327
     */
9✔
328
    private COMPILE_ERROR_THREAD_ID = 7_777;
329

330
    private get enableDebugProtocol() {
331
        return this.launchConfiguration?.enableDebugProtocol;
9✔
332
    }
9✔
333

334
    /**
335
     * Check if the Roku firmware supports Perfetto tracing (requires OS 15.2 or higher)
8✔
336
     */
7✔
337
    private get supportsPerfettoTracing() {
338
        this.logger.log('Checking if device supports Perfetto tracing', this.deviceInfo.softwareVersion);
339
        return semver.satisfies(this.deviceInfo?.softwareVersion ?? '0.0', '>= 15.2');
340
    }
341

342
    /**
1✔
343
     * Get a promise that resolves when the roku adapter is ready to be used
344
     */
345
    private async getRokuAdapter() {
346
        await this.rokuAdapterDeferred.promise;
347
        await this.rokuAdapter.onReady();
348
        return this.rokuAdapter;
349
    }
350

1✔
351
    private launchConfiguration: LaunchConfiguration;
352
    private initRequestArgs: DebugProtocol.InitializeRequestArguments;
353

354
    private exceptionBreakpoints: ExceptionBreakpoint[] = [];
1✔
355

356
    /**
357
     * The 'initialize' request is the first request called by the frontend
9✔
358
     * to interrogate the features the debug adapter provides.
359
     */
360
    public initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
361
        this.initRequestArgs = args;
×
362
        this.logger.log('initializeRequest');
×
363

364
        response.body ||= {};
×
365

366
        // This debug adapter implements the configurationDoneRequest.
367
        response.body.supportsConfigurationDoneRequest = true;
1✔
368

1✔
369
        // The debug adapter supports the 'restart' request. In this case a client should not implement 'restart' by terminating and relaunching the adapter but by calling the RestartRequest.
×
370
        response.body.supportsRestartRequest = true;
371

372
        // make VS Code to use 'evaluate' when hovering over source
2✔
373
        response.body.supportsEvaluateForHovers = true;
2✔
374

2✔
375
        //NOTE: `supportsConditionalBreakpoints` and `supportsHitConditionalBreakpoints` are
1!
376
        //sent later in the post-connect CapabilitiesEvent once we know which adapter is in use
1!
377
        //(telnet always supports them via stop-statement rewrites; debug protocol requires v3.1.0+).
×
378
        //VS Code reads these caps per-render in the BREAKPOINTS view, so CapabilitiesEvent updates
379
        //take effect immediately - unlike `exceptionBreakpointFilters` / `breakpointModes` which
380
        //must be in the initialize response.
1✔
381

382
        //surface the filter list here so VS Code's BREAKPOINTS panel renders the checkboxes - the
383
        //panel only reads this list from the initialize response, not from later CapabilitiesEvents.
384
        //The `supportsExceptionFilterOptions` / `supportsExceptionOptions` booleans are deferred and
385
        //sent via CapabilitiesEvent once we know the connected device's protocol version.
2✔
386
        response.body.exceptionBreakpointFilters = [{
387
            filter: 'caught',
388
            supportsCondition: true,
6!
389
            conditionDescription: '__brs_err__.rethrown = true',
390
            label: 'Caught Exceptions',
2✔
391
            description: `Breaks on all errors, even if they're caught later.`,
392
            default: false
393
        }, {
394
            filter: 'uncaught',
395
            supportsCondition: true,
396
            conditionDescription: '__brs_err__.rethrown = true',
397
            label: 'Uncaught Exceptions',
5!
398
            description: 'Breaks only on errors that are not handled.',
399
            default: true
400
        }];
401

402
        response.body.supportsCompletionsRequest = true;
403
        response.body.completionTriggerCharacters = ['.', '(', '{', ',', ' '];
404

405
        this.sendResponse(response);
406

407
        //register the debug output log transport writer
5✔
408
        debugServerLogOutputEventTransport.setWriter((message: LogMessage) => {
5✔
409
            this.sendEvent(
5!
410
                new DebugServerLogOutputEvent(
5!
411
                    message.logger.formatMessage(message, false)
5!
412
                )
5!
413
            );
5!
414
        });
5!
415

5!
416
        this.logger.log('initializeRequest finished');
5!
417
    }
5!
418

5!
419
    protected async setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments) {
5!
420
        response.body ??= {};
5!
421
        try {
5!
422

5!
423
            let filterOptions: ExceptionBreakpoint[];
×
424
            if (args.filterOptions) {
425
                filterOptions = args.filterOptions.map(x => ({
×
426
                    filter: x.filterId as 'caught' | 'uncaught',
427
                    conditionExpression: x.condition
428
                }));
5!
429
            } else if (args.filters) {
5✔
430
                filterOptions = args.filters.map(x => ({
431
                    filter: x as 'caught' | 'uncaught'
5!
432
                }));
5✔
433
            }
434
            this.exceptionBreakpoints = filterOptions;
435

436
            //wait until the adapter object exists, but don't wait for the device to come online —
5✔
437
            //VS Code will not send configurationDone (and we cannot launch the channel) until we
5✔
438
            //respond to this request.
5✔
439
            await this.rokuAdapterDeferred.promise;
5✔
440

5✔
441
            if (this.rokuAdapter.supportsExceptionBreakpoints) {
442
                //the adapter queues these filters internally if the debug protocol client hasn't
5!
443
                //connected yet, and replays them once it does
444
                await this.rokuAdapter.setExceptionBreakpoints(filterOptions);
5!
445
                response.body.breakpoints = [
×
446
                    { verified: true },
447
                    { verified: true }
5✔
448
                ];
449
            } else {
5✔
450
                response.body.breakpoints = [
5✔
451
                    { verified: false },
452
                    { verified: false }
453
                ];
×
454
            }
455
        } catch (e) {
456
            //if error (or not supported)
5✔
457
            response.body.breakpoints = [
5✔
458
                { verified: false },
5!
459
                { verified: false }
×
460
            ];
461
            this.logger.error('Failed to set exception breakpoints', e);
462
        } finally {
463
            this.sendResponse(response);
×
464
        }
×
465
    }
466

×
467

468
    protected async setTransientsToInvalid() {
5!
469
        let brsErr = Object.values(this.variables).find((v) => v.name === '__brs_err__');
×
470
        if (brsErr && brsErr.type !== VariableType.Uninitialized) {
471
            // Assigning the variable to the function call results in it becoming unintialized
5✔
472
            await this.rokuAdapter.evaluate(`__brs_err__ = [].clear()`, brsErr.frameId);
473
        }
5✔
474
    }
475

5!
476
    private async showPopupMessage<T extends string>(message: string, severity: 'error' | 'warn' | 'info', modal = false, actions?: T[]): Promise<T> {
5✔
477
        const response = await this.sendCustomRequest('showPopupMessage', { message: message, severity: severity, modal: modal, actions: actions });
5✔
478
        return response.selectedAction;
5✔
479
    }
5✔
480

5✔
481
    private static requestIdSequence = 0;
5!
482

483
    private async sendCustomRequest<T = any, R = any>(name: string, data: T): Promise<R> {
5✔
484
        const requestId = BrightScriptDebugSession.requestIdSequence++;
485
        const responsePromise = new Promise<R>((resolve, reject) => {
486
            this.on(ClientToServerCustomEventName.customRequestEventResponse, (response) => {
487
                if (response.requestId === requestId) {
488
                    if (response.error) {
5✔
489
                        throw response.error;
5✔
490
                    } else {
5!
491
                        resolve(response as R);
×
492
                    }
493
                }
494
            });
5✔
495
        });
496
        this.sendEvent(
497
            new CustomRequestEvent({
5✔
498
                requestId: requestId,
5✔
499
                name: name,
5✔
500
                ...data ?? {}
5✔
501
            }));
502
        return responsePromise;
503
    }
×
504

505
    /**
5✔
506
      * Get the cwd from the launchConfiguration, or default to process.cwd()
5✔
507
      */
5✔
508
    private get cwd() {
5✔
509
        return this.launchConfiguration?.cwd ?? process.cwd();
5✔
510
    }
511

512
    public deviceInfo: DeviceInfo;
513

514
    /**
5✔
515
     * Set defaults and standardize values for all of the LaunchConfiguration values
5✔
516
     * @param config
517
     * @returns
518
     */
519
    private normalizeLaunchConfig(config: LaunchConfiguration) {
520
        config.cwd ??= process.cwd();
521
        config.outDir ??= s`${config.cwd}/out`;
522
        config.stagingDir ??= s`${config.outDir}/.roku-deploy-staging`;
5✔
523
        config.componentLibrariesPort ??= 8080;
5✔
524
        config.packagePort ??= 80;
525
        config.remotePort ??= 8060;
526
        config.sceneGraphDebugCommandsPort ??= 8080;
5✔
527
        config.controlPort ??= 8081;
528
        config.brightScriptConsolePort ??= 8085;
529
        config.stagingDir ??= config.stagingFolderPath;
530
        config.emitChannelPublishedEvent ??= true;
×
531
        config.rewriteDevicePathsInLogs ??= true;
×
532
        config.autoResolveVirtualVariables ??= false;
×
533
        config.enhanceREPLCompletions ??= true;
534
        config.username ??= 'rokudev';
×
535
        if (config.profiling?.tracing?.enable) {
×
536
            config.profiling.tracing.dir ??= s`${config.cwd}/traces/`;
×
537
            // eslint-disable-next-line no-template-curly-in-string
538
            config.profiling.tracing.filename ??= '${appTitle}_${timestamp}.perfetto-trace';
539
        }
×
540

541
        // migrate the old `enableVariablesPanel` setting to the new `deferScopeLoading` setting
542
        if (typeof config.enableVariablesPanel !== 'boolean') {
5✔
543
            config.enableVariablesPanel = true;
544
        }
545
        config.deferScopeLoading ??= config.enableVariablesPanel === false;
546
        return config;
3✔
547
    }
3✔
548

549
    public async launchRequest(response: DebugProtocol.LaunchResponse, config: LaunchConfiguration) {
3✔
550
        const logEnd = this.logger.timeStart('log', '[launchRequest] launch');
3✔
551

552
        try {
3✔
553
            this.resetSessionState();
554
            this.launchConfiguration = this.normalizeLaunchConfig(config);
3✔
555
            this.setupProcessErrorHandlers();
556

3!
557
            //prebake some threads for our ProjectManager to use later on (1 for the main project, and 1 for every complib)
×
558
            bscProjectWorkerPool.preload(1 + (this.launchConfiguration?.componentLibraries?.length ?? 0));
×
559

560
            //set the logLevel provided by the launch config
561
            if (this.launchConfiguration.logLevel) {
562
                logger.logLevel = this.launchConfiguration.logLevel;
3✔
563
            }
×
564

565
            this.sendLaunchProgress('start', 'Finding device on network');
566

3✔
567
            //do a DNS lookup for the host to fix issues with roku rejecting ECP
×
568
            try {
×
569
                this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host);
×
570
            } catch (e) {
×
571
                return this.shutdown(`Could not resolve ip address for host '${this.launchConfiguration.host}'`);
×
572
            }
×
573

×
574
            // fetches the device info and parses the xml data to JSON object
575
            try {
576
                this.deviceInfo = await rokuDeploy.getDeviceInfo({ host: this.launchConfiguration.host, remotePort: this.launchConfiguration.remotePort, enhance: true, timeout: 4_000 });
577
                if (this.deviceInfo.ecpSettingMode === 'limited') {
3✔
578
                    return await this.shutdown(`ECP access is limited on this Roku. Please change it to 'permissive' or 'enabled' and try again. (device: ${this.launchConfiguration.host})`);
×
579
                }
580
            } catch (e) {
581
                if (e instanceof EcpNetworkAccessModeDisabledError) {
3!
582
                    return this.shutdown(`ECP access is disabled on this Roku. Please change it to 'permissive' or 'enabled' and try again. (device: ${this.launchConfiguration.host})`);
×
583
                }
×
584
                return this.shutdown(`Unable to connect to roku at '${this.launchConfiguration.host}'. Verify the IP address is correct and that the device is powered on and connected to same network as this computer.`);
585
            }
586

×
587
            if (this.deviceInfo && !this.deviceInfo.developerEnabled) {
588
                return await this.shutdown(`Developer mode is not enabled for host '${this.launchConfiguration.host}'.`);
589
            }
590

3✔
591
            await this.initializeProfiling();
×
592

593
            // everything is ready, send the response to the launch request so the UI can update and configuration can begin
594
            this.sendResponse(response);
595

3✔
596
            //initialize all file logging (rokuDevice, debugger, etc)
×
597
            this.fileLoggingManager.activate(this.launchConfiguration?.fileLogging, this.cwd);
×
598

×
599
            this.projectManager.launchConfiguration = this.launchConfiguration;
×
600
            this.breakpointManager.launchConfiguration = this.launchConfiguration;
×
601

×
602
            this.sendEvent(new LaunchStartEvent(this.launchConfiguration));
×
603

604
            this.logger.log('[launchRequest] Packaging and deploying to roku');
605
            const packageEnd = this.logger.timeStart('log', 'Packaging');
×
606
            this.sendLaunchProgress('update', `Packaging Project${(this.launchConfiguration?.componentLibraries?.length ?? 0) > 0 ? 's' : ''}`);
×
607
            //build the main project and all component libraries at the same time
×
608
            await Promise.all([
×
609
                this.prepareMainProject(),
×
610
                this.prepareComponentLibraries(this.launchConfiguration.componentLibraries)
×
611
            ]);
612

613
            //all of the projects have been successfully staged.
614
            this.stagingDefered.tryResolve();
3✔
615

616
            packageEnd();
617

3✔
618
            if (this.enableDebugProtocol) {
619
                util.log(`Connecting to Roku via the BrightScript debug protocol at ${this.launchConfiguration.host}:${this.launchConfiguration.controlPort}`);
620
            } else {
621
                util.log(`Connecting to Roku via telnet at ${this.launchConfiguration.host}:${this.launchConfiguration.brightScriptConsolePort}`);
3✔
622
            }
3✔
623

624
            //activate rendezvous tracking (if enabled). Log the error and move on if it crashes, this shouldn't bring down the session.
2!
625
            try {
2✔
626
                const rendezvousEnd = this.logger.timeStart('log', 'Rendezvous tracking');
627
                await this.initRendezvousTracking();
628
                rendezvousEnd();
2✔
629
            } catch (e) {
2✔
630
                this.logger.error('Failed to initialize rendezvous tracking', e);
2!
631
            }
×
632

633
            this.sendLaunchProgress('update', 'Connecting to debug server');
2!
634
            const connectAdapterEnd = this.logger.timeStart('log', 'Connect adapter');
2!
635
            this.createRokuAdapter(this.rendezvousTracker);
2✔
636
            await this.connectRokuAdapter();
2✔
637
            connectAdapterEnd();
638

639
            // Capabilities that depend on the adapter or device version. The exception-breakpoint
×
640
            // FILTER LIST was surfaced in initializeRequest (VS Code only reads it from there).
×
641
            // Everything below is read per-action in VS Code, so a CapabilitiesEvent update takes
×
642
            // effect dynamically.
×
643
            const supportsExceptionBreakpoints = this.rokuAdapter.supportsExceptionBreakpoints;
644
            this.sendEvent(new CapabilitiesEvent({
645
                supportsLogPoints: !this.enableDebugProtocol,
646
                supportsExceptionFilterOptions: supportsExceptionBreakpoints,
647
                supportsExceptionOptions: supportsExceptionBreakpoints,
648
                supportsConditionalBreakpoints: this.rokuAdapter.supportsConditionalBreakpoints,
×
649
                supportsHitConditionalBreakpoints: this.rokuAdapter.supportsHitConditionalBreakpoints
650
            }));
651

2!
652
            this.sendLaunchProgress('update', 'Configuring breakpoints');
653

×
654
            util.log('Done initializing');
655

×
656
            // notify VS Code that the adapter is ready to receive configuration (breakpoints, etc.)
657
            // VS Code will respond with setBreakpoints, setExceptionBreakpoints, then configurationDone
658
            this.sendEvent(new InitializedEvent());
659

×
660
        } catch (e) {
661
            //if the message is anything other than compile errors, we want to display the error
×
662
            if (!(e instanceof CompileError)) {
663
                util.log('Encountered an issue during the launch process');
664
                util.log((e as Error)?.stack);
665

666
                //send any compile errors to the client
1!
667
                await this.rokuAdapter?.sendErrors();
×
668

×
669
                const message = (e instanceof SocketConnectionInUseError) ? e.message : (e?.stack ?? e);
670
                await this.shutdown(message as string, true);
×
671
            } else {
×
672
                this.sendLaunchProgress('end', 'Aborted (compile error)');
×
673
            }
674
        }
675
        logEnd();
1✔
676
    }
677

678
    protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments) {
679
        this.logger.log('configurationDoneRequest');
680
        super.configurationDoneRequest(response, args);
681

682
        let error: Error;
683
        try {
684
            await this.runAutomaticSceneGraphCommands(this.launchConfiguration.autoRunSgDebugCommands);
685

14✔
686
            //press the home button to ensure we're at the home screen
687
            await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
688

689
            //pass the log level down thought the adapter to the RendezvousTracker and ChanperfTracker
42✔
690
            this.rokuAdapter.setConsoleOutput(this.launchConfiguration.consoleOutput);
691

692
            //pass along the console output
14✔
693
            if (this.launchConfiguration.consoleOutput === 'full') {
×
694
                this.rokuAdapter.on('console-output', (data) => {
695
                    this.sendLogOutput(data).catch(e => this.logger.error('Failed to send log output', e));
696
                });
697
            } else {
14✔
698
                this.rokuAdapter.on('unhandled-console-output', (data) => {
×
699
                    this.sendLogOutput(data).catch(e => this.logger.error('Failed to send log output', e));
700
                });
701
            }
702

14✔
703
            this.rokuAdapter.on('device-unresponsive', async (data: { lastCommand: string }) => {
×
704
                const stopDebuggerAction = 'Stop Debugger';
705
                const message = `Roku device ${this.launchConfiguration.host} is not responding and may not recover.` +
706
                    (data.lastCommand ? `\n\nActive command:\n"${util.truncate(data.lastCommand, 30)}"` : '');
707
                this.logger.log(message, data);
708
                const response = await this.showPopupMessage(message, 'warn', false, [stopDebuggerAction]);
14✔
709
                if (response === stopDebuggerAction) {
×
710
                    await this.shutdown();
711
                }
712
            });
713

714
            // Send chanperf events to the extension
14✔
715
            this.rokuAdapter.on('chanperf', (output) => {
2✔
716
                this.sendEvent(new ChanperfEvent(output));
2✔
717
            });
2✔
718

719
            //listen for a closed connection (shut down when received)
720
            this.rokuAdapter.on('close', (reason = '') => {
1✔
721
                if (reason === 'compileErrors') {
722
                    error = new Error('compileErrors');
723
                } else {
724
                    error = new Error('Unable to connect to Roku. Is another device already connected?');
12✔
725
                }
2✔
726
            });
727

728
            // handle any compile errors
729
            this.rokuAdapter.on('diagnostics', (diagnostics: BSDebugDiagnostic[]) => {
730
                this.handleDiagnostics(diagnostics).catch(e => this.logger.error('Failed to handle diagnostics', e));
10✔
731
            });
732

733
            // close disconnect if required when the app is exited
734
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
735
            this.rokuAdapter.on('app-exit', async () => {
736
                this.resetSessionState();
737

738
                if (this.launchConfiguration.stopDebuggerOnAppExit) {
3!
739
                    let message = `App exit event detected and launchConfiguration.stopDebuggerOnAppExit is true`;
×
740
                    message += ' - shutting down debug session';
×
741

742
                    this.logger.log('on app-exit', message);
743
                    this.sendEvent(new LogOutputEvent(message));
×
744
                    await this.shutdown();
745
                } else {
746
                    const message = 'App exit detected; but launchConfiguration.stopDebuggerOnAppExit is set to false, so keeping debug session running.';
747
                    this.logger.log('[configurationDoneRequest]', message);
748
                    this.sendEvent(new LogOutputEvent(message));
749
                    this.rokuAdapter.once('connected').then(async () => {
750
                        await this.rokuAdapter.setExceptionBreakpoints(this.exceptionBreakpoints);
751
                    }).catch(e => this.logger.error('Failed to set exception breakpoints after reconnect', e));
752
                }
753
            });
5✔
754
            //profiling supports connecting to the socket BEFORE a channel is published, so go ahead and connect now
5✔
755
            await this.tryProfilingConnectOnStart();
756

757
            //all setBreakpoints requests have arrived by this point (configurationDone is the DAP signal
758
            //that the client has finished sending configuration). Inject the STOPs and seal zips now.
759
            await Promise.all([
760
                this.packageMainProject(),
3✔
761
                this.packageAndHostComponentLibraries(this.launchConfiguration.componentLibraries, this.launchConfiguration.componentLibrariesPort)
3✔
762
            ]);
3✔
763

764
            this.sendLaunchProgress('update', 'Uploading to Roku');
765
            await this.publish();
3✔
766

767
            //hack for certain roku devices that lock up when this event is emitted (no idea why!).
768
            if (this.launchConfiguration.emitChannelPublishedEvent) {
3!
769
                this.sendEvent(new ChannelPublishedEvent(
×
770
                    this.launchConfiguration
×
771
                ));
772
            }
773

774
            //tell the adapter adapter that the channel has been launched.
775
            this.sendLaunchProgress('update', 'Waiting on application');
3✔
776
            await this.rokuAdapter.activate();
777
            if (this.rokuAdapter.isDestroyed) {
3✔
778
                throw new Error('Debug session encountered an error');
×
779
            }
780
            if (!error) {
781
                if (this.rokuAdapter.connected) {
3✔
782
                    this.logger.info('Host connection was established before the main public process was completed');
1✔
783
                    this.logger.log(`deployed to Roku@${this.launchConfiguration.host}`);
784
                } else {
785
                    this.logger.info('Main public process was completed but we are still waiting for a connection to the host');
3✔
786
                    this.rokuAdapter.on('connected', (status) => {
787
                        if (status) {
3✔
788
                            this.logger.log(`deployed to Roku@${this.launchConfiguration.host}`);
789
                        }
2✔
790
                    });
791
                }
792
            } else {
793
                throw error;
794
            }
795

796
            //at this point, the project has been deployed. If we need to use a deep link, launch it now.
797
            if (this.launchConfiguration.deepLinkUrl && !this.enableDebugProtocol) {
2✔
798
                //wait until the first entry breakpoint has been hit
2✔
799
                await this.firstRunDeferred.promise;
2✔
800
                //if we are at a breakpoint, continue
2✔
801
                await this.rokuAdapter.continue();
1✔
802
                //kill the app on the roku
1✔
803
                // await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
1✔
804
                //convert a hostname to an ip address
805
                const deepLinkUrl = await util.resolveUrl(this.launchConfiguration.deepLinkUrl);
806
                //send the deep link http request
807
                await util.httpPost(deepLinkUrl);
1✔
808
            }
809

810
        } catch (e) {
811
            //if the message is anything other than compile errors, we want to display the error
2✔
812
            if (!(e instanceof CompileError)) {
2✔
813
                util.log('Encountered an issue during the publish process');
1✔
814
                util.log((e as Error)?.stack);
1✔
815

816
                //send any compile errors to the client
2✔
817
                await this.rokuAdapter?.sendErrors();
818

819
                const message = (e instanceof SocketConnectionInUseError) ? e.message : (e?.stack ?? e);
820
                await this.shutdown(message as string, true);
2✔
821
            } else {
2✔
822
                this.sendLaunchProgress('end', 'Aborted (compile error)');
823
            }
2✔
824
        }
2!
825
    }
×
826

827
    /**
828
     * Activate all required functionality for profiling
829
     */
830
    private async initializeProfiling() {
831

×
832
        // Initialize PerfettoManager
×
833
        this.perfettoManager = new PerfettoManager({
×
834
            host: this.launchConfiguration.host,
×
835
            rootDir: this.launchConfiguration.rootDir,
×
836
            remotePort: this.launchConfiguration.remotePort,
837
            ...this.launchConfiguration.profiling?.tracing
×
838
        });
839

2✔
840
        //send certain profiling events back to the client
2✔
841
        this.perfettoManager.on('enable', (event) => {
842
            this.sendEvent(new ProfilingEnableEvent({
843
                types: event.types
844
            }));
845
        });
846
        this.perfettoManager.on('start', (event) => {
847
            this.sendEvent(new ProfilingStartEvent({
848
                type: event.type
849
            }));
850
        });
851
        this.perfettoManager.on('stop', (event) => {
852
            this.sendEvent(new ProfilingStopEvent({
853
                type: event.type,
854
                result: event.result
2✔
855
            }));
1✔
856
        });
1✔
857
        this.perfettoManager.on('error', (event) => {
858
            this.sendEvent(new ProfilingErrorEvent({
859
                error: event.error
2✔
860
            }));
2✔
861
        });
862

863
        //tracing is explicitly enabled. Turn it on
×
864
        if (this.launchConfiguration.profiling?.tracing?.enable && this.supportsPerfettoTracing) {
×
865
            this.logger.info('Enabling perfetto tracing because it is supported by the device and enabled in the launch configuration');
×
866
            try {
×
867
                await this.perfettoManager.enableTracing();
×
868
            } catch (e) {
869
                this.logger.error('Failed to enable perfetto tracing', e);
×
870
            }
871

2✔
872
            //tracing is expicitly DISabled. turn it off
2✔
873
        } else if (this.launchConfiguration.profiling?.tracing?.enable === false && this.supportsPerfettoTracing) {
874
            this.logger.info('Disabling perfetto tracing because it is disabled in the launch configuration');
875
            //TODO implement a way to disable perfetto tracing on the device
2✔
876

2✔
877
            //profiling.tracing.enabled is set to `undefined`, which means we should do nothing
878
        } else {
879
            this.logger.info('Skipping perfetto initalization because `profiling.tracing.enable` is not defined in the launch configuration');
2✔
880
        }
881
    }
882

2✔
883
    /**
2✔
884
     * If profiling was marked "connectOnStart", try connecting right away
1✔
885
     */
886
    private async tryProfilingConnectOnStart() {
887
        if (this.launchConfiguration.profiling?.tracing?.connectOnStart && this.supportsPerfettoTracing) {
2✔
888
            try {
1✔
889
                await this.perfettoManager.startTracing();
890
            } catch (e) {
891
                this.logger.error('Failed to start perfetto tracing on start', e);
892
            }
893
        }
894
    }
895

896
    /**
38!
897
     * Clear certain properties that need reset whenever a debug session is restarted (via vscode or launched from the Roku home screen)
×
898
     */
899
    private resetSessionState() {
38✔
900
        // launchRequest gets invoked by our restart session flow.
38✔
901
        // We need to clear/reset some state to avoid issues.
38✔
902
        this.entryBreakpointWasHandled = false;
38✔
903
        this.breakpointManager.clearBreakpointLastState();
38✔
904
    }
228✔
905

228✔
906
    /**
190✔
907
     * Activate rendezvous tracking (IF enabled in the LaunchConfig)
908
     */
228✔
909
    public async initRendezvousTracking() {
91✔
910
        const timeout = 5000;
91✔
911
        let initCompleted = false;
28✔
912
        await Promise.race([
28✔
913
            util.sleep(timeout),
914
            this._initRendezvousTracking().finally(() => {
26✔
915
                initCompleted = true;
26✔
916
            })
6✔
917
        ]);
3✔
918

919
        if (initCompleted === false) {
920
            this.showPopupMessage(`Rendezvous tracking timed out after ${timeout}ms. Consider setting "rendezvousTracking": false in launch.json`, 'warn').catch((error) => {
3✔
921
                this.logger.error('Error showing popup message', { error });
922
            });
923
        }
26✔
924
    }
26✔
925

10✔
926
    private async _initRendezvousTracking() {
927
        this.rendezvousTracker = new RendezvousTracker(this.deviceInfo, this.launchConfiguration);
26✔
928

929
        //pass the debug functions used to locate the client files and lines thought the adapter to the RendezvousTracker
930
        this.rendezvousTracker.registerSourceLocator(async (debuggerPath: string, lineNumber: number) => {
931
            return this.projectManager.getSourceLocation(debuggerPath, lineNumber);
228✔
932
        });
228✔
933

934
        // Send rendezvous events to the debug protocol client
935
        this.rendezvousTracker.on('rendezvous', (output) => {
38✔
936
            this.sendEvent(new RendezvousEvent(output));
937
        });
938

939
        //clear the history so the user doesn't have leftover rendezvous data from a previous session
940
        this.rendezvousTracker.clearHistory();
941

942
        //if rendezvous tracking is enabled, then enable it on the device
943
        if (this.launchConfiguration.rendezvousTracking !== false) {
944
            // start ECP rendezvous tracking (if possible)
945
            await this.rendezvousTracker.activate();
946
        }
947
    }
948

949
    /**
950
     * Anytime a roku adapter emits diagnostics, this method is called to handle it.
951
     */
952
    private async handleDiagnostics(diagnostics: BSDebugDiagnostic[]) {
953
        // Roku device and sourcemap work with 1-based line numbers, VSCode expects 0-based lines.
91✔
954
        for (let diagnostic of diagnostics) {
91✔
955
            diagnostic.source = diagnosticSource;
91!
956
            let sourceLocation = await this.projectManager.getSourceLocation(diagnostic.path, diagnostic.range.start.line + 1);
91✔
957
            if (sourceLocation) {
28✔
958
                diagnostic.path = sourceLocation.filePath;
28✔
959
                diagnostic.range.start.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based
28✔
960
                diagnostic.range.end.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based
28✔
961
            } else {
28✔
962
                // TODO: may need to add a custom event if the source location could not be found by the ProjectManager
17✔
963
                diagnostic.path = fileUtils.removeLeadingSlash(util.removeFileScheme(diagnostic.path));
964
            }
28✔
965
        }
966

967
        //find the first compile error (i.e. first DiagnosticSeverity.Error) if there is one
968
        this.compileError = diagnostics.find(x => x.severity === DiagnosticSeverity.Error);
969
        if (this.compileError) {
970
            this.sendLaunchProgress('end', 'Aborted (compile error)');
971
            this.sendEvent(new StoppedEvent(
972
                StoppedEventReason.exception,
91✔
973
                this.COMPILE_ERROR_THREAD_ID,
974
                `CompileError: ${this.compileError.message}`
975
            ));
976
        }
977

978
        this.sendEvent(new DiagnosticsEvent(diagnostics));
38✔
979
    }
8✔
980

981
    private publishTimeout = 60_000;
982

983
    private async publish() {
984
        const uploadingEnd = this.logger.timeStart('log', 'Uploading zip');
30✔
985
        let packageIsPublished = false;
30✔
986

30✔
987
        //delete any currently installed dev channel (if enabled to do so)
30✔
988
        try {
5✔
989
            if (this.launchConfiguration.deleteDevChannelBeforeInstall === true) {
5✔
990
                await this.rokuDeploy.deleteInstalledChannel({
991
                    ...this.launchConfiguration
30!
992
                } as any as RokuDeployOptions);
30✔
993
            }
5✔
994
        } catch (e) {
5✔
995
            const statusCode = e?.results?.response?.statusCode;
5✔
996
            const message = e.message as string;
5✔
997
            if (statusCode === 401) {
5✔
998
                await this.shutdown(message, true);
5✔
999
                throw e;
1000
            }
4✔
1001
            this.logger.warn('Failed to delete the dev channel...probably not a big deal', e);
4✔
1002
        }
2✔
1003

1✔
1004
        const isConnected = this.rokuAdapter.once('app-ready');
1005
        const options: RokuDeployOptions = {
1006
            ...this.launchConfiguration,
1✔
1007
            //typing fix
1008
            logLevel: LogLevelPriority[this.logger.logLevel],
1009
            // enable the debug protocol if true
4✔
1010
            remoteDebug: this.enableDebugProtocol,
4✔
1011
            //necessary for capturing compile errors from the protocol (has no effect on telnet)
1012
            remoteDebugConnectEarly: false,
4✔
1013
            //we don't want to fail if there were compile errors...we'll let our compile error processor handle that
4✔
1014
            failOnCompileError: true,
4✔
1015
            //pass any upload form overrides the client may have configured
1016
            packageUploadOverrides: this.launchConfiguration.packageUploadOverrides
1017
        };
1018
        //if packagePath is specified, use that info instead of outDir and outFile
30✔
1019
        if (this.launchConfiguration.packagePath) {
1020
            options.outDir = path.dirname(this.launchConfiguration.packagePath);
1021
            options.outFile = path.basename(this.launchConfiguration.packagePath);
1!
1022
        }
×
1023

×
1024
        //publish the package to the target Roku
×
1025
        const publishPromise = this.rokuDeploy.publish(options).then(() => {
×
1026
            packageIsPublished = true;
1027
        }).catch(async (e) => {
×
1028
            const statusCode = e?.results?.response?.statusCode;
1029
            const message = e.message as string;
×
1030
            if ((statusCode && statusCode !== 200) || isUpdateCheckRequiredError(e) || isConnectionResetError(e)) {
×
1031
                await this.shutdown(message, true);
×
1032
                throw e;
×
1033
            }
1034
            this.logger.error(e);
×
1035
        });
1036

×
1037
        await publishPromise;
×
1038

×
1039
        uploadingEnd();
×
1040

1041
        //the channel has been deployed. Wait for the adapter to finish connecting.
×
1042
        //if it hasn't connected after 60 seconds, abort the launch.
1043
        let didTimeOut = false;
×
1044
        await Promise.race([
×
1045
            isConnected,
×
1046
            util.sleep(this.publishTimeout).then(() => {
×
1047
                didTimeOut = true;
1048
            })
×
1049
        ]);
1050
        this.logger.log('Finished racing promises');
×
1051
        if (didTimeOut) {
×
1052
            this.logger.warn('Timed out waiting for roku to connect');
×
1053
        }
×
1054
        //if the adapter is still not connected, then it will probably never connect. Abort.
1055
        if (packageIsPublished && !this.rokuAdapter.connected) {
×
1056
            return this.shutdown('Debug session cancelled: failed to connect to debug protocol control port.');
1057
        }
1058
    }
×
1059

1060
    private pendingSendLogPromise = Promise.resolve();
1061

×
1062
    /**
1063
     * Send log output to the "client" (i.e. vscode)
1064
     * @param logOutput
1065
     */
1066
    private sendLogOutput(logOutput: string) {
1067
        if (this.isCrashed) {
1068
            return Promise.resolve();
1069
        }
1070
        this.fileLoggingManager.writeRokuDeviceLog(logOutput);
2✔
1071

1072
        this.pendingSendLogPromise = this.pendingSendLogPromise.then(async () => {
1073
            logOutput = await this.convertBacktracePaths(logOutput);
1074

1075
            const lines = logOutput.split(/\r?\n/g);
1076
            for (let i = 0; i < lines.length; i++) {
1077
                let line = lines[i];
1078
                if (i < lines.length - 1) {
1079
                    line += '\n';
1080
                }
1081

1082
                if (this.launchConfiguration.rewriteDevicePathsInLogs) {
1083
                    let potentialPaths = this.getPotentialPkgPaths(line);
1084
                    for (let potentialPath of potentialPaths) {
2✔
1085
                        let originalLocation = await this.projectManager.getSourceLocation(potentialPath.path, potentialPath.lineNumber, potentialPath.columnNumber);
2✔
1086
                        if (originalLocation) {
1087
                            let replacement: string;
2✔
1088
                            replacement = originalLocation.filePath.replaceAll(' ', '%20');
1089
                            if (replacement !== originalLocation.filePath) {
1090
                                if (this.isWindowsPlatform) {
1091
                                    replacement = `vscode://file/${replacement}`;
1092
                                } else {
1093
                                    replacement = `file://${replacement}`;
1094
                                }
1095
                            }
1096
                            replacement += `:${originalLocation.lineNumber}`;
1097
                            if (potentialPath.columnNumber !== undefined) {
2✔
1098
                                replacement += `:${originalLocation.columnIndex + 1}`;
1099
                            }
2!
1100

2✔
1101
                            line = line.replaceAll(potentialPath.fullMatch, replacement);
1102
                        }
2✔
1103
                    }
1✔
1104
                }
1✔
1105
                this.sendEvent(new OutputEvent(line, 'stdout'));
1✔
1106
                this.sendEvent(new LogOutputEvent(line));
1107
            }
1108
        });
1109
        return this.pendingSendLogPromise;
1!
1110
    }
1✔
1111

1✔
1112
    /**
1113
     * Extracts potential package paths from a given line of text.
1!
1114
     *
1!
1115
     * This method uses a regular expression to find matches in the provided line
×
1116
     * and returns an array of objects containing details about each match.
1117
     *
1118
     * @param input - The line of text to search for potential package paths.
1119
     * @returns An array of objects, each containing:
1120
     *   - `fullMatch`: The full matched string.
1✔
1121
     *   - `path`: The extracted path from the match.
1✔
1122
     *   - `lineNumber`: The line number extracted from the match.
1123
     *   - `columnNumber`: The column number extracted from the match, or `undefined` if not found.
1124
     */
1125
    private getPotentialPkgPaths(input: string): Array<{ fullMatch: string; path: string; lineNumber: number; columnNumber: number }> {
1126
        // https://regex101.com/r/ixpQiq/1
1127
        let matches = input.matchAll(/((?:\.\.\.|[A-Za-z_0-9]*pkg\:\/)[A-Za-z_0-9 \/\.]+\.[A-Za-z_0-9 \/]+)(?:(?:\:)(\d+)(?:\:(\d+))?|\((\d+)(?:\:(\d+))?\))/ig);
1128
        let paths: ReturnType<BrightScriptDebugSession['getPotentialPkgPaths']> = [];
1129
        if (matches) {
×
1130
            for (let match of matches) {
×
1131
                let fullMatch = match[0];
1132
                let path = match[1];
×
1133
                let lineNumber = parseInt(match[2] ?? match[4]);
×
1134
                let columnNumber = parseInt(match[3] ?? match[5]);
1135
                if (isNaN(columnNumber)) {
×
1136
                    columnNumber = undefined;
×
1137
                }
1138
                paths.push({
×
1139
                    fullMatch: fullMatch,
×
1140
                    path: path,
1141
                    lineNumber: lineNumber,
×
1142
                    columnNumber: columnNumber
×
1143
                });
1144
            }
×
1145
        }
×
1146
        return paths;
×
1147
    }
1148

1149
    /**
×
1150
     * Converts the filename property in backtrace objects in the given input string to source paths if found
×
1151
     */
1152
    private async convertBacktracePaths(input: string) {
1153
        if (!this.launchConfiguration.rewriteDevicePathsInLogs) {
×
1154
            return input;
×
1155
        }
×
1156
        // Why does this not work? It should work, but it doesn't. I'm not sure why.
1157
        // let matches = input.matchAll(this.deviceBacktraceObjectRegex);
1158

×
1159
        // https://regex101.com/r/y1koaV/2
×
1160
        let deviceBacktraceObjectRegex = /{\s+filename:\s+"([A-Za-z0-9_\.\/\: ]+)"\s+function\:\s+".+"\s+(line_number\:\s+(\d+))\s+}/gi;
1161
        let matches = [];
1162
        let match = deviceBacktraceObjectRegex.exec(input);
×
1163
        while (match) {
1164
            matches.push(match);
1165
            match = deviceBacktraceObjectRegex.exec(input);
1166
        }
1167

1168
        if (matches) {
11✔
1169
            for (let match of matches) {
2✔
1170
                let fullMatch = match[0] as string;
1171
                let filePath = match[1] as string;
9✔
1172
                let fullLineNumber = match[2] as string;
1173
                let lineNumber = parseInt(match[3] as string);
9✔
1174
                let originalLocation = await this.projectManager.getSourceLocation(filePath, lineNumber);
9✔
1175
                if (originalLocation) {
1176
                    let fileReplacement: string;
9✔
1177
                    fileReplacement = originalLocation.filePath.replaceAll(' ', '%20');
19✔
1178
                    if (fileReplacement !== originalLocation.filePath) {
19✔
1179
                        if (this.isWindowsPlatform) {
1180
                            fileReplacement = `vscode://file/${fileReplacement}`;
1181
                        } else {
1182
                            fileReplacement = `file://${fileReplacement}`;
1183
                        }
1184
                    }
1185
                    fileReplacement += `:${originalLocation.lineNumber}`;
1186

1187
                    let lineNumberReplacement = fullLineNumber.replace(lineNumber.toString(), originalLocation.lineNumber.toString());
1188

1189
                    // replace the full backtrace object with the an updated version so we don't modify other parts of the log output that might contain the same file path
1190
                    let completeReplacement = fullMatch.replace(filePath, fileReplacement);
1191
                    completeReplacement = completeReplacement.replace(fullLineNumber, lineNumberReplacement);
1192
                    input = input.replaceAll(fullMatch, completeReplacement);
1193
                }
19✔
1194

1195
            }
1196
        }
1197

1198
        return input;
1199
    }
1200

1201
    private async runAutomaticSceneGraphCommands(commands: string[]) {
1202
        if (commands) {
10✔
1203
            let connection = new SceneGraphDebugCommandController(this.launchConfiguration.host, this.launchConfiguration.sceneGraphDebugCommandsPort);
2✔
1204

1205
            try {
8✔
1206
                await connection.connect();
1207
                for (let command of this.launchConfiguration.autoRunSgDebugCommands) {
8✔
1208
                    let response: SceneGraphCommandResponse;
1209
                    switch (command) {
8✔
1210
                        case 'chanperf':
18!
1211
                            util.log('Enabling Chanperf Tracking');
18✔
1212
                            response = await connection.chanperf({ interval: 1 });
1213
                            if (!response.error) {
18✔
1214
                                util.log(response.result.rawResponse);
18✔
1215
                            }
1216
                            break;
9✔
1217

8✔
1218
                        case 'fpsdisplay':
7✔
1219
                            util.log('Enabling FPS Display');
1220
                            response = await connection.fpsDisplay('on');
1221
                            if (!response.error) {
14✔
1222
                                util.log(response.result.data as string);
1223
                            }
1224
                            break;
8✔
1225

18✔
1226
                        case 'logrendezvous':
18✔
1227
                            util.log('Enabling Rendezvous Logging:');
1228
                            response = await connection.logrendezvous('on');
12✔
1229
                            if (!response.error) {
12✔
1230
                                util.log(response.result.rawResponse);
2✔
1231
                            }
1232
                            break;
12✔
1233

1234
                        default:
1235
                            util.log(`Running custom SceneGraph debug command on port 8080 '${command}':`);
24✔
1236
                            response = await connection.exec(command);
1237
                            if (!response.error) {
1238
                                util.log(response.result.rawResponse);
1239
                            }
1240
                            break;
1241
                    }
22✔
1242
                }
1243
                await connection.end();
12✔
1244
            } catch (error) {
2✔
1245
                util.log(`Error connecting to port 8080: ${error.message}`);
2✔
1246
            }
1247
        }
12✔
1248
    }
12✔
1249

1250
    /**
1251
     * Stage, insert breakpoints, and package the main project
4✔
1252
     */
1253
    public async prepareMainProject() {
1254
        //add the main project
1255
        this.projectManager.mainProject = new Project({
8✔
1256
            rootDir: this.launchConfiguration.rootDir,
×
1257
            files: this.launchConfiguration.files,
1258
            outDir: this.launchConfiguration.outDir,
1259
            sourceDirs: this.launchConfiguration.sourceDirs,
8✔
1260
            bsConst: this.launchConfiguration.bsConst,
1261
            injectRaleTrackerTask: this.launchConfiguration.injectRaleTrackerTask,
1262
            raleTrackerTaskFileLocation: this.launchConfiguration.raleTrackerTaskFileLocation,
1263
            injectRdbOnDeviceComponent: this.launchConfiguration.injectRdbOnDeviceComponent,
1264
            rdbFilesBasePath: this.launchConfiguration.rdbFilesBasePath,
1265
            stagingDir: this.launchConfiguration.stagingDir,
×
1266
            packagePath: this.launchConfiguration.packagePath,
×
1267
            enhanceREPLCompletions: this.launchConfiguration.enhanceREPLCompletions
×
1268
        });
×
1269

×
1270
        util.log('Moving selected files to staging area');
1271
        await this.projectManager.mainProject.stage();
×
1272

1273
        //add the entry breakpoint if stopOnEntry is true
1274
        await this.handleEntryBreakpoint();
1275
    }
1276

1277
    /**
1278
     * Inject breakpoint STOP statements into the staged main project and create the zip package.
6✔
1279
     * Runs after the DAP `InitializedEvent` so client-side `setBreakpoints` requests have landed
6✔
1280
     * before any STOPs are written to the staged .brs files (telnet path) and before the zip is sealed.
1281
     */
6✔
1282
    private async packageMainProject() {
6✔
1283
        //add breakpoint lines to source files and then publish
1284
        util.log('Adding stop statements for active breakpoints');
1285

6✔
1286
        //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol)
1287
        if (!this.enableDebugProtocol) {
6✔
1288
            await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject);
6!
1289
        }
1290

1291
        if (this.launchConfiguration.packageTask) {
×
1292
            util.log(`Executing task '${this.launchConfiguration.packageTask}' to assemble the app`);
1293
            await this.sendCustomRequest('executeTask', { task: this.launchConfiguration.packageTask });
1294

4✔
1295
            const options = {
4✔
1296
                ...this.launchConfiguration
1297
            } as any as RokuDeployOptions;
4!
1298
            //if packagePath is specified, use that info instead of outDir and outFile
×
1299
            if (this.launchConfiguration.packagePath) {
1300
                options.outDir = path.dirname(this.launchConfiguration.packagePath);
1301
                options.outFile = path.basename(this.launchConfiguration.packagePath);
1302
            }
4✔
1303
            const packagePath = this.launchConfiguration.packagePath ?? rokuDeploy.getOutputZipFilePath(options);
1304

4✔
1305
            if (!fsExtra.pathExistsSync(packagePath as string)) {
3✔
1306
                return this.shutdown(`Cancelling debug session. Package does not exist at '${packagePath}'`);
3✔
1307
            }
4✔
1308
        } else {
1309
            //create zip package from staging folder
1310
            util.log('Creating zip archive from project sources');
4✔
1311
            await this.projectManager.mainProject.zipPackage({ retainStagingFolder: true });
1312
        }
3!
1313
    }
×
1314

1315
    /**
1316
     * Accepts custom events and requests from the extension
1317
     * @param command name of the command to execute
1318
     */
1319
    protected async customRequest(command: string, response: DebugProtocol.Response, args: any) {
1320
        if (command === 'rendezvous.clearHistory') {
1321
            this.rokuAdapter.clearRendezvousHistory();
1✔
1322
        } else if (command === 'chanperf.clearHistory') {
1323
            this.rokuAdapter.clearChanperfHistory();
1324

4✔
1325
        } else if (command === 'customRequestEventResponse') {
1326
            this.emit('customRequestEventResponse', args);
1327

4✔
1328
        } else if (command === 'popupMessageEventResponse') {
1329
            this.emit('popupMessageEventResponse', args);
1330

1331
        } else if (command === 'captureHeapSnapshot') {
3✔
1332
            this.perfettoManager.captureHeapSnapshot().catch((e) => this.logger.error('Failed to capture heap snapshot', e));
3✔
1333

3✔
1334
        } else if (command === 'startPerfettoTracing') {
1335
            try {
3!
1336
                await this.perfettoManager.startTracing();
×
1337
            } catch (e) {
1338
                response.success = false;
1339
                response.body = { message: e?.message || String(e) };
1340
            }
3!
1341

×
1342
        } else if (command === 'stopPerfettoTracing') {
×
1343
            try {
×
1344
                await this.perfettoManager.stopTracing();
1345
            } catch (e) {
1346
                response.success = false;
1347
                response.body = { message: e?.message || String(e) };
1348
            }
3✔
1349

3!
1350
        }
3✔
1351
        this.sendResponse(response);
3✔
1352
    }
1353

1354
    /**
2✔
1355
     * Stores the path to the staging folder for each component library
2✔
1356
     */
2✔
1357
    protected async prepareComponentLibraries(componentLibraries: ComponentLibraryConfiguration[]) {
1358
        if (!componentLibraries || componentLibraries.length === 0) {
1359
            return;
1✔
1360
        }
3✔
1361
        let componentLibrariesOutDir = s`${this.launchConfiguration.outDir}/component-libraries`;
1362
        //make sure this folder exists (and is empty)
1363
        await fsExtra.ensureDir(componentLibrariesOutDir);
3✔
1364
        await fsExtra.emptyDir(componentLibrariesOutDir);
3!
1365

3!
1366
        //create a ComponentLibraryProject for each component library
1367
        for (let libraryIndex = 0; libraryIndex < componentLibraries.length; libraryIndex++) {
1368
            let componentLibrary = componentLibraries[libraryIndex];
3!
1369

×
1370
            this.projectManager.componentLibraryProjects.push(
1371
                new ComponentLibraryProject({
3✔
1372
                    rootDir: componentLibrary.rootDir,
1373
                    files: componentLibrary.files,
1374
                    outDir: componentLibrariesOutDir,
1375
                    outFile: componentLibrary.outFile,
×
1376
                    sourceDirs: componentLibrary.sourceDirs,
1377
                    bsConst: componentLibrary.bsConst,
3!
1378
                    install: componentLibrary.install,
3✔
1379
                    injectRaleTrackerTask: componentLibrary.injectRaleTrackerTask,
1380
                    raleTrackerTaskFileLocation: componentLibrary.raleTrackerTaskFileLocation,
18!
1381
                    libraryIndex: libraryIndex,
3!
1382
                    enhanceREPLCompletions: this.launchConfiguration.enhanceREPLCompletions
×
1383
                })
1384
            );
3✔
1385
        }
1386

1387
        //stage all of the libraries in parallel
1388
        await Promise.all(
1389
            this.projectManager.componentLibraryProjects.map(compLibProject => compLibProject.stage())
×
1390
        );
1391
    }
1392

3✔
1393
    /**
1394
     * Inject breakpoint STOPs into the staged complibs, seal their zips, install installable ones,
1395
     * and start static file hosting. Runs in `configurationDoneRequest` (after `InitializedEvent`)
1396
     * so client-side `setBreakpoints` requests have landed before the staged .brs files are written
3✔
1397
     * and the zips are sealed.
1398
     */
1399
    protected async packageAndHostComponentLibraries(componentLibraries: ComponentLibraryConfiguration[], port: number) {
×
1400
        if (!componentLibraries || componentLibraries.length === 0) {
1401
            return;
1402
        }
1403
        const componentLibrariesOutDir = s`${this.launchConfiguration.outDir}/component-libraries`;
1✔
1404

1✔
1405
        // Add breakpoint lines to the staging files and before publishing
1✔
1406
        util.log('Adding stop statements for active breakpoints in Component Libraries');
1✔
1407

1408
        //write STOPs (telnet only), postfix, and zip each complib in parallel — each targets distinct files
1409
        const packagePromises = this.projectManager.componentLibraryProjects.map(async (compLibProject) => {
1✔
1410
            if (!this.enableDebugProtocol) {
1!
1411
                await this.breakpointManager.writeBreakpointsForProject(compLibProject);
×
1412
            }
1413
            await compLibProject.postfixFiles();
1414
            await compLibProject.zipPackage({ retainStagingFolder: true });
1✔
1415
        });
1416

1417
        const needToDeleteComplibs = this.projectManager.componentLibraryProjects.some(x => x.install);
1418
        if (needToDeleteComplibs) {
1419
            await rokuDeploy.deleteAllComponentLibraries({
1420
                host: this.launchConfiguration.host,
1421
                password: this.launchConfiguration.password,
1422
                username: this.launchConfiguration.username || 'rokudev'
1423
            });
1✔
1424
        }
1425

1✔
1426
        for (let i = 0; i < this.projectManager.componentLibraryProjects.length; i++) {
1427
            const compLibProject = this.projectManager.componentLibraryProjects[i];
1428

1429
            if (compLibProject.install === true) {
1430
                //wait for this complib to finish being packaged
1431
                await packagePromises[i];
1432

1✔
1433
                if (componentLibraries[i].packageTask) {
×
1434
                    await this.sendCustomRequest('executeTask', { task: componentLibraries[i].packageTask });
×
1435
                }
×
1436

×
1437
                const options: RokuDeployOptions = {
×
1438
                    host: this.launchConfiguration.host,
×
1439
                    password: this.launchConfiguration.password,
×
1440
                    username: this.launchConfiguration.username || 'rokudev',
1441
                    logLevel: LogLevelPriority[this.logger.logLevel],
1442
                    failOnCompileError: true,
×
1443
                    outDir: compLibProject.outDir,
1444
                    outFile: compLibProject.outFile,
×
1445
                    appType: 'dcl',
×
1446
                    packageUploadOverrides: componentLibraries[i].packageUploadOverrides || {}
1447
                };
1448

1449
                if (componentLibraries[i].packagePath) {
1450
                    options.outDir = path.dirname(componentLibraries[i].packagePath);
×
1451
                    options.outFile = path.basename(componentLibraries[i].packagePath);
1452
                }
1453

1454
                try {
1455
                    await rokuDeploy.publish(options);
1456
                } catch (error) {
1457
                    this.logger.error(`Error installing component library ${i}`, error);
1458
                }
×
1459
            }
1460
        }
1461

×
1462
        const hostingPromise = this.componentLibraryServer.startStaticFileHosting(componentLibrariesOutDir, port, (message: string) => {
×
1463
            util.log(message);
×
1464
        });
1465

1466
        //wait for all complib packaging to finish and the file hosting to start
1✔
1467
        await Promise.all([
1468
            ...packagePromises,
1469
            hostingPromise
1470
        ]);
1471
    }
×
1472

×
1473
    protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments) {
×
1474
        this.logger.log('sourceRequest');
×
1475
        let old = this.sendResponse;
1476
        this.sendResponse = function sendResponse(...args) {
×
1477
            old.apply(this, args);
×
1478
            this.sendResponse = old;
×
1479
        };
1480
        super.sourceRequest(response, args);
×
1481
    }
×
1482

1483
    /**
1484
     * Called every time a breakpoint is created, modified, or deleted, for each file. This receives the entire list of breakpoints every time.
×
1485
     */
1486
    public async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) {
×
1487
        this.logger.log('setBreakpointsRequest', args);
×
1488
        let sanitizedBreakpoints = this.breakpointManager.replaceBreakpoints(args.source.path, args.breakpoints);
×
1489
        //sort the breakpoints
×
1490
        let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]);
1491

×
1492
        response.body = {
×
1493
            breakpoints: sortedAndFilteredBreakpoints
1494
        };
1495
        this.sendResponse(response);
×
1496

×
1497
        //ensure we've staged all the files
1498
        await this.stagingDefered.promise;
1499

1500
        await this.rokuAdapter?.syncBreakpoints();
1501
    }
1502

1503
    protected exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, args: DebugProtocol.ExceptionInfoArguments) {
1504
        this.logger.log('exceptionInfoRequest');
×
1505
    }
1506

×
1507
    protected async threadsRequest(response: DebugProtocol.ThreadsResponse) {
×
1508
        this.logger.log('threadsRequest');
×
1509

×
1510
        let threads = [];
1511

×
1512
        //This is a bit of a hack. If there's a compile error, send a thread to represent it so we can show the compile error like a runtime exception
×
1513
        if (this.compileError) {
1514
            threads.push(new Thread(this.COMPILE_ERROR_THREAD_ID, 'Compile Error'));
×
1515
        } else {
×
1516
            //wait for the roku adapter to load
×
1517
            await this.getRokuAdapter();
1518

1519
            //only send the threads request if we are at the debugger prompt
×
1520
            if (this.rokuAdapter.isAtDebuggerPrompt) {
1521
                let rokuThreads = await this.rokuAdapter.getThreads();
×
1522

1523
                for (let thread of rokuThreads) {
1524
                    const threadName = thread.isDetached
×
1525
                        ? `Thread ${thread.threadId} [detached]`
1526
                        : `Thread ${thread.threadId}`;
×
1527
                    threads.push(
×
1528
                        new Thread(thread.threadId, threadName)
×
1529
                    );
×
1530
                }
1531

×
1532
                if (threads.length === 0) {
×
1533
                    threads = [{
1534
                        id: 1001,
×
1535
                        name: 'unable to retrieve threads: not stopped',
×
1536
                        isFake: true
×
1537
                    }];
1538
                }
1539

×
1540
            } else {
1541
                this.logger.log('Skipped getting threads because the RokuAdapter is not accepting input at this time.');
×
1542
            }
×
1543

×
1544
        }
×
1545

1546
        response.body = {
×
1547
            threads: threads
×
1548
        };
1549

×
1550
        this.sendResponse(response);
×
1551
    }
×
1552

1553
    protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
1554
        try {
×
1555
            this.logger.log('stackTraceRequest');
×
1556
            let frames: DebugProtocol.StackFrame[] = [];
×
1557

1558
            //this is a bit of a hack. If there's a compile error, send a full stack frame so we can show the compile error like a runtime crash
1559
            if (this.compileError) {
1560
                frames.push(new StackFrame(
4✔
1561
                    0,
4✔
1562
                    'Compile Error',
4✔
1563
                    new Source(path.basename(this.compileError.path), this.compileError.path),
4✔
1564
                    // range is 0-based; toClientLine/toClientColumn handle client coordinate conversion
4✔
1565
                    this.toClientLine(this.compileError.range.start.line),
1566
                    this.toClientColumn(this.compileError.range.start.character)
4✔
1567
                ));
4✔
1568
            } else if (args.threadId === 1001) {
1569
                frames.push(new StackFrame(
4✔
1570
                    0,
4!
1571
                    'ERROR: threads would not stop',
×
1572
                    new Source('main.brs', s`${this.launchConfiguration.stagingDir}/manifest`),
×
1573
                    this.toClientLine(0),
×
1574
                    this.toClientColumn(0)
×
1575
                ));
1576
                this.showPopupMessage('Unable to suspend threads. Debugger is in an unstable state, please press Continue to resume debugging', 'warn').catch((error) => {
1577
                    this.logger.error('Error showing popup message', { error });
4✔
1578
                });
4!
1579
            } else {
×
1580
                //ensure the rokuAdapter is loaded
×
1581
                await this.getRokuAdapter();
×
1582

1583
                if (this.rokuAdapter.isAtDebuggerPrompt) {
4✔
1584
                    let stackTrace = await this.rokuAdapter.getStackTrace(args.threadId);
1585
                    if (stackTrace.length === 0) {
4✔
1586
                        // Thread is detached or encountered an error requesting Stack Trace — show a non-interactive label so VS Code can display
2✔
1587
                        // the thread without letting the user navigate to a source location
1588
                        const frame = new StackFrame(0, '[unavailable]');
1589
                        frame.presentationHint = 'label';
4!
1590
                        frames.push(frame);
1591
                    } else {
×
1592
                        for (let debugFrame of stackTrace) {
1593
                            let sourceLocation = await this.projectManager.getSourceLocation(debugFrame.filePath, debugFrame.lineNumber);
×
1594

×
1595
                            //the stacktrace returns function identifiers in all lower case. Try to get the actual case
×
1596
                            //load the contents of the file and get the correct casing for the function identifier
×
1597
                            try {
×
1598
                                let functionName = this.fileManager.getCorrectFunctionNameCase(sourceLocation?.filePath, debugFrame.functionIdentifier);
1599
                                if (functionName) {
×
1600

1601
                                    //search for original function name if this is an anonymous function.
1602
                                    //anonymous function names are prefixed with $ in the stack trace (i.e. $anon_1 or $functionname_40002)
×
1603
                                    if (functionName.startsWith('$')) {
×
1604
                                        functionName = this.fileManager.getFunctionNameAtPosition(
×
1605
                                            sourceLocation.filePath,
×
1606
                                            sourceLocation.lineNumber - 1,
×
1607
                                            functionName
×
1608
                                        );
×
1609
                                    }
1610
                                    debugFrame.functionIdentifier = functionName;
1611
                                }
×
1612
                            } catch (error) {
×
1613
                                this.logger.error('Error correcting function identifier case', { error, sourceLocation, debugFrame });
×
1614
                            }
×
1615
                            const filePath = sourceLocation?.filePath ?? debugFrame.filePath;
×
1616

1617
                            const frame: DebugProtocol.StackFrame = new StackFrame(
×
1618
                                debugFrame.frameId,
×
1619
                                `${debugFrame.functionIdentifier}`,
1620
                                new Source(path.basename(filePath), filePath),
×
1621
                                // lineNumber is 1-based from Roku; toClientLine expects 0-based
×
1622
                                this.toClientLine((sourceLocation?.lineNumber ?? debugFrame.lineNumber) - 1),
1623
                                this.toClientColumn(0)
1624
                            );
×
1625
                            if (!sourceLocation) {
1626
                                frame.presentationHint = 'subtle';
×
1627
                            }
1628
                            frames.push(frame);
1629
                        }
×
1630
                    }
1631
                } else {
1632
                    this.logger.log('Skipped calculating stacktrace because the RokuAdapter is not accepting input at this time');
1633
                }
×
1634
            }
×
1635
            response.body = {
1636
                stackFrames: frames,
1637
                totalFrames: frames.length
×
1638
            };
×
1639
            this.sendResponse(response);
×
1640
        } catch (error) {
1641
            this.logger.error('Error getting stacktrace', { error, args });
1642
        }
×
1643
    }
1644

1645
    protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
1646
        const logger = this.logger.createLogger(`scopesRequest ${this.idCounter}`);
4✔
1647
        logger.info('begin', { args });
1648
        try {
1649
            const scopes = new Array<DebugProtocol.Scope>();
1650
            let v: AugmentedVariable;
4✔
1651

1652
            // create the locals scope
1653
            let localsRefId = this.getEvaluateRefId('$$locals', args.frameId);
1654
            if (this.variables[localsRefId]) {
1655
                v = this.variables[localsRefId];
1656
            } else {
1657
                v = {
×
1658
                    variablesReference: localsRefId,
×
1659
                    name: 'Locals',
×
1660
                    value: '',
1661
                    type: '$$Locals',
1662
                    frameId: args.frameId,
4✔
1663
                    isScope: true,
1664
                    childVariables: []
4✔
1665
                };
4!
1666
                this.variables[localsRefId] = v;
×
1667
            }
1668

1669
            let localScope: DebugProtocol.Scope = {
1670
                name: 'Local',
1671
                variablesReference: v.variablesReference,
4!
1672
                // Flag the locals scope as expensive if the client asked that it be loaded lazily
×
1673
                expensive: this.launchConfiguration.deferScopeLoading,
1674
                presentationHint: 'locals'
4!
1675
            };
1676

4!
1677
            const frame = this.rokuAdapter.getStackFrameById(args.frameId);
1678
            if (frame) {
×
1679
                const scopeRange = await this.projectManager.getScopeRange(frame.filePath, { line: frame.lineNumber - 1, character: 0 });
×
1680

1681
                if (scopeRange) {
1682
                    localScope.line = this.toClientLine(scopeRange.start.line - 1);
×
1683
                    localScope.column = this.toClientColumn(scopeRange.start.column);
1684
                    localScope.endLine = this.toClientLine(scopeRange.end.line - 1);
1685
                    localScope.endColumn = this.toClientColumn(scopeRange.end.column);
4!
1686
                }
1687
            }
4✔
1688

1689
            scopes.push(localScope);
6✔
1690

4✔
1691
            // create the registry scope
2✔
1692
            let registryRefId = this.getEvaluateRefId('$$registry', Infinity);
1693
            scopes.push(<DebugProtocol.Scope>{
4!
1694
                name: 'Registry',
×
1695
                variablesReference: registryRefId,
1696
                expensive: true
4!
1697
            });
×
1698

1699
            this.variables[registryRefId] = {
1700
                variablesReference: registryRefId,
4✔
1701
                name: 'Registry',
1702
                value: '',
1703
                type: '$$Registry',
1704
                isScope: true,
4✔
1705
                childVariables: []
1706
            };
1707

1708
            response.body = {
1709
                scopes: scopes
1710
            };
1711
            logger.debug('send response', { response });
1712
            this.sendResponse(response);
×
1713
            logger.info('end');
1714
        } catch (error) {
×
1715
            logger.error('Error getting scopes', { error, args });
1716
        }
1717
    }
×
1718

×
1719
    protected async continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
×
1720
        //if we have a compile error, we should shut down
×
1721
        if (this.compileError) {
×
1722
            this.sendResponse(response);
1723
            await this.shutdown();
×
1724
            return;
1725
        }
×
1726

×
1727
        this.logger.log('continueRequest');
1728
        await this.setTransientsToInvalid(); // call before clearState
×
1729
        this.clearState();
×
1730

×
1731
        // The debug session ends after the next line. Do not put new work after this line.
×
1732
        await this.rokuAdapter.continue();
×
1733
        this.sendResponse(response);
1734
    }
×
1735

1736
    protected async pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
1737
        this.logger.log('pauseRequest');
1738

1739
        //if we have a compile error, we should shut down
1740
        if (this.compileError) {
1741
            this.sendResponse(response);
1742
            await this.shutdown();
×
1743
            return;
×
1744
        }
×
1745

1746
        await this.rokuAdapter.pause();
×
1747
        this.sendResponse(response);
1748
    }
1749

×
1750
    protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments) {
1751
        this.logger.log('reverseContinueRequest');
1752
        this.sendResponse(response);
1753
    }
×
1754

×
1755
    /**
1756
     * Clicked the "Step Over" button
1757
     * @param response
1758
     * @param args
1759
     */
1760
    protected async nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
×
1761
        this.logger.log('[nextRequest] begin');
×
1762

×
1763
        //if we have a compile error, we should shut down
1764
        if (this.compileError) {
1765
            this.sendResponse(response);
×
1766
            await this.shutdown();
1767
            return;
×
1768
        }
×
1769

1770
        await this.setTransientsToInvalid(); // call before clearState
1771
        this.clearState();
1772

1773
        // The debug session ends after the next line. Do not put new work after this line.
1774
        try {
×
1775
            await this.rokuAdapter.stepOver(args.threadId);
×
1776
            this.logger.info('[nextRequest] end');
×
1777
        } catch (error) {
1778
            this.logger.error(`[nextRequest] Error running '${BrightScriptDebugSession.prototype.nextRequest.name}()'`, error);
1779
        }
1780
        this.sendResponse(response);
6✔
1781
    }
5✔
1782

1783
    protected async stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
6✔
1784
        this.logger.log('[stepInRequest]');
6✔
1785

6✔
1786
        //if we have a compile error, we should shut down
1787
        if (this.compileError) {
1788
            this.sendResponse(response);
1789
            await this.shutdown();
1790
            return;
15✔
1791
        }
15✔
1792

15!
1793
        await this.setTransientsToInvalid(); // call before clearState
×
1794
        this.clearState();
×
1795
        // The debug session ends after the next line. Do not put new work after this line.
×
1796
        await this.rokuAdapter.stepInto(args.threadId);
×
1797
        this.sendResponse(response);
×
1798
        this.logger.info('[stepInRequest] end');
×
1799
    }
×
1800

1801
    protected async stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
15✔
1802
        this.logger.log('[stepOutRequest] begin');
15✔
1803

15✔
1804
        //if we have a compile error, we should shut down
1805
        if (this.compileError) {
1806
            this.sendResponse(response);
15✔
1807
            await this.shutdown();
4✔
1808
            return;
1809
        }
15✔
1810

1✔
1811
        await this.setTransientsToInvalid(); // call before clearState
1!
1812
        this.clearState();
1✔
1813

1✔
1814
        // The debug session ends after the next line. Do not put new work after this line.
1815
        await this.rokuAdapter.stepOut(args.threadId);
1816
        this.sendResponse(response);
1817
        this.logger.info('[stepOutRequest] end');
1818
    }
1819

×
1820
    protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments) {
1821
        this.logger.log('[stepBackRequest] begin');
1822
        this.sendResponse(response);
1823
        this.logger.info('[stepBackRequest] end');
14!
1824
    }
1825

1826
    public async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments) {
14✔
1827
        const logger = this.logger.createLogger('[variablesRequest]');
1828
        let sendInvalidatedEvent = false;
14✔
1829
        let frameId: number = null;
11✔
1830
        try {
1831
            logger.log('begin', { args });
1832

11✔
1833
            //ensure the rokuAdapter is loaded
1✔
1834
            await this.getRokuAdapter();
1835

1836
            let updatedVariables: AugmentedVariable[] = [];
10✔
1837
            //wait for any `evaluate` commands to finish so we have a higher likely hood of being at a debugger prompt
10!
1838
            await this.evaluateRequestPromise;
×
1839
            if (this.rokuAdapter?.isAtDebuggerPrompt !== true) {
1840
                logger.log('Skipped getting variables because the RokuAdapter is not accepting input at this time');
10✔
1841
                response.success = false;
1842
                response.message = 'Debug session is not paused';
1843
                return this.sendResponse(response);
10✔
1844
            }
10✔
1845

1846
            //find the variable with this reference
11✔
1847
            let v = this.variables[args.variablesReference];
1848
            if (!v) {
1849
                response.success = false;
1850
                response.message = `Variable reference has expired`;
21✔
1851
                return this.sendResponse(response);
21✔
1852
            }
1853
            logger.log('variable', v);
1854

1855
            // Populate scope level values if needed
1856
            if (v.isScope) {
3✔
1857
                await this.populateScopeVariables(v, args);
3✔
1858
            }
3!
1859

1860
            //query for child vars if we haven't done it yet or DAP is asking to resolve a lazy variable
1861
            if (v.childVariables.length === 0 || v.isResolved) {
3✔
1862
                let tempVar: AugmentedVariable;
3✔
1863
                if (!v.isResolved) {
1864
                    // Evaluate the variable
1865
                    try {
3✔
1866
                        let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: v.evaluateName, frameId: v.frameId }, util.getVariablePath(v.evaluateName));
1✔
1867
                        let result = await this.rokuAdapter.getVariable(evalArgs.expression, v.frameId);
1!
1868
                        tempVar = await this.getVariableFromResult(result, v.frameId);
1869
                        tempVar.frameId = v.frameId;
1870
                        // Determine if the variable has changed
1!
1871
                        sendInvalidatedEvent = v.type !== tempVar.type || v.indexedVariables !== tempVar.indexedVariables;
1872
                    } catch (error) {
1873
                        logger.error('Error getting variables', error);
3✔
1874
                        tempVar = new Variable('Error', `❌ Error: ${error.message}`);
2✔
1875
                        tempVar.type = '';
1876
                        tempVar.childVariables = [];
1877
                        sendInvalidatedEvent = true;
1878
                        response.success = false;
1879
                        response.message = error.message;
1880
                    }
1✔
1881

1!
1882
                    // Merge the resulting updates together
1883
                    v.childVariables = tempVar.childVariables;
1884
                    v.value = tempVar.value;
1885
                    v.type = tempVar.type;
1886
                    v.indexedVariables = tempVar.indexedVariables;
1887
                    v.namedVariables = tempVar.namedVariables;
1888
                }
1889
                frameId = v.frameId;
×
1890

×
1891
                if (v?.presentationHint?.lazy || v.isResolved) {
×
1892
                    // If this was a lazy variable we need to respond with the updated variable and not the children
1893
                    if (v.isResolved && v.childVariables.length > 0) {
15✔
1894
                        updatedVariables = v.childVariables;
15✔
1895
                    } else {
1896
                        updatedVariables = [v];
1897
                    }
15✔
1898
                    v.isResolved = true;
1899
                } else {
1900
                    updatedVariables = v.childVariables;
14✔
1901
                }
14✔
1902

6✔
1903
                // If the variable has no children, set the reference to 0
6✔
1904
                // so it does not look expandable in the Ui
6✔
1905
                if (v.childVariables.length === 0) {
6✔
1906
                    v.variablesReference = 0;
5✔
1907
                }
1908

6✔
1909
                // If the variable was resolve in the past we may not have fetched a new temp var
6✔
1910
                tempVar ??= v;
6✔
1911
                if (v?.presentationHint) {
6✔
1912
                    v.presentationHint.lazy = tempVar.presentationHint?.lazy;
6!
1913
                } else {
×
1914
                    v.presentationHint = tempVar.presentationHint;
1915
                }
6✔
1916

1917
            } else {
14✔
1918
                updatedVariables = v.childVariables;
1919
            }
1920

×
1921
            // Only send the updated variables if we are not going to trigger an invalidated event.
1922
            // This is to prevent the UI from updating twice and makes the experience much smoother to the end user.
1923
            response.body = {
1924
                variables: this.filterVariablesUpdates(updatedVariables, args, this.variables[args.variablesReference])
×
1925
                // TODO: Re-enable this when we can send the correct variables based on the initial inspect context
×
1926
                // variables: sendInvalidatedEvent ? [] : this.filterVariablesUpdates(updatedVariables, args, this.variables[args.variablesReference])
×
1927
            };
×
1928
        } catch (error) {
×
1929
            logger.error('Error during variablesRequest', error, { args });
×
1930
            response.success = false;
×
1931
            response.message = error?.message ?? 'Error during variablesRequest';
×
1932
        } finally {
×
1933
            logger.info('end', { response });
×
1934
        }
×
1935
        this.sendResponse(response);
1936
        if (sendInvalidatedEvent) {
×
1937
            this.debounceSendInvalidatedEvent(null, frameId);
×
1938
        }
×
1939
    }
×
1940

×
1941
    private debounceSendInvalidatedEvent = debounce((threadId: number, frameId: number) => {
1942
        this.sendInvalidatedEvent(threadId, frameId);
×
1943
    }, 50);
1944

×
1945

1946
    private filterVariablesUpdates(updatedVariables: Array<AugmentedVariable>, args: DebugProtocol.VariablesArguments, v: DebugProtocol.Variable): Array<AugmentedVariable> {
×
1947
        if (!updatedVariables || !v) {
×
1948
            return [];
×
1949
        }
×
1950

×
1951
        let start = args.start ?? 0;
1952

×
1953
        //if the variable is an array, send only the requested range
×
1954
        if (Array.isArray(updatedVariables) && args.filter === 'indexed') {
×
1955
            //only send the variable range requested by the debugger
×
1956
            if (!args.count) {
×
1957
                updatedVariables = updatedVariables.slice(0, v.indexedVariables);
×
1958
            } else {
1959
                updatedVariables = updatedVariables.slice(start, start + args.count);
1960
            }
×
1961
        }
1962

1963
        if (Array.isArray(updatedVariables) && args.filter === 'named') {
1964
            // We currently do not support named variable paging so we always send all named variables
×
1965
            updatedVariables = updatedVariables.slice(v.indexedVariables);
1966
        }
1967

×
1968
        let filteredUpdatedVariables = this.launchConfiguration.showHiddenVariables !== true ? updatedVariables.filter(
×
1969
            (child: AugmentedVariable) => !child.name.startsWith(this.tempVarPrefix)) : updatedVariables;
×
1970

×
1971
        if (this.launchConfiguration.showHiddenVariables !== true) {
1972
            filteredUpdatedVariables = filteredUpdatedVariables.filter((child: AugmentedVariable) => {
×
1973
                //A transient variable that we show when there is a value
1974
                if (child.name === '__brs_err__' && child.type !== VariableType.Uninitialized) {
1975
                    return true;
×
1976
                } else if (util.isTransientVariable(child.name)) {
1977
                    return false;
×
1978
                } else {
×
1979
                    return true;
1980
                }
×
1981
            });
1982
        }
×
1983

×
1984
        return filteredUpdatedVariables;
1985
    }
1986

×
1987
    /**
×
1988
     * Takes a scope variable and populates its child variables based on the scope type and the current adapter type.
1989
     * @param v scope variable to populate
×
1990
     * @param args
×
1991
     */
×
1992
    private async populateScopeVariables(v: AugmentedVariable, args: DebugProtocol.VariablesArguments) {
×
1993
        if (v.childVariables.length > 0) {
1994
            // Already populated
1995
            return;
×
1996
        }
1997

1998
        let tempVar: AugmentedVariable;
1999
        try {
×
2000
            if (v.type === '$$Locals') {
×
2001
                if (this.rokuAdapter.isDebugProtocolAdapter()) {
2002
                    let result = await this.rokuAdapter.getLocalVariables(v.frameId);
×
2003
                    tempVar = await this.getVariableFromResult(result, v.frameId);
×
2004
                } else if (this.rokuAdapter.isTelnetAdapter()) {
2005
                    // NOTE: Legacy telnet support
×
2006
                    let variables: AugmentedVariable[] = [];
×
2007
                    const varNames = await this.rokuAdapter.getScopeVariables();
2008

×
2009
                    // Fetch each variable individually
×
2010
                    for (const varName of varNames) {
2011
                        let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: varName, frameId: -1 }, util.getVariablePath(varName));
×
2012
                        let result = await this.rokuAdapter.getVariable(evalArgs.expression, -1);
2013
                        let tempLocalsVar = await this.getVariableFromResult(result, -1);
2014
                        variables.push(tempLocalsVar);
×
2015
                    }
×
2016
                    tempVar = {
2017
                        ...v,
×
2018
                        childVariables: variables,
2019
                        namedVariables: variables.length,
2020
                        indexedVariables: 0
×
2021
                    };
×
2022
                }
2023

2024
                // Merge the resulting updates together onto the original variable
2025
                v.childVariables = tempVar.childVariables;
×
2026
                v.namedVariables = tempVar.namedVariables;
2027
                v.indexedVariables = tempVar.indexedVariables;
×
2028
            } else if (v.type === '$$Registry') {
2029
                // This is a special scope variable used to load registry data via an ECP call
2030
                // Send the registry ECP call for the `dev` app as side loaded apps are always `dev`
2031
                await populateVariableFromRegistryEcp({ host: this.launchConfiguration.host, remotePort: this.launchConfiguration.remotePort, appId: 'dev' }, v, this.variables, this.getEvaluateRefId.bind(this));
2032
            }
2033
        } catch (error) {
×
2034
            logger.error(`Error getting variables for scope ${v.type}`, error);
2035
            tempVar = {
×
2036
                name: '',
2037
                value: `❌ Error: ${error.message}`,
×
2038
                variablesReference: 0,
2039
                childVariables: []
×
2040
            };
2041
            v.childVariables = [tempVar];
×
2042
            v.namedVariables = 1;
2043
            v.indexedVariables = 0;
2044
        }
×
2045

×
2046
        // Mark the scope as resolved so we don't re-fetch the variables
2047
        v.isResolved = true;
2048

×
2049
        // If the scope has no children, add a single child to indicate there are no values
2050
        if (v.childVariables.length === 0) {
2051
            tempVar = {
2052
                name: '',
2053
                value: `No values for scope '${v.name}'`,
×
2054
                variablesReference: 0,
×
2055
                childVariables: []
×
2056
            };
2057
            v.childVariables = [tempVar];
2058
            v.namedVariables = 1;
×
2059
            v.indexedVariables = 0;
2060
        }
2061
    }
2062

×
2063
    private evaluateRequestPromise = Promise.resolve();
×
2064
    private evaluateVarIndexByFrameId = new Map<number, number>();
×
2065

×
2066
    private getNextVarIndex(frameId: number): number {
×
2067
        if (!this.evaluateVarIndexByFrameId.has(frameId)) {
×
2068
            this.evaluateVarIndexByFrameId.set(frameId, 0);
2069
        }
2070
        let value = this.evaluateVarIndexByFrameId.get(frameId);
2071
        this.evaluateVarIndexByFrameId.set(frameId, value + 1);
2072
        return value;
2073
    }
2074

2075
    public async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
2076
        //ensure the rokuAdapter is loaded
×
2077
        await this.getRokuAdapter();
2078

2079
        let deferred = defer<void>();
2080
        if (args.context === 'repl' && this.rokuAdapter.isTelnetAdapter() && args.expression.trim().startsWith('>')) {
2081
            this.clearState();
2082
            this.rokuAdapter.clearCache();
2083
            const expression = args.expression.replace(/^\s*>\s*/, '');
×
2084
            this.logger.log('Sending raw telnet command...I sure hope you know what you\'re doing', { expression });
2085
            this.rokuAdapter.requestPipeline.client.write(`${expression}\r\n`);
2086
            this.sendResponse(response);
2087
            return deferred.promise;
2088
        }
2089

2090
        try {
×
2091
            this.evaluateRequestPromise = this.evaluateRequestPromise.then(() => {
2092
                return deferred.promise;
×
2093
            });
2094

2095
            //fix vscode hover bug that excludes closing quotemark sometimes.
2096
            if (args.context === 'hover') {
2097
                args.expression = util.ensureClosingQuote(args.expression);
2098
            }
28✔
2099

28✔
2100
            if (!this.rokuAdapter.isAtDebuggerPrompt) {
28✔
2101
                let message = 'Skipped evaluate request because RokuAdapter is not accepting requests at this time';
28✔
2102
                if (args.context === 'repl') {
28✔
2103
                    this.sendEvent(new OutputEvent(message, 'stderr'));
28✔
2104
                    response.body = {
28✔
2105
                        result: 'invalid',
28✔
2106
                        variablesReference: 0
2107
                    };
28✔
2108
                } else {
2✔
2109
                    throw new Error(message);
2110
                }
2111

26✔
2112
                //is at debugger prompt
196✔
2113
            } else if (args.expression.trim()) {
2114
                // We trim and check that the expression is not an empty string so that we do not send empty expressions to the Roku
2115
                // This happens mostly when hovering over leading whitespace in the editor
26✔
2116

2117
                let { evalArgs, variablePath } = await this.evaluateExpressionToTempVar(args, util.getVariablePath(args.expression));
2118

2119
                //if we found a variable path (e.g. ['a', 'b', 'c']) then do a variable lookup because it's faster and more widely supported than `evaluate`
26✔
2120
                if (variablePath) {
4✔
2121
                    let refId = this.getEvaluateRefId(evalArgs.expression, evalArgs.frameId);
2122
                    let v: AugmentedVariable;
2123
                    //if we already looked this item up, return it
22✔
2124
                    if (this.variables[refId]) {
22✔
2125
                        v = this.variables[refId];
2126
                    } else {
2✔
2127
                        let result = await this.rokuAdapter.getVariable(evalArgs.expression, evalArgs.frameId);
2128
                        if (!result) {
20✔
2129
                            throw new Error('Error: unable to evaluate expression');
2130
                        }
2✔
2131

2132
                        v = await this.getVariableFromResult(result, evalArgs.frameId);
2133
                        //TODO - testing something, remove later
18✔
2134
                        // eslint-disable-next-line camelcase
2135
                        v.request_seq = response.request_seq;
2136
                        v.frameId = evalArgs.frameId;
22✔
2137
                    }
2✔
2138
                    response.body = {
2139
                        result: v.value,
2140
                        type: v.type,
2141
                        variablesReference: v.variablesReference,
20✔
2142
                        namedVariables: v.namedVariables || 0,
2✔
2143
                        indexedVariables: v.indexedVariables || 0
2144
                    };
2145

2146
                    //run an `evaluate` call
18✔
2147
                } else {
2148
                    let commandResults = await this.rokuAdapter.evaluate(evalArgs.expression, evalArgs.frameId);
2149

20✔
2150
                    commandResults.message = util.trimDebugPrompt(commandResults.message);
8✔
2151
                    if (args.context === 'repl') {
2152
                        // Clear variable cache since this action could have side-effects
20✔
2153
                        // Only do this for REPL requests as hovers and watches should not clear the cache
2154
                        this.clearState();
2155
                        this.sendInvalidatedEvent(null, evalArgs.frameId);
2156
                    }
×
2157

×
2158
                    // If the adapter captured output (probably only telnet), log the results
2159
                    if (typeof commandResults.message === 'string') {
×
2160
                        this.logger.debug('evaluateRequest', { commandResults });
×
2161
                        if (args.context === 'repl') {
2162
                            // If the command was a repl command, send the output to the debug console for the developer as well
2163
                            // We limit this to repl only so you don't get extra logs when hovering over variables ro running watches
×
2164
                            this.sendEvent(new OutputEvent(commandResults.message, commandResults.type === 'error' ? 'stderr' : 'stdio'));
×
2165
                        }
2166
                    }
2167

×
2168
                    if (this.enableDebugProtocol || (typeof commandResults.message !== 'string')) {
2169
                        response.body = {
×
2170
                            result: 'invalid',
2171
                            variablesReference: 0
2172
                        };
×
2173
                    } else {
2174
                        response.body = {
2175
                            result: commandResults.message === '\r\n' ? 'invalid' : commandResults.message,
×
2176
                            variablesReference: 0
2177
                        };
×
2178
                    }
2179
                }
×
2180
            }
2181
        } catch (error) {
×
2182
            this.logger.error('Error during variables request', error);
2183
            response.success = false;
×
2184
            response.message = error?.message ?? error;
2185
        }
×
2186
        try {
2187
            this.sendResponse(response);
×
2188
        } catch { }
2189
        deferred.resolve();
×
2190
    }
2191

×
2192
    private async evaluateExpressionToTempVar(args: DebugProtocol.EvaluateArguments, variablePath: string[]): Promise<{ evalArgs: DebugProtocol.EvaluateArguments; variablePath: string[] }> {
2193
        let returnVal = { evalArgs: args, variablePath };
×
2194
        if (!variablePath && util.isAssignableExpression(args.expression)) {
2195
            let varIndex = this.getNextVarIndex(args.frameId);
×
2196
            let arrayVarName = this.tempVarPrefix + 'eval';
2197
            let command = '';
2198
            if (varIndex === 0) {
2199
                await this.rokuAdapter.evaluate(`if type(${arrayVarName}) = "<uninitialized>" then ${arrayVarName} = []\n`, args.frameId);
2200
            }
2201
            let statement = `${arrayVarName}[${varIndex}] = ${args.expression}`;
2202
            returnVal.evalArgs.expression = `${arrayVarName}[${varIndex}]`;
2203
            command += statement;
2204
            let commandResults = await this.rokuAdapter.evaluate(command, args.frameId);
2205
            if (commandResults.type === 'error') {
×
2206
                throw new Error(commandResults.message);
×
2207
            }
2208
            returnVal.variablePath = [arrayVarName, varIndex.toString()];
×
2209
        }
×
2210
        return returnVal;
2211
    }
2212

1!
2213
    private async bulkEvaluateExpressionToTempVar(frameId: number, argsArray: Array<DebugProtocol.EvaluateArguments>, variablePathArray: Array<string[]>): Promise<{ evaluations: Array<{ evalArgs: DebugProtocol.EvaluateArguments; variablePath: string[] }>; bulkVarName: string }> {
×
2214
        let results = {
2215
            evaluations: [],
2216
            bulkVarName: ''
1✔
2217
        };
2218
        let storedVariables = [];
2219
        let command = '';
2220
        for (let i = 0; i < argsArray.length; i++) {
×
2221
            let args = argsArray[i];
×
2222
            let variablePath = variablePathArray[i];
×
2223
            let returnVal = { evalArgs: args, variablePath };
×
2224
            if (!variablePath && util.isAssignableExpression(args.expression)) {
2225
                let varIndex = this.getNextVarIndex(frameId);
×
2226
                let arrayVarName = this.tempVarPrefix + 'eval';
×
2227
                if (varIndex === 0) {
×
2228
                    command += `if type(${arrayVarName}) = "<uninitialized>" then ${arrayVarName} = []\n`;
×
2229
                }
×
2230
                let statement = `${arrayVarName}[${varIndex}] = ${args.expression}\n`;
2231
                returnVal.evalArgs.expression = `${arrayVarName}[${varIndex}]`;
×
2232
                command += statement;
2233

2234
                storedVariables.push(`${arrayVarName}[${varIndex}]`);
×
2235
                returnVal.variablePath = [arrayVarName, varIndex.toString()];
×
2236
            }
×
2237

×
2238
            results.evaluations[i] = returnVal;
2239
        }
×
2240

×
2241
        if (command) {
2242

2243
            // create a bulk container for the command results
2244
            let varIndex = this.getNextVarIndex(frameId);
2245
            let arrayVarName = this.tempVarPrefix + 'eval';
2246
            let bulkContainerStatement = `${arrayVarName}[${varIndex}] = [\n`;
×
2247
            for (let storedVariable of storedVariables) {
×
2248
                bulkContainerStatement += `${storedVariable},\n`;
2249
            }
2250
            bulkContainerStatement += `]`;
2251

2252
            command += bulkContainerStatement;
×
2253

2254
            results.bulkVarName = `${arrayVarName}[${varIndex}]`;
2255

2256
            let commandResults = await this.rokuAdapter.evaluate(command, frameId);
2257
            if (commandResults.type === 'error') {
2258
                throw new Error(commandResults.message);
2259
            }
×
2260
        }
×
2261

2262
        return results;
2263
    }
2264

×
2265
    protected async completionsRequest(response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments, request?: DebugProtocol.Request) {
2266
        this.logger.log('completionsRequest', args, request);
×
2267
        // this.sendEvent(new LogOutputEvent(`completionsRequest: ${args.text}`));
2268
        // this.sendEvent(new OutputEvent(`completionsRequest: ${args.text}\n`, 'stderr'));
2269

2270
        try {
2271
            let supplyLocalScopeCompletions = false;
2272

2273
            let closestCompletionDetails = this.getClosestCompletionDetails(args);
×
2274

×
2275
            if (!closestCompletionDetails) {
×
2276
                // If the cursor is not at the end of the line, then we should not supply completions at this time
×
2277
                response.body = {
2278
                    targets: []
2279
                };
×
2280
                return this.sendResponse(response);
×
2281
            }
2282
            let completions = new Map<string, DebugProtocol.CompletionItem>();
2283

2284
            let parentVariablePath = closestCompletionDetails.parentVariablePath;
×
2285
            // Get the completions if the variable path was valid
×
2286
            if (parentVariablePath) {
2287

2288
                // If the parent variable path is an empty string, then we are looking up the local scope variables and global functions
2289
                if (parentVariablePath.length === 1 && parentVariablePath[0] === '') {
×
2290
                    supplyLocalScopeCompletions = true;
2291
                }
×
2292

×
2293
                // Look up the parent variable
×
2294
                let parentVariable = this.findVariableByPath(Object.values(this.variables), parentVariablePath, args.frameId);
×
2295

2296
                if (!parentVariable || parentVariable.childVariables.length === 0) {
2297
                    // We did not find the parent variable, so try to look it up from the device
×
2298
                    try {
×
2299
                        let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: parentVariablePath.join('.'), frameId: args.frameId }, parentVariablePath);
2300
                        let result = await this.rokuAdapter.getVariable(evalArgs.expression, args.frameId);
2301
                        parentVariable = await this.getVariableFromResult(result, args.frameId);
×
2302
                    } catch (error) {
×
2303
                        this.logger.error('Error looking up parent completions', error, { parentVariablePath });
×
2304
                    }
2305
                }
2306

2307
                // provide completions for the parent variable if one was found
1✔
2308
                if (parentVariable) {
1✔
2309
                    let possibleFieldsAndMethods: AugmentedVariable[] = [];
2310
                    // Filter out virtual variables
1!
2311
                    possibleFieldsAndMethods = parentVariable.childVariables.filter((v) => v.presentationHint?.kind !== 'virtual');
1✔
2312

2313
                    for (let v of possibleFieldsAndMethods) {
1!
2314
                        // Default completion type should be variable
×
2315
                        let completionType: DebugProtocol.CompletionItemType = 'variable';
×
2316
                        if (!supplyLocalScopeCompletions) {
2317
                            // We are not supplying local scope completions, so we need to determine the completion type relative to the parent variable
2318
                            if (parentVariable.type === 'roSGNode' || parentVariable.type === VariableType.AssociativeArray || parentVariable.type === VariableType.Object) {
1✔
2319
                                completionType = 'field';
2320
                            }
6!
2321

2322
                            switch (v.type) {
2323
                                case VariableType.Function:
1✔
2324
                                case VariableType.Subroutine:
1✔
2325
                                    completionType = 'method';
2326
                                    break;
2327
                                default:
2328
                                    break;
2329
                            }
8✔
2330
                        }
8✔
2331

2332
                        let label = v.name;
2333
                        if (parentVariable.type === VariableType.Array ||
8✔
2334
                            parentVariable.type === VariableType.List ||
2335
                            parentVariable.type === 'roXMLList' ||
9✔
2336
                            parentVariable.type === 'roByteArray'
9✔
2337
                        ) {
9✔
2338
                            label = `[${v.name}]`;
2339
                        }
2340
                        completions.set(`${completionType}-${v.name}`, {
9✔
2341
                            label: label,
2✔
2342
                            type: completionType,
2✔
2343
                            sortText: '000000'
2!
2344
                        });
2345
                    }
2346

8✔
2347
                    let parentComponentType = this.debuggerVarTypeToRoType(parentVariable.type).toLowerCase();
4✔
2348
                    //assemble a list of all methods on the parent component
4✔
2349
                    const methods = [
2350
                        //if the parent variable is an actual interface (if applicable) Ex: `ifString` or `ifArray`
2351
                        ...interfaces[parentComponentType as 'ifappinfo']?.methods ?? [],
4✔
2352
                        //interfaces from component of this name (if applicable) Ex: `roSGNode` or `roDateTime`
1✔
2353
                        ...components[parentComponentType as 'roappinfo']?.interfaces.map((i) => interfaces[i.name.toLowerCase() as 'ifappinfo']?.methods) ?? [],
×
2354
                        // Add parent event function completions (if applicable) Ex: `roSGNodeEvent` or `roDeviceInfoEvent`
2355
                        ...events[parentComponentType as 'roappmemorymonitorevent']?.methods ?? []
1✔
2356
                    ].flat();
1✔
2357

2358
                    // Based on the results of interface, component, and event looks up, add all the methods to the completions
2359
                    for (const method of methods) {
2360
                        completions.set(`method-${method.name}`, {
2361
                            label: method.name,
8!
2362
                            type: 'method',
8✔
2363
                            detail: method.description ?? '',
8✔
2364
                            sortText: '000000'
8✔
2365
                        });
2366
                    }
10✔
2367

2368
                    // Add the global functions to the completions results
2369
                    if (supplyLocalScopeCompletions) {
14!
2370
                        for (let globalCallable of globalCallables) {
14✔
2371
                            completions.set(`function-${globalCallable.name.toLocaleLowerCase()}`, {
5✔
2372
                                label: globalCallable.name,
5!
2373
                                type: 'function',
×
2374
                                detail: globalCallable.shortDescription ?? globalCallable.documentation ?? '',
2375
                                sortText: '000000'
×
2376
                            });
×
2377
                        }
×
2378

2379
                        const frame = this.rokuAdapter.getStackFrameById(args.frameId);
2380

×
2381
                        try {
×
2382
                            let scopeFunctions = await this.projectManager.getScopeFunctionsForFile(frame.filePath as string);
2383
                            for (let scopeFunction of scopeFunctions) {
2384
                                if (!completions.has(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`)) {
2385
                                    completions.set(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`, {
2386
                                        label: scopeFunction.name,
2387
                                        type: scopeFunction.completionItemKind,
2388
                                        sortText: '000000'
2389
                                    });
2390
                                }
2391
                            }
2392
                        } catch (e) {
5!
2393
                            this.logger.warn('Could not build list of scope functions for file', e);
×
2394
                        }
×
2395
                    }
×
2396
                }
×
2397
            }
2398

2399
            // this.sendEvent(new LogOutputEvent(`text: ${args.text} | completions: ${completions.map(v => v.label).join(', ')}`));
×
2400
            // this.sendEvent(new OutputEvent(`text: ${args.text} | completions: ${completions.map(v => v.label).join(', ')}\n`, 'stderr'));
×
2401

2402
            response.body = {
2403
                targets: [...completions.values()]
×
2404
            };
2405
        } catch (error) {
×
2406
            // this.sendEvent(new LogOutputEvent(`text: ${args.text} | ${error}`));
2407
            // this.sendEvent(new OutputEvent(`text: ${args.text} | ${error}\n`, 'stderr'));
×
2408
            this.logger.error('Error during completionsRequest', error, { args });
2409
        }
×
2410
        this.sendResponse(response);
2411
    }
×
2412

2413
    /**
2414
     * Gets the closest completion details the incoming completion request.
2415
     */
5!
2416
    private getClosestCompletionDetails(args: DebugProtocol.CompletionsArguments): { parentVariablePath: string[] } {
×
2417
        const incomingText = args.text;
2418
        const lines = incomingText.split('\n');
5!
2419
        let lineNumber = this.toDebuggerLine(args.line, 0);
×
2420
        let column = this.toDebuggerColumn(args.column);
2421

2422
        const targetLine = lines[lineNumber];
5✔
2423
        let variablePathString = '';
2424

2425
        let i = column - 1;
2426
        const variableChars = /[a-z0-9_\.]/i;
5!
2427

2428
        // If the character at immediate to the right of the cursor is a variable character, then we are not at the end of the variable path.
5✔
2429
        if (targetLine.length - 1 > i && variableChars.test(targetLine[i + 1])) {
2430
            return undefined;
9!
2431
        }
9✔
2432

7✔
2433
        // Find the start of the variable path by looking for the first non-alphanumeric or non_underscore character before the cursor
2434
        while (i >= 0 && (variableChars.test(targetLine[i]))) {
2✔
2435
            i--;
1✔
2436
        }
1!
2437

1✔
2438
        // Pull the variable path string from the line
2439
        variablePathString = targetLine.slice(i + 1, column);
1!
2440

2441
        // Attempted dot access something unexpected
2442
        // Example: `getPerson().name` where `getPerson()` is not a valid variable
1!
2443
        // and results in `.name` being the variable path string
1✔
2444
        if (variablePathString.startsWith('.')) {
2445
            return undefined;
1!
2446
        }
1✔
2447

2448
        // Get the variable path from the text
×
2449
        let variablePath: string[] = [];
×
2450
        if (!variablePathString.trim()) {
2451
            // The text was empty so assume via '' that we are looking up the local scope variables and global functions
2452
            variablePath = [''];
2453
        } else if (variablePathString.endsWith('.')) {
×
2454
            // supplied text ends with a period, so strip it off to create a valid variable path
2455
            variablePath = util.getVariablePath(variablePathString.slice(0, -1));
2456
        } else {
14✔
2457
            variablePath = util.getVariablePath(variablePathString);
14✔
2458
        }
14✔
2459

14✔
2460
        // the target string is not a valid variable path
14!
2461
        if (!variablePath) {
14!
2462
            return undefined;
×
2463
        }
2464

14✔
2465
        let parentVariablePath: string[];
2!
2466
        // If the last character is a period, then pull completions for the parent variable before the period
2✔
2467
        if (variablePathString.endsWith('.')) {
2468
            parentVariablePath = variablePath;
2469
        } else {
2✔
2470
            // Otherwise, pull completions for the parent variable
2471
            parentVariablePath = variablePath.slice(0, variablePath.length - 1);
4!
2472
        }
4✔
2473

2474
        // If the parent variable path is empty or an empty string, then we are looking up the local scope variables and global functions
2!
2475
        if (parentVariablePath.length === 0) {
×
2476
            parentVariablePath = [''];
×
2477
        }
×
2478

2479
        return { parentVariablePath: parentVariablePath };
×
2480
    }
×
2481

2482
    private findVariableByPath(variables: AugmentedVariable[], path: string[], frameId: number) {
×
2483
        let current: AugmentedVariable = null;
×
2484
        for (const name of path) {
×
2485
            // Find the object matching the current name in the data
×
2486
            current = (Array.isArray(variables) ? variables : current?.childVariables)?.find(obj => {
×
2487
                return obj.name === name && obj.frameId === frameId;
×
2488
            });
×
2489

×
2490
            // If no match is found, return null
×
2491
            if (!current) {
2492
                return null;
2493
            }
2494

2495
            // Move to the children for the next iteration
×
2496
            variables = current.childVariables;
2497
        }
2498
        return current;
2499
    }
2✔
2500

4✔
2501
    private debuggerVarTypeToRoType(type: string): string {
2502
        switch (type) {
2503
            case VariableType.Function:
2504
            case VariableType.Subroutine:
12✔
2505
                return 'roFunction';
2506
            case VariableType.AssociativeArray:
2507
                return 'roAssociativeArray';
14!
2508
            case VariableType.List:
×
2509
                return 'roList';
×
2510
            case VariableType.Array:
2511
                return 'roArray';
2512
            case VariableType.Boolean:
×
2513
                return 'roBoolean';
2514
            case VariableType.Double:
2515
                return 'roDouble';
2516
            case VariableType.Float:
14✔
2517
                return 'roFloat';
2518
            case VariableType.Integer:
2519
                return 'roInteger';
2520
            case VariableType.LongInteger:
2521
                return 'roLongInteger';
2522
            case VariableType.String:
2523
                return 'roString';
×
2524
            default:
×
2525
                return type;
×
2526
        }
×
2527
    }
×
2528

×
2529
    /**
×
2530
     * Called when the host stops debugging
2531
     * @param response
2532
     * @param args
28✔
2533
     */
28✔
2534
    protected async disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request) {
19✔
2535
        //return to the home screen
2536
        if (!this.enableDebugProtocol) {
28✔
2537
            await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
2538
        }
2539
        this.sendResponse(response);
2540
        await this.shutdown();
11✔
2541
    }
2542

2543
    private createRokuAdapter(rendezvousTracker: RendezvousTracker) {
2544
        if (this.enableDebugProtocol) {
2545
            this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration, this.projectManager, this.breakpointManager, rendezvousTracker, this.deviceInfo);
2546
        } else {
2547
            this.rokuAdapter = new TelnetAdapter(this.launchConfiguration, rendezvousTracker);
2548
        }
2549
    }
2550

56✔
2551
    protected async restartRequest(response: DebugProtocol.RestartResponse, args: DebugProtocol.RestartArguments, request?: DebugProtocol.Request) {
25✔
2552
        this.logger.log('[restartRequest] begin');
2553
        if (this.rokuAdapter) {
31✔
2554
            if (!this.enableDebugProtocol) {
10✔
2555
                this.rokuAdapter.removeAllListeners();
10✔
2556
            }
2557
            await this.rokuAdapter.destroy();
21✔
2558
            await this.ensureAppIsInactive();
18✔
2559
            this.rokuAdapterDeferred = defer();
13✔
2560
            this.stagingDefered.tryResolve();
2561
            this.stagingDefered = defer();
2562
        }
5✔
2563
        await this.launchRequest(response, args.arguments as LaunchConfiguration);
5✔
2564
    }
5✔
2565

5✔
2566
    private exitAppTimeout = 5000;
2567
    private async ensureAppIsInactive() {
5✔
2568
        const startTime = Date.now();
2569

2570
        while (true) {
2571
            if (Date.now() - startTime > this.exitAppTimeout) {
2572
                return;
2573
            }
2574

2575
            try {
2576
                let appStateResult = await rokuECP.getAppState({
2577
                    host: this.launchConfiguration.host,
2578
                    remotePort: this.launchConfiguration.remotePort,
3!
2579
                    appId: 'dev',
×
2580
                    requestOptions: { timeout: 300 }
2581
                });
2582

2583
                const state = appStateResult.state;
2584

2585
                if (state === AppState.active || state === AppState.background) {
2586
                    // Suspends or terminates an app that is running:
4!
2587
                    // If the app supports Instant Resume and is running in the foreground, sending this command suspends the app (the app runs in the background).
4✔
2588
                    // If the app supports Instant Resume and is running in the background or the app does not support Instant Resume and is running, sending this command terminates the app.
4✔
2589
                    // This means that we might need to send this command twice to terminate the app.
1✔
2590
                    await rokuECP.exitApp({
2591
                        host: this.launchConfiguration.host,
2592
                        remotePort: this.launchConfiguration.remotePort,
2593
                        appId: 'dev',
2594
                        requestOptions: { timeout: 300 }
2595
                    });
2596
                } else if (state === AppState.inactive) {
2597
                    return;
2598
                }
2599
            } catch (e) {
2600
                this.logger.error('Error attempting to exit application', e);
2601
            }
3!
2602

2603
            await util.sleep(200);
2604
        }
2605
    }
2606

2607
    /**
2608
     * Used to track whether the entry breakpoint has already been handled
2609
     */
2610
    private entryBreakpointWasHandled = false;
2611

3!
2612
    /**
2613
     * Registers the main events for the RokuAdapter
2614
     */
2615
    private async connectRokuAdapter() {
2616
        this.rokuAdapter.on('start', () => {
2617
            this.sendLaunchProgress('end', 'Complete');
2618
            if (!this.firstRunDeferred.isCompleted) {
2619
                this.firstRunDeferred.resolve();
2620
            }
2621
        });
28✔
2622

2✔
2623
        this.rokuAdapter.on('launch-status', (message) => {
2624
            this.sendLaunchProgress('update', message);
26✔
2625
        });
2626

2627
        //when the debugger suspends (pauses for debugger input)
2628
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
2629
        this.rokuAdapter.on('suspend', async () => {
2630
            await this.onSuspend();
2631
        });
2632

2633
        //anytime the adapter encounters an exception on the roku,
2634
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
28!
2635
        this.rokuAdapter.on('runtime-error', async (exception) => {
28✔
2636
            await this.getRokuAdapter();
2637
            const threads = await this.setupSuspendedState();
×
2638
            let threadId = threads[0]?.threadId;
2639
            this.sendEvent(new StoppedEvent(StoppedEventReason.exception, threadId, exception.message));
2640
        });
2641

2642
        // If the roku says it can't continue, we are no longer able to debug, so kill the debug session
2643
        this.rokuAdapter.on('cannot-continue', () => {
15✔
2644
            this.shutdown().catch(e => this.logger.error(e));
15!
2645
        });
15✔
2646

15✔
2647
        //make the connection
2648
        await this.rokuAdapter.connect();
2649
        this.rokuAdapterDeferred.resolve(this.rokuAdapter);
×
2650
        return this.rokuAdapter;
2651
    }
15✔
2652

2653
    private async onSuspend() {
×
2654
        const threads = await this.setupSuspendedState();
2655
        const activeThread = threads.find(x => x.isSelected);
2656

15✔
2657
        //if !stopOnEntry, and we haven't encountered a suspend yet, THIS is the entry breakpoint. auto-continue
2658
        if (!this.entryBreakpointWasHandled && !this.launchConfiguration.stopOnEntry) {
15✔
2659
            this.entryBreakpointWasHandled = true;
15!
2660
            //if there's a user-defined breakpoint at this exact position, it needs to be handled like a regular breakpoint (i.e. suspend). So only auto-continue if there's no breakpoint here
×
2661
            if (activeThread && !await this.breakpointManager.lineHasBreakpoint(this.projectManager.getAllProjects(), activeThread.filePath, activeThread.lineNumber - 1)) {
×
2662
                this.logger.info('Encountered entry breakpoint and `stopOnEntry` is disabled. Continuing...');
×
2663
                return this.rokuAdapter.continue();
2664
            }
2665
        }
2666

2667
        const event: StoppedEvent = new StoppedEvent(
×
2668
            StoppedEventReason.breakpoint,
2669
            //Not sure why, but sometimes there is no active thread. Just pick thread 0 to prevent the app from totally crashing
2670
            activeThread?.threadId ?? 0,
15✔
2671
            '' //exception text
15✔
2672
        );
2673
        // Socket debugger will always stop all threads and supports multi thread inspection.
2674
        (event.body as any).allThreadsStopped = this.enableDebugProtocol;
15✔
2675
        this.sendEvent(event);
2676
    }
15✔
2677

15!
2678
    private async setupSuspendedState() {
2679
        //clear the index for storing evalutated expressions
2680
        this.evaluateVarIndexByFrameId.clear();
×
2681

2682
        const threads = await this.rokuAdapter.getThreads();
2683

15✔
2684
        //TODO remove this once Roku fixes their threads off-by-one line number issues
15✔
2685
        //look up the correct line numbers for each thread from the StackTrace
15!
2686
        await Promise.all(
2687
            threads.map(async (thread) => {
15✔
2688
                const stackTrace = await this.rokuAdapter.getStackTrace(thread.threadId);
15✔
2689
                const stackTraceLineNumber = stackTrace[0]?.lineNumber;
15✔
2690
                const stackTraceFilePath = stackTrace[0]?.filePath;
2691
                // Only apply the line correction when we actually have valid data — never clobber
2692
                // thread.filePath with undefined, which would crash getSourceLocation downstream.
×
2693
                if (stackTraceLineNumber !== undefined && stackTraceLineNumber !== thread.lineNumber) {
2694
                    this.logger.warn(`Thread ${thread.threadId} reported incorrect line (${thread.lineNumber}). Using line from stack trace instead (${stackTraceLineNumber})`, thread, stackTrace);
2695
                    thread.lineNumber = stackTraceLineNumber;
2696
                    thread.filePath = stackTraceFilePath ?? thread.filePath;
×
2697
                }
2698
            })
15✔
2699
        );
15!
2700

2701
        outer: for (const bp of this.breakpointManager.failedDeletions) {
2702
            for (const thread of threads) {
×
2703
                let sourceLocation = await this.projectManager.getSourceLocation(thread.filePath, thread.lineNumber);
2704
                // This stop was due to a breakpoint that we tried to delete, but couldn't.
15✔
2705
                // Now that we are stopped, we can delete it. We won't stop here again unless you re-add the breakpoint. You're welcome.
15!
2706
                if (sourceLocation && (bp.srcPath === sourceLocation.filePath) && (bp.line === sourceLocation.lineNumber)) {
2707
                    this.showPopupMessage(`Stopped at breakpoint that failed to delete. Deleting now, and should not cause future stops.`, 'info').catch((error) => {
2708
                        this.logger.error('Error showing popup message', { error });
×
2709
                    });
2710
                    this.logger.warn(`Stopped at breakpoint that failed to delete. Deleting now, and should not cause future stops`, bp, thread, sourceLocation);
15✔
2711
                    break outer;
15!
2712
                }
2713
            }
2714
        }
×
2715

2716
        //sync breakpoints
15✔
2717
        await this.rokuAdapter?.syncBreakpoints();
15!
2718

2719
        this.logger.info('received "suspend" event from adapter');
2720

×
2721
        this.clearState();
2722
        return threads;
15✔
2723
    }
2724

15!
2725
    private async getVariableFromResult(result: EvaluateContainer, frameId: number, maxDepth = 1) {
15!
2726
        let v: AugmentedVariable;
15✔
2727

15✔
2728
        if (result) {
2✔
2729
            if (this.rokuAdapter.isDebugProtocolAdapter()) {
2✔
2730
                let refId = this.getEvaluateRefId(result.evaluateName, frameId);
2731
                if (result.isCustom && !result.presentationHint?.lazy && result.evaluateNow) {
2732
                    try {
×
2733
                        // We should not wait to resolve this variable later. Fetch, store, and merge the results right away.
×
2734
                        let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: result.evaluateName, frameId: frameId }, util.getVariablePath(result.evaluateName));
2735
                        let newResult = await this.rokuAdapter.getVariable(evalArgs.expression, frameId);
2736
                        this.mergeEvaluateContainers(result, newResult);
2737
                    } catch (error) {
2738
                        logger.error('Error getting variables', error);
2739
                        this.mergeEvaluateContainers(result, {
×
2740
                            name: result.name,
2741
                            evaluateName: result.evaluateName,
15✔
2742
                            children: [],
15✔
2743
                            value: `❌ Error: ${error.message}`,
15✔
2744
                            type: '',
2745
                            highLevelType: undefined,
15✔
2746
                            keyType: undefined
15✔
2747
                        });
15✔
2748
                    }
2749
                }
2750

×
2751
                if (result.keyType) {
2752
                    let value = `${result.value ?? result.type}`;
15✔
2753
                    let indexedVariables = result.indexedVariables;
15✔
2754
                    let namedVariables = result.namedVariables;
2755

2756
                    if (indexedVariables === undefined || namedVariables === undefined) {
×
2757
                        // If either indexed or named variables are undefined, we should tell the debugger to ask for everything
2758
                        // by supplying undefined values for both
2759
                        indexedVariables = undefined;
2760
                        namedVariables = undefined;
2✔
2761
                    }
2✔
2762

2763
                    // check to see if this is an dictionary or a list
2764
                    if (result.keyType === 'Integer') {
2765
                        // list type
2766
                        v = new Variable(result.name, value, refId, indexedVariables as number, namedVariables as number);
2767
                    } else if (result.keyType === 'String') {
2768
                        // dictionary type
2769
                        v = new Variable(result.name, value, refId, indexedVariables as number, namedVariables as number);
2770
                    }
2771
                    v.type = result.type;
2772
                } else {
2773

2774
                    let value: string;
2775
                    if (result.type === VariableType.Invalid) {
2776
                        value = result.value ?? 'Invalid';
2777
                    } else if (result.type === VariableType.Uninitialized) {
2778
                        value = 'Uninitialized';
2779
                    } else {
2780
                        value = `${result.value}`;
2781
                    }
2782
                    // If the variable is lazy we must assign a refId to inform the system
2783
                    // to request this variable again in the future for value resolution
2784
                    v = new Variable(result.name, value, result?.presentationHint?.lazy ? refId : 0);
2785
                }
2786
                this.variables[refId] = v;
2787
            } else if (this.rokuAdapter.isTelnetAdapter()) {
2788
                if (result.highLevelType === 'primative' || result.highLevelType === 'uninitialized') {
2789
                    v = new Variable(result.name, `${result.value}`);
2790
                } else if (result.highLevelType === 'array') {
2791
                    let refId = this.getEvaluateRefId(result.evaluateName, frameId);
2792
                    v = new Variable(result.name, result.type, refId, result.children?.length ?? 0, 0);
2793
                    this.variables[refId] = v;
2794
                } else if (result.highLevelType === 'object') {
2795
                    let refId: number;
2796
                    //handle collections
2797
                    if (this.rokuAdapter.isScrapableContainObject(result.type)) {
2798
                        refId = this.getEvaluateRefId(result.evaluateName, frameId);
2799
                    }
2800
                    v = new Variable(result.name, result.type, refId, 0, result.children?.length ?? 0);
2801
                    this.variables[refId] = v;
2802
                } else if (result.highLevelType === 'function') {
2803
                    v = new Variable(result.name, `${result.value}`);
2804
                } else {
2805
                    //all other cases, but mostly for HighLevelType.unknown
2806
                    v = new Variable(result.name, `${result.value}`);
2807
                }
2808
            }
2809

2810
            v.type = result.type;
2811
            v.evaluateName = result.evaluateName;
2812
            v.frameId = frameId;
2813
            v.type = result.type;
2814
            v.presentationHint = result.presentationHint ? { kind: result.presentationHint?.kind, lazy: result.presentationHint?.lazy } : undefined;
2815
            if (util.isTransientVariable(v.name)) {
2816
                v.presentationHint = { kind: 'virtual' };
2817
            }
2818

2819
            if (result.children && maxDepth > 0) {
2820
                if (!v.childVariables) {
2821
                    v.childVariables = [];
2822
                }
2823

2824
                // Create a mapping of the children to their index so we can evaluate them in bulk
2825
                let indexMappedChildren = result.children.map((child, index) => {
2826
                    let remapped = { child: child, index: index, evaluate: !!(child.isCustom && !child.presentationHint?.lazy && child.evaluateNow) };
2827
                    return remapped;
2828
                });
2829
                if (this.enableDebugProtocol) {
2830
                    let childrenToEvaluate = indexMappedChildren.filter(x => x.evaluate);
2831
                    let evaluateArgsArray = childrenToEvaluate.map(x => {
2832
                        return { expression: x.child.evaluateName, frameId: frameId };
2833
                    });
2834

2835
                    let variablePathArray = childrenToEvaluate.map(x => {
2836
                        return util.getVariablePath(x.child.evaluateName);
2837
                    });
2838

2839
                    try {
2840
                        let bulkEvaluations = await this.bulkEvaluateExpressionToTempVar(frameId, evaluateArgsArray, variablePathArray);
2841
                        if (bulkEvaluations.bulkVarName) {
2842
                            let newResults = await this.rokuAdapter.getVariable(bulkEvaluations.bulkVarName, frameId);
2843
                            childrenToEvaluate.map((mappedChild, index) => {
2844
                                let newResult = newResults.children[index];
2845
                                this.mergeEvaluateContainers(mappedChild.child, newResult);
2846
                                mappedChild.child.evaluateNow = false;
2847
                                return mappedChild;
2848
                            });
2849
                        }
2850
                    } catch (error) {
2851
                        this.logger.error('Error getting bulk variables, will fall back to var by var lookups', error);
2852
                    }
2853
                }
2854
                // If bulk evaluations failed, there is fall back logic in `getVariableFromResult` to do individual evaluations
2855
                v.childVariables = await Promise.all(indexMappedChildren.map(async (mappedChild) => {
2856
                    return this.getVariableFromResult(mappedChild.child, frameId, maxDepth - 1);
2857
                }));
2858
            } else {
2859
                v.childVariables = [];
2860
            }
2861

2862
            // if the var is an array and debugProtocol is enabled, include the array size
2863
            if (this.enableDebugProtocol && v.type === VariableType.Array) {
2864
                if (isNaN(result.indexedVariables as number)) {
2865
                    v.value = v.type;
2866
                } else {
2867
                    v.value = `${v.type}(${result.indexedVariables})`;
2868
                }
2869
            }
2870
        }
2871
        return v;
2872
    }
2873

2874
    /**
2875
     * Helper function to merge the results of an evaluate call into an existing EvaluateContainer
2876
     * Used primarily for custom variables
2877
     */
2878
    private mergeEvaluateContainers(original: EvaluateContainer, updated: EvaluateContainer) {
2879
        original.children = updated.children;
2880
        original.value = updated.value;
2881
        original.type = updated.type;
2882
        original.highLevelType = updated.highLevelType;
2883
        original.keyType = updated.keyType;
2884
        original.indexedVariables = updated.indexedVariables;
2885
        original.namedVariables = updated.namedVariables;
2886
    }
2887

2888
    private getEvaluateRefId(expression: string, frameId: number) {
2889
        let evaluateRefId = `${expression}-${frameId}`;
2890
        if (!this.evaluateRefIdLookup[evaluateRefId]) {
2891
            this.evaluateRefIdLookup[evaluateRefId] = this.evaluateRefIdCounter++;
2892
        }
2893
        return this.evaluateRefIdLookup[evaluateRefId];
2894
    }
2895

2896
    private clearState() {
2897
        //erase all cached variables
2898
        this.variables = {};
2899
    }
2900

2901
    /**
2902
     * Sends a launch progress event to the client if the client supports progress reporting.
2903
     * - `'start'`: begins a new progress bar with the given message. Assigns a new progressId.
2904
     * - `'update'`: updates the message on the active progress bar.
2905
     * - `'end'`: dismisses the active progress bar with an optional final message.
2906
     */
2907
    private sendLaunchProgress(type: 'start' | 'update' | 'end', message?: string) {
2908
        if (!this.initRequestArgs?.supportsProgressReporting) {
2909
            return;
2910
        }
2911
        if (type === 'start') {
2912
            this.launchProgressId = `rokudebug-launch-${this.idCounter++}`;
2913
            this.sendEvent(new ProgressStartEvent(this.launchProgressId, 'Launching', `${message}...`));
2914
        } else if (this.launchProgressId) {
2915
            if (type === 'update') {
2916
                this.sendEvent(new ProgressUpdateEvent(this.launchProgressId, `${message}...`));
2917
            } else {
2918
                const lastId = this.launchProgressId;
2919
                this.sendEvent(new ProgressUpdateEvent(lastId, message));
2920
                setTimeout(() => {
2921
                    this.sendEvent(new ProgressEndEvent(lastId, message));
2922
                }, 1000); // add a slight delay before ending the progress to improve UX
2923
                this.launchProgressId = undefined;
2924
            }
2925
        }
2926
    }
2927

2928
    /**
2929
     * Tells the client to re-request all variables because we've invalidated them
2930
     * @param threadId
2931
     * @param stackFrameId
2932
     */
2933
    private sendInvalidatedEvent(threadId?: number, stackFrameId?: number) {
2934
        //if the client supports this request, send it
2935
        if (this.initRequestArgs.supportsInvalidatedEvent) {
2936
            this.sendEvent(new InvalidatedEvent(['variables'], threadId, stackFrameId));
2937
        }
2938
    }
2939

2940
    /**
2941
     * If `stopOnEntry` is enabled, register the entry breakpoint.
2942
     */
2943
    public async handleEntryBreakpoint() {
2944
        if (!this.enableDebugProtocol) {
2945
            this.entryBreakpointWasHandled = true;
2946
            if (this.launchConfiguration.stopOnEntry || this.launchConfiguration.deepLinkUrl) {
2947
                await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingDir);
2948
            }
2949
        }
2950
    }
2951

2952
    /**
2953
     * Converts a debugger line number to a client line number.
2954
     *
2955
     * @param debuggerLine - The line number from the debugger as zero based.
2956
     * @param defaultDebuggerLine - An optional default line number, as zero based, to use if `debuggerLine` is not provided.
2957
     * @returns The corresponding client line number.
2958
     */
2959
    private toClientLine(debuggerLine: number, defaultDebuggerLine?: number) {
2960
        return this.convertDebuggerLineToClient(debuggerLine ?? defaultDebuggerLine);
2961
    }
2962

2963
    /**
2964
     * Converts a debugger column number to a client column number.
2965
     *
2966
     * @param debuggerLine - The column number from the debugger as zero based.
2967
     * @param defaultDebuggerLine - An optional default column number, as zero based, to use if `debuggerLine` is not provided.
2968
     * @returns The corresponding client column number.
2969
     */
2970
    private toClientColumn(debuggerLine: number, defaultDebuggerLine?: number) {
2971
        return this.convertDebuggerColumnToClient(debuggerLine ?? defaultDebuggerLine);
2972
    }
2973

2974
    /**
2975
     * Converts a client line number to a debugger line number.
2976
     *
2977
     * @param clientLine - The line number from the client.
2978
     * @param defaultDebuggerLine - An optional default line number, as zero based, to use if `clientLine` is not provided.
2979
     * @returns The corresponding debugger line number as zero based.
2980
     */
2981
    private toDebuggerLine(clientLine: number, defaultDebuggerLine?: number) {
2982
        if (typeof clientLine === 'number') {
2983
            return this.convertClientLineToDebugger(clientLine);
2984
        }
2985
        return defaultDebuggerLine;
2986
    }
2987

2988
    /**
2989
     * Converts a client column number to a debugger column number.
2990
     *
2991
     * @param clientLine - The column number from the client.
2992
     * @param defaultDebuggerLine - An optional default column number, as zero based, to use if `clientLine` is not provided.
2993
     * @returns The corresponding debugger column number as zero based.
2994
     */
2995
    private toDebuggerColumn(clientLine: number, defaultDebuggerLine?: number) {
2996
        if (typeof clientLine === 'number') {
2997
            return this.convertClientColumnToDebugger(clientLine);
2998
        }
2999
        return defaultDebuggerLine;
3000
    }
3001

3002
    private shutdownPromise: Promise<void> | undefined = undefined;
3003

3004
    /**
3005
     * Called when the debugger is terminated. Feel free to call this as frequently as you want; we'll only run the shutdown process the first time, and return
3006
     * the same promise on subsequent calls
3007
     */
3008
    public async shutdown(errorMessage?: string, modal = false): Promise<void> {
3009
        if (this.shutdownPromise === undefined) {
3010
            this.logger.log('[shutdown] Beginning shutdown sequence', errorMessage);
3011
            this.shutdownPromise = this._shutdown(errorMessage, modal);
3012
        } else {
3013
            this.logger.log('[shutdown] Tried to call `.shutdown()` again. Returning the same promise');
3014
        }
3015
        return this.shutdownPromise;
3016
    }
3017

3018
    private async _shutdown(errorMessage?: string, modal = false): Promise<void> {
3019
        // Ensure any active launch progress bar is dismissed before showing error messages or the terminated event.
3020
        this.sendLaunchProgress('end', 'Complete');
3021

3022
        //send the message FIRST before anything else. This improves the chances that the message will be displayed to the user
3023
        try {
3024
            if (errorMessage) {
3025
                this.logger.error(errorMessage);
3026
                this.showPopupMessage(errorMessage, 'error', modal).catch((error) => {
3027
                    this.logger.error('Error showing popup message', { error });
3028
                });
3029
            }
3030
        } catch (e) {
3031
            this.logger.error(e);
3032
        }
3033
        // stop perfetto tracing if it's running
3034
        try {
3035
            await this.perfettoManager.stopTracing();
3036
        } catch (e) {
3037
            this.logger.error('Error stopping perfetto tracing', e);
3038
        }
3039

3040
        try {
3041
            await this.perfettoManager?.dispose?.();
3042
        } catch (e) {
3043
            this.logger.error('Error disposing perfetto manager', e);
3044
        }
3045

3046
        //close the debugger connection
3047
        try {
3048
            this.logger.log('Destroy rokuAdapter');
3049
            await this.rokuAdapter?.destroy?.();
3050
            //press the home button to return to the home screen
3051
            try {
3052
                this.logger.log('Press home button');
3053
                await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
3054
            } catch (e) {
3055
                this.logger.error(e);
3056
            }
3057
        } catch (e) {
3058
            this.logger.error(e);
3059
        }
3060

3061
        try {
3062
            this.projectManager?.dispose?.();
3063
        } catch (e) {
3064
            this.logger.error(e);
3065
        }
3066

3067
        try {
3068
            this.componentLibraryServer?.stop();
3069
        } catch (e) {
3070
            this.logger.error(e);
3071
        }
3072

3073
        try {
3074
            await this.rendezvousTracker?.destroy?.();
3075
        } catch (e) {
3076
            this.logger.error(e);
3077
        }
3078

3079
        try {
3080
            await this.sourceMapManager?.destroy?.();
3081
        } catch (e) {
3082
            this.logger.error(e);
3083
        }
3084

3085
        try {
3086
            //if configured, delete the staging directory
3087
            if (!this.launchConfiguration.retainStagingFolder) {
3088
                const stagingDirs = this.projectManager?.getStagingDirs() ?? [];
3089
                this.logger.info('deleting staging folders', stagingDirs);
3090
                for (let stagingDir of stagingDirs) {
3091
                    try {
3092
                        fsExtra.removeSync(stagingDir);
3093
                    } catch (e) {
3094
                        this.logger.error(e);
3095
                        util.log(`Error removing staging directory '${stagingDir}': ${JSON.stringify(e)}`);
3096
                    }
3097
                }
3098
            }
3099
        } catch (e) {
3100
            this.logger.error(e);
3101
        }
3102

3103
        try {
3104
            this.logger.log('Send terminated event');
3105
            this.sendEvent(new TerminatedEvent());
3106

3107
            //shut down the process
3108
            this.logger.log('super.shutdown()');
3109
            super.shutdown();
3110
            this.logger.log('shutdown complete');
3111
        } catch (e) {
3112
            this.logger.error(e);
3113
        }
3114

3115
        try {
3116
            this.teardownProcessErrorHandlers();
3117
        } catch (e) {
3118
            this.logger.error(e);
3119
        }
3120
    }
3121
}
3122

3123
export interface AugmentedVariable extends DebugProtocol.Variable {
3124
    childVariables?: AugmentedVariable[];
3125
    // eslint-disable-next-line camelcase
3126
    request_seq?: number;
3127
    frameId?: number;
3128
    /**
3129
     * only used for lazy variables
3130
     */
3131
    isResolved?: boolean;
3132
    /**
3133
     * used to indicate that this variable is a scope variable
3134
     * and may require special handling
3135
     */
3136
    isScope?: boolean;
3137
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc