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

rokucommunity / roku-debug / #2017

pending completion
#2017

push

web-flow
Merge 4961675eb into f30a7deaa

1876 of 2742 branches covered (68.42%)

Branch coverage included in aggregate %.

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

3422 of 4578 relevant lines covered (74.75%)

27.43 hits per line

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

44.55
/src/debugSession/BrightScriptDebugSession.ts
1
import * as fsExtra from 'fs-extra';
1✔
2
import { orderBy } from 'natural-orderby';
1✔
3
import * as path from 'path';
1✔
4
import * as request from 'request';
1✔
5
import { rokuDeploy, CompileError } from 'roku-deploy';
1✔
6
import type { RokuDeploy, RokuDeployOptions } from 'roku-deploy';
7
import {
1✔
8
    BreakpointEvent,
9
    DebugSession as BaseDebugSession,
10
    ErrorDestination,
11
    Handles,
12
    InitializedEvent,
13
    InvalidatedEvent,
14
    OutputEvent,
15
    Scope,
16
    Source,
17
    StackFrame,
18
    StoppedEvent,
19
    TerminatedEvent,
20
    Thread,
21
    Variable
22
} from 'vscode-debugadapter';
23
import type { SceneGraphCommandResponse } from '../SceneGraphDebugCommandController';
24
import { SceneGraphDebugCommandController } from '../SceneGraphDebugCommandController';
1✔
25
import type { DebugProtocol } from 'vscode-debugprotocol';
26
import { defer, util } from '../util';
1✔
27
import { fileUtils, standardizePath as s } from '../FileUtils';
1✔
28
import { ComponentLibraryServer } from '../ComponentLibraryServer';
1✔
29
import { ProjectManager, Project, ComponentLibraryProject } from '../managers/ProjectManager';
1✔
30
import type { EvaluateContainer } from '../adapters/DebugProtocolAdapter';
31
import { isDebugProtocolAdapter, DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter';
1✔
32
import { TelnetAdapter } from '../adapters/TelnetAdapter';
1✔
33
import type { BSDebugDiagnostic } from '../CompileErrorProcessor';
34
import {
1✔
35
    LaunchStartEvent,
36
    LogOutputEvent,
37
    RendezvousEvent,
38
    DiagnosticsEvent,
39
    StoppedEventReason,
40
    ChanperfEvent,
41
    DebugServerLogOutputEvent,
42
    ChannelPublishedEvent,
43
    PopupMessageEvent
44
} from './Events';
45
import type { LaunchConfiguration, ComponentLibraryConfiguration } from '../LaunchConfiguration';
46
import { FileManager } from '../managers/FileManager';
1✔
47
import { SourceMapManager } from '../managers/SourceMapManager';
1✔
48
import { LocationManager } from '../managers/LocationManager';
1✔
49
import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager';
50
import { BreakpointManager } from '../managers/BreakpointManager';
1✔
51
import type { LogMessage } from '../logging';
52
import { logger, debugServerLogOutputEventTransport } from '../logging';
1✔
53
import { VariableType } from '../debugProtocol/events/responses/VariablesResponse';
1✔
54

55
export class BrightScriptDebugSession extends BaseDebugSession {
1✔
56
    public constructor() {
57
        super();
28✔
58

59
        // this debugger uses one-based lines and columns
60
        this.setDebuggerLinesStartAt1(true);
28✔
61
        this.setDebuggerColumnsStartAt1(true);
28✔
62

63
        //give util a reference to this session to assist in logging across the entire module
64
        util._debugSession = this;
28✔
65
        this.fileManager = new FileManager();
28✔
66
        this.sourceMapManager = new SourceMapManager();
28✔
67
        this.locationManager = new LocationManager(this.sourceMapManager);
28✔
68
        this.breakpointManager = new BreakpointManager(this.sourceMapManager, this.locationManager);
28✔
69
        //send newly-verified breakpoints to vscode
70
        this.breakpointManager.on('breakpoints-verified', (data) => this.onDeviceVerifiedBreakpoints(data));
28✔
71
        this.projectManager = new ProjectManager(this.breakpointManager, this.locationManager);
28✔
72
    }
73

74
    private onDeviceVerifiedBreakpoints(data: { breakpoints: AugmentedSourceBreakpoint[] }) {
75
        this.logger.info('Sending verified device breakpoints to client', data);
2✔
76
        //send all verified breakpoints to the client
77
        for (const breakpoint of data.breakpoints) {
2✔
78
            const event: DebugProtocol.Breakpoint = {
2✔
79
                line: breakpoint.line,
80
                column: breakpoint.column,
81
                verified: true,
82
                id: breakpoint.id,
83
                source: {
84
                    path: breakpoint.srcPath
85
                }
86
            };
87
            this.sendEvent(new BreakpointEvent('changed', event));
2✔
88
        }
89
    }
90

91
    public logger = logger.createLogger(`[${BrightScriptDebugSession.name}]`);
28✔
92

93
    /**
94
     * A sequence used to help identify log statements for requests
95
     */
96
    private idCounter = 1;
28✔
97

98
    public fileManager: FileManager;
99

100
    public projectManager: ProjectManager;
101

102
    public breakpointManager: BreakpointManager;
103

104
    public locationManager: LocationManager;
105

106
    public sourceMapManager: SourceMapManager;
107

108
    //set imports as class properties so they can be spied upon during testing
109
    public rokuDeploy = rokuDeploy as unknown as RokuDeploy;
28✔
110

111
    private componentLibraryServer = new ComponentLibraryServer();
28✔
112

113
    private rokuAdapterDeferred = defer<DebugProtocolAdapter | TelnetAdapter>();
28✔
114
    /**
115
     * A promise that is resolved whenever the app has started running for the first time
116
     */
117
    private firstRunDeferred = defer<void>();
28✔
118

119
    private evaluateRefIdLookup: Record<string, number> = {};
28✔
120
    private evaluateRefIdCounter = 1;
28✔
121

122
    private variables: Record<number, AugmentedVariable> = {};
28✔
123

124
    private variableHandles = new Handles<string>();
28✔
125

126
    private rokuAdapter: DebugProtocolAdapter | TelnetAdapter;
127

128
    public tempVarPrefix = '__rokudebug__';
28✔
129

130
    private get enableDebugProtocol() {
131
        return this.launchConfiguration.enableDebugProtocol;
59✔
132
    }
133

134
    private getRokuAdapter() {
135
        return this.rokuAdapterDeferred.promise;
×
136
    }
137

138
    private launchConfiguration: LaunchConfiguration;
139
    private initRequestArgs: DebugProtocol.InitializeRequestArguments;
140

141
    /**
142
     * The 'initialize' request is the first request called by the frontend
143
     * to interrogate the features the debug adapter provides.
144
     */
145
    public initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
146
        this.initRequestArgs = args;
1✔
147
        this.logger.log('initializeRequest');
1✔
148
        // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time,
149
        // we request them early by sending an 'initializeRequest' to the frontend.
150
        // The frontend will end the configuration sequence by calling 'configurationDone' request.
151
        this.sendEvent(new InitializedEvent());
1✔
152

153
        response.body = response.body || {};
1✔
154

155
        // This debug adapter implements the configurationDoneRequest.
156
        response.body.supportsConfigurationDoneRequest = true;
1✔
157

158
        // 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.
159
        response.body.supportsRestartRequest = true;
1✔
160

161
        // make VS Code to use 'evaluate' when hovering over source
162
        response.body.supportsEvaluateForHovers = true;
1✔
163

164
        // make VS Code to show a 'step back' button
165
        response.body.supportsStepBack = false;
1✔
166

167
        // This debug adapter supports conditional breakpoints
168
        response.body.supportsConditionalBreakpoints = true;
1✔
169

170
        // This debug adapter supports breakpoints that break execution after a specified number of hits
171
        response.body.supportsHitConditionalBreakpoints = true;
1✔
172

173
        // This debug adapter supports log points by interpreting the 'logMessage' attribute of the SourceBreakpoint
174
        response.body.supportsLogPoints = true;
1✔
175

176
        this.sendResponse(response);
1✔
177

178
        //register the debug output log transport writer
179
        debugServerLogOutputEventTransport.setWriter((message: LogMessage) => {
1✔
180
            this.sendEvent(
238✔
181
                new DebugServerLogOutputEvent(
182
                    message.logger.formatMessage(message, false)
183
                )
184
            );
185
        });
186
        this.logger.log('initializeRequest finished');
1✔
187
    }
188

189
    private showPopupMessage(message: string, severity: 'error' | 'warn' | 'info') {
190
        this.sendEvent(new PopupMessageEvent(message, severity));
×
191
    }
192

193
    public async launchRequest(response: DebugProtocol.LaunchResponse, config: LaunchConfiguration) {
194
        this.logger.log('[launchRequest] begin');
×
195
        this.launchConfiguration = config;
×
196

197
        //set the logLevel provided by the launch config
198
        if (this.launchConfiguration.logLevel) {
×
199
            logger.logLevel = this.launchConfiguration.logLevel;
×
200
        }
201

202
        //do a DNS lookup for the host to fix issues with roku rejecting ECP
203
        try {
×
204
            this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host);
×
205
        } catch (e) {
206
            const errorMessage = `Could not resolve ip address for "${this.launchConfiguration.host}"`;
×
207
            this.showPopupMessage(errorMessage, 'error');
×
208
            throw e;
×
209
        }
210

211
        this.projectManager.launchConfiguration = this.launchConfiguration;
×
212
        this.breakpointManager.launchConfiguration = this.launchConfiguration;
×
213

214
        this.sendEvent(new LaunchStartEvent(this.launchConfiguration));
×
215

216
        let error: Error;
217
        this.logger.log('[launchRequest] Packaging and deploying to roku');
×
218
        try {
×
219
            const start = Date.now();
×
220
            //build the main project and all component libraries at the same time
221
            await Promise.all([
×
222
                this.prepareMainProject(),
223
                this.prepareAndHostComponentLibraries(this.launchConfiguration.componentLibraries, this.launchConfiguration.componentLibrariesPort)
224
            ]);
225
            this.logger.log(`Packaging projects took: ${(util.formatTime(Date.now() - start))}`);
×
226

227
            util.log(`Connecting to Roku via ${this.enableDebugProtocol ? 'the BrightScript debug protocol' : 'telnet'} at ${this.launchConfiguration.host}`);
×
228

229
            this.createRokuAdapter(this.launchConfiguration.host);
×
230
            if (!this.enableDebugProtocol) {
×
231
                //connect to the roku debug via telnet
232
                if (!this.rokuAdapter.connected) {
×
233
                    await this.connectRokuAdapter();
×
234
                }
235
            } else {
236
                await (this.rokuAdapter as DebugProtocolAdapter).watchCompileOutput();
×
237
            }
238

239
            await this.runAutomaticSceneGraphCommands(this.launchConfiguration.autoRunSgDebugCommands);
×
240

241
            //press the home button to ensure we're at the home screen
242
            await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
×
243

244
            //pass the debug functions used to locate the client files and lines thought the adapter to the RendezvousTracker
245
            this.rokuAdapter.registerSourceLocator(async (debuggerPath: string, lineNumber: number) => {
×
246
                return this.projectManager.getSourceLocation(debuggerPath, lineNumber);
×
247
            });
248

249
            //pass the log level down thought the adapter to the RendezvousTracker and ChanperfTracker
250
            this.rokuAdapter.setConsoleOutput(this.launchConfiguration.consoleOutput);
×
251

252
            //pass along the console output
253
            if (this.launchConfiguration.consoleOutput === 'full') {
×
254
                this.rokuAdapter.on('console-output', (data) => {
×
255
                    this.sendLogOutput(data);
×
256
                });
257
            } else {
258
                this.rokuAdapter.on('unhandled-console-output', (data) => {
×
259
                    this.sendLogOutput(data);
×
260
                });
261
            }
262

263
            // Send chanperf events to the extension
264
            this.rokuAdapter.on('chanperf', (output) => {
×
265
                this.sendEvent(new ChanperfEvent(output));
×
266
            });
267

268
            // Send rendezvous events to the extension
269
            this.rokuAdapter.on('rendezvous', (output) => {
×
270
                this.sendEvent(new RendezvousEvent(output));
×
271
            });
272

273
            //listen for a closed connection (shut down when received)
274
            this.rokuAdapter.on('close', (reason = '') => {
×
275
                if (reason === 'compileErrors') {
×
276
                    error = new Error('compileErrors');
×
277
                } else {
278
                    error = new Error('Unable to connect to Roku. Is another device already connected?');
×
279
                }
280
            });
281

282
            // handle any compile errors
283
            this.rokuAdapter.on('diagnostics', (diagnostics: BSDebugDiagnostic[]) => {
×
284
                void this.handleDiagnostics(diagnostics);
×
285
            });
286

287
            // close disconnect if required when the app is exited
288
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
289
            this.rokuAdapter.on('app-exit', async () => {
×
290
                if (this.launchConfiguration.stopDebuggerOnAppExit || !this.rokuAdapter.supportsMultipleRuns) {
×
291
                    let message = `App exit event detected${this.rokuAdapter.supportsMultipleRuns ? ' and launchConfiguration.stopDebuggerOnAppExit is true' : ''}`;
×
292
                    message += ' - shutting down debug session';
×
293

294
                    this.logger.log('on app-exit', message);
×
295
                    this.sendEvent(new LogOutputEvent(message));
×
296
                    if (this.rokuAdapter) {
×
297
                        void this.rokuAdapter.destroy();
×
298
                    }
299
                    //return to the home screen
300
                    await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
×
301
                    this.shutdown();
×
302
                    this.sendEvent(new TerminatedEvent());
×
303
                } else {
304
                    const message = 'App exit detected; but launchConfiguration.stopDebuggerOnAppExit is set to false, so keeping debug session running.';
×
305
                    this.logger.log('[launchRequest]', message);
×
306
                    this.sendEvent(new LogOutputEvent(message));
×
307
                }
308
            });
309

310
            //ignore the compile error failure from within the publish
311
            (this.launchConfiguration as any).failOnCompileError = false;
×
312
            // Set the remote debug flag on the args to be passed to roku deploy so the socket debugger can be started if needed.
313
            (this.launchConfiguration as any).remoteDebug = this.enableDebugProtocol;
×
314

315
            await this.connectAndPublish();
×
316

317
            this.sendEvent(new ChannelPublishedEvent(
×
318
                this.launchConfiguration
319
            ));
320

321
            //tell the adapter adapter that the channel has been launched.
322
            await this.rokuAdapter.activate();
×
323

324
            if (!error) {
×
325
                if (this.rokuAdapter.connected) {
×
326
                    this.logger.info('Host connection was established before the main public process was completed');
×
327
                    this.logger.log(`deployed to Roku@${this.launchConfiguration.host}`);
×
328
                    this.sendResponse(response);
×
329
                } else {
330
                    this.logger.info('Main public process was completed but we are still waiting for a connection to the host');
×
331
                    this.rokuAdapter.on('connected', (status) => {
×
332
                        if (status) {
×
333
                            this.logger.log(`deployed to Roku@${this.launchConfiguration.host}`);
×
334
                            this.sendResponse(response);
×
335
                        }
336
                    });
337
                }
338
            } else {
339
                throw error;
×
340
            }
341
        } catch (e) {
342
            //if the message is anything other than compile errors, we want to display the error
343
            if (!(e instanceof CompileError)) {
×
344
                util.log('Encountered an issue during the publish process');
×
345
                util.log((e as Error).message);
×
346
                this.sendErrorResponse(response, -1, (e as Error).message);
×
347
            }
348

349
            //send any compile errors to the client
350
            await this.rokuAdapter.sendErrors();
×
351
            this.logger.error('Error. Shutting down.', e);
×
352
            this.shutdown();
×
353
            return;
×
354
        }
355

356
        //at this point, the project has been deployed. If we need to use a deep link, launch it now.
357
        if (this.launchConfiguration.deepLinkUrl) {
×
358
            //wait until the first entry breakpoint has been hit
359
            await this.firstRunDeferred.promise;
×
360
            //if we are at a breakpoint, continue
361
            await this.rokuAdapter.continue();
×
362
            //kill the app on the roku
363
            await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
×
364
            //convert a hostname to an ip address
365
            const deepLinkUrl = await util.resolveUrl(this.launchConfiguration.deepLinkUrl);
×
366
            //send the deep link http request
367
            await new Promise((resolve, reject) => {
×
368
                request.post(deepLinkUrl, (err, response) => {
×
369
                    return err ? reject(err) : resolve(response);
×
370
                });
371
            });
372
        }
373
    }
374

375
    /**
376
     * Anytime a roku adapter emits diagnostics, this methid is called to handle it.
377
     */
378
    private async handleDiagnostics(diagnostics: BSDebugDiagnostic[]) {
379
        // Roku device and sourcemap work with 1-based line numbers, VSCode expects 0-based lines.
380
        for (let diagnostic of diagnostics) {
1✔
381
            let sourceLocation = await this.projectManager.getSourceLocation(diagnostic.path, diagnostic.range.start.line + 1);
1✔
382
            if (sourceLocation) {
1!
383
                diagnostic.path = sourceLocation.filePath;
1✔
384
                diagnostic.range.start.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based
1✔
385
                diagnostic.range.end.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based
1✔
386
            } else {
387
                // TODO: may need to add a custom event if the source location could not be found by the ProjectManager
388
                diagnostic.path = fileUtils.removeLeadingSlash(util.removeFileScheme(diagnostic.path));
×
389
            }
390
        }
391

392
        this.sendEvent(new DiagnosticsEvent(diagnostics));
1✔
393
        //stop the roku adapter and exit the channel
394
        void this.rokuAdapter.destroy();
1✔
395
        void this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
1✔
396
    }
397

398
    private async connectAndPublish() {
399
        let connectPromise: Promise<any>;
400
        //connect to the roku debug via sockets
401
        if (this.enableDebugProtocol) {
×
402
            connectPromise = this.connectRokuAdapter().catch(e => this.logger.error(e));
×
403
        }
404

405
        let packageIsPublished = false;
×
406
        //publish the package to the target Roku
407
        const publishPromise = this.rokuDeploy.publish({
×
408
            ...this.launchConfiguration,
409
            failOnCompileError: true
410
        } as any as RokuDeployOptions).then(() => {
411
            packageIsPublished = true;
×
412
        });
413

414
        await publishPromise;
×
415

416
        //the channel has been deployed. Wait for the adapter to finish connecting.
417
        //if it hasn't connected after 5 seconds, it probably will never connect.
418
        await Promise.race([
×
419
            connectPromise,
420
            util.sleep(10000)
421
        ]);
422
        this.logger.log('Finished racing promises');
×
423
        //if the adapter is still not connected, then it will probably never connect. Abort.
424
        if (packageIsPublished && !this.rokuAdapter.connected) {
×
425
            //kill the session cuz it won't ever come back
426
            await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
×
427
            const message = 'Debug session cancelled: failed to connect to debug protocol control port.';
×
428
            this.showPopupMessage(message, 'error');
×
429
            this.logger.error(message);
×
430
            this.shutdown();
×
431
            this.sendEvent(new TerminatedEvent());
×
432
        }
433
    }
434

435
    /**
436
     * Send log output to the "client" (i.e. vscode)
437
     * @param logOutput
438
     */
439
    private sendLogOutput(logOutput: string) {
440
        const lines = logOutput.split(/\r?\n/g);
×
441
        for (let line of lines) {
×
442
            line += '\n';
×
443
            this.sendEvent(new OutputEvent(line, 'stdout'));
×
444
            this.sendEvent(new LogOutputEvent(line));
×
445
        }
446
    }
447

448
    private async runAutomaticSceneGraphCommands(commands: string[]) {
449
        if (commands) {
×
450
            let connection = new SceneGraphDebugCommandController(this.launchConfiguration.host);
×
451

452
            try {
×
453
                await connection.connect();
×
454
                for (let command of this.launchConfiguration.autoRunSgDebugCommands) {
×
455
                    let response: SceneGraphCommandResponse;
456
                    switch (command) {
×
457
                        case 'chanperf':
458
                            util.log('Enabling Chanperf Tracking');
×
459
                            response = await connection.chanperf({ interval: 1 });
×
460
                            if (!response.error) {
×
461
                                util.log(response.result.rawResponse);
×
462
                            }
463
                            break;
×
464

465
                        case 'fpsdisplay':
466
                            util.log('Enabling FPS Display');
×
467
                            response = await connection.fpsDisplay('on');
×
468
                            if (!response.error) {
×
469
                                util.log(response.result.data as string);
×
470
                            }
471
                            break;
×
472

473
                        case 'logrendezvous':
474
                            util.log('Enabling Rendezvous Logging:');
×
475
                            response = await connection.logrendezvous('on');
×
476
                            if (!response.error) {
×
477
                                util.log(response.result.rawResponse);
×
478
                            }
479
                            break;
×
480

481
                        default:
482
                            util.log(`Running custom SceneGraph debug command on port 8080 '${command}':`);
×
483
                            response = await connection.exec(command);
×
484
                            if (!response.error) {
×
485
                                util.log(response.result.rawResponse);
×
486
                            }
487
                            break;
×
488
                    }
489
                }
490
                await connection.end();
×
491
            } catch (error) {
492
                util.log(`Error connecting to port 8080: ${error.message}`);
×
493
            }
494
        }
495
    }
496

497
    /**
498
     * Stage, insert breakpoints, and package the main project
499
     */
500
    public async prepareMainProject() {
501
        //add the main project
502
        this.projectManager.mainProject = new Project({
1✔
503
            rootDir: this.launchConfiguration.rootDir,
504
            files: this.launchConfiguration.files,
505
            outDir: this.launchConfiguration.outDir,
506
            sourceDirs: this.launchConfiguration.sourceDirs,
507
            bsConst: this.launchConfiguration.bsConst,
508
            injectRaleTrackerTask: this.launchConfiguration.injectRaleTrackerTask,
509
            raleTrackerTaskFileLocation: this.launchConfiguration.raleTrackerTaskFileLocation,
510
            injectRdbOnDeviceComponent: this.launchConfiguration.injectRdbOnDeviceComponent,
511
            rdbFilesBasePath: this.launchConfiguration.rdbFilesBasePath,
512
            stagingFolderPath: this.launchConfiguration.stagingFolderPath
513
        });
514

515
        util.log('Moving selected files to staging area');
1✔
516
        await this.projectManager.mainProject.stage();
1✔
517

518
        //add the entry breakpoint if stopOnEntry is true
519
        await this.handleEntryBreakpoint();
1✔
520

521
        //add breakpoint lines to source files and then publish
522
        util.log('Adding stop statements for active breakpoints');
1✔
523

524
        //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol)
525
        if (!this.enableDebugProtocol) {
1!
526

527
            await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject);
1✔
528
        }
529

530
        //create zip package from staging folder
531
        util.log('Creating zip archive from project sources');
1✔
532
        await this.projectManager.mainProject.zipPackage({ retainStagingFolder: true });
1✔
533
    }
534

535
    /**
536
     * Accepts custom events and requests from the extension
537
     * @param command name of the command to execute
538
     */
539
    protected customRequest(command: string) {
540
        if (command === 'rendezvous.clearHistory') {
×
541
            this.rokuAdapter.clearRendezvousHistory();
×
542
        }
543

544
        if (command === 'chanperf.clearHistory') {
×
545
            this.rokuAdapter.clearChanperfHistory();
×
546
        }
547
    }
548

549
    /**
550
     * Stores the path to the staging folder for each component library
551
     */
552
    protected async prepareAndHostComponentLibraries(componentLibraries: ComponentLibraryConfiguration[], port: number) {
553
        if (componentLibraries && componentLibraries.length > 0) {
×
554
            let componentLibrariesOutDir = s`${this.launchConfiguration.outDir}/component-libraries`;
×
555
            //make sure this folder exists (and is empty)
556
            await fsExtra.ensureDir(componentLibrariesOutDir);
×
557
            await fsExtra.emptyDir(componentLibrariesOutDir);
×
558

559
            //create a ComponentLibraryProject for each component library
560
            for (let libraryIndex = 0; libraryIndex < componentLibraries.length; libraryIndex++) {
×
561
                let componentLibrary = componentLibraries[libraryIndex];
×
562

563
                this.projectManager.componentLibraryProjects.push(
×
564
                    new ComponentLibraryProject({
565
                        rootDir: componentLibrary.rootDir,
566
                        files: componentLibrary.files,
567
                        outDir: componentLibrariesOutDir,
568
                        outFile: componentLibrary.outFile,
569
                        sourceDirs: componentLibrary.sourceDirs,
570
                        bsConst: componentLibrary.bsConst,
571
                        injectRaleTrackerTask: componentLibrary.injectRaleTrackerTask,
572
                        raleTrackerTaskFileLocation: componentLibrary.raleTrackerTaskFileLocation,
573
                        libraryIndex: libraryIndex
574
                    })
575
                );
576
            }
577

578
            //prepare all of the libraries in parallel
579
            let compLibPromises = this.projectManager.componentLibraryProjects.map(async (compLibProject) => {
×
580

581
                await compLibProject.stage();
×
582

583
                // Add breakpoint lines to the staging files and before publishing
584
                util.log('Adding stop statements for active breakpoints in Component Libraries');
×
585

586
                //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol)
587
                if (!this.enableDebugProtocol) {
×
588
                    await this.breakpointManager.writeBreakpointsForProject(compLibProject);
×
589
                }
590

591
                await compLibProject.postfixFiles();
×
592

593
                await compLibProject.zipPackage({ retainStagingFolder: true });
×
594
            });
595

596
            let hostingPromise: Promise<any>;
597
            if (compLibPromises) {
×
598
                // prepare static file hosting
599
                hostingPromise = this.componentLibraryServer.startStaticFileHosting(componentLibrariesOutDir, port, (message: string) => {
×
600
                    util.log(message);
×
601
                });
602
            }
603

604
            //wait for all component libaries to finish building, and the file hosting to start up
605
            await Promise.all([
×
606
                ...compLibPromises,
607
                hostingPromise
608
            ]);
609
        }
610
    }
611

612
    protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments) {
613
        this.logger.log('sourceRequest');
×
614
        let old = this.sendResponse;
×
615
        this.sendResponse = function sendResponse(...args) {
×
616
            old.apply(this, args);
×
617
            this.sendResponse = old;
×
618
        };
619
        super.sourceRequest(response, args);
×
620
    }
621

622
    protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments) {
623
        this.logger.log('configurationDoneRequest');
×
624
    }
625

626
    /**
627
     * Called every time a breakpoint is created, modified, or deleted, for each file. This receives the entire list of breakpoints every time.
628
     */
629
    public async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) {
630
        let sanitizedBreakpoints = this.breakpointManager.replaceBreakpoints(args.source.path, args.breakpoints);
5✔
631
        //sort the breakpoints
632
        let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]);
5✔
633

634
        response.body = {
5✔
635
            breakpoints: sortedAndFilteredBreakpoints
636
        };
637
        this.sendResponse(response);
5✔
638

639
        await this.rokuAdapter?.syncBreakpoints();
5!
640
    }
641

642
    protected exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, args: DebugProtocol.ExceptionInfoArguments) {
643
        this.logger.log('exceptionInfoRequest');
×
644
    }
645

646
    protected async threadsRequest(response: DebugProtocol.ThreadsResponse) {
647
        this.logger.log('threadsRequest');
×
648
        //wait for the roku adapter to load
649
        await this.getRokuAdapter();
×
650

651
        let threads = [];
×
652

653
        //only send the threads request if we are at the debugger prompt
654
        if (this.rokuAdapter.isAtDebuggerPrompt) {
×
655
            let rokuThreads = await this.rokuAdapter.getThreads();
×
656

657
            for (let thread of rokuThreads) {
×
658
                threads.push(
×
659
                    new Thread(thread.threadId, `Thread ${thread.threadId}`)
660
                );
661
            }
662
        } else {
663
            this.logger.log('Skipped getting threads because the RokuAdapter is not accepting input at this time.');
×
664
        }
665

666
        response.body = {
×
667
            threads: threads
668
        };
669

670
        this.sendResponse(response);
×
671
    }
672

673
    protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
674
        try {
1✔
675
            this.logger.log('stackTraceRequest');
1✔
676
            let frames = [];
1✔
677

678
            if (this.rokuAdapter.isAtDebuggerPrompt) {
1!
679
                let stackTrace = await this.rokuAdapter.getStackTrace(args.threadId);
1✔
680

681
                for (let debugFrame of stackTrace) {
1✔
682
                    let sourceLocation = await this.projectManager.getSourceLocation(debugFrame.filePath, debugFrame.lineNumber);
3✔
683

684
                    //the stacktrace returns function identifiers in all lower case. Try to get the actual case
685
                    //load the contents of the file and get the correct casing for the function identifier
686
                    try {
3✔
687
                        let functionName = this.fileManager.getCorrectFunctionNameCase(sourceLocation?.filePath, debugFrame.functionIdentifier);
3✔
688
                        if (functionName) {
3!
689

690
                            //search for original function name if this is an anonymous function.
691
                            //anonymous function names are prefixed with $ in the stack trace (i.e. $anon_1 or $functionname_40002)
692
                            if (functionName.startsWith('$')) {
3!
693
                                functionName = this.fileManager.getFunctionNameAtPosition(
×
694
                                    sourceLocation.filePath,
695
                                    sourceLocation.lineNumber - 1,
696
                                    functionName
697
                                );
698
                            }
699
                            debugFrame.functionIdentifier = functionName;
3✔
700
                        }
701
                    } catch (error) {
702
                        this.logger.error('Error correcting function identifier case', { error, sourceLocation, debugFrame });
×
703
                    }
704
                    const filePath = sourceLocation?.filePath ?? debugFrame.filePath;
3✔
705

706
                    const frame: DebugProtocol.StackFrame = new StackFrame(
3✔
707
                        debugFrame.frameId,
708
                        `${debugFrame.functionIdentifier}`,
709
                        new Source(path.basename(filePath), filePath),
710
                        sourceLocation?.lineNumber ?? debugFrame.lineNumber,
18✔
711
                        1
712
                    );
713
                    if (!sourceLocation) {
3✔
714
                        frame.presentationHint = 'subtle';
1✔
715
                    }
716
                    frames.push(frame);
3✔
717
                }
718
            } else {
719
                this.logger.log('Skipped calculating stacktrace because the RokuAdapter is not accepting input at this time');
×
720
            }
721
            response.body = {
1✔
722
                stackFrames: frames,
723
                totalFrames: frames.length
724
            };
725
            this.sendResponse(response);
1✔
726
        } catch (error) {
727
            this.logger.error('Error getting stacktrace', { error, args });
×
728
        }
729
    }
730

731
    protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
732
        const logger = this.logger.createLogger(`scopesRequest ${this.idCounter}`);
1✔
733
        logger.info('begin', { args });
1✔
734
        try {
1✔
735
            const scopes = new Array<Scope>();
1✔
736

737
            if (isDebugProtocolAdapter(this.rokuAdapter)) {
1!
738
                let refId = this.getEvaluateRefId('', args.frameId);
×
739
                let v: AugmentedVariable;
740
                //if we already looked this item up, return it
741
                if (this.variables[refId]) {
×
742
                    v = this.variables[refId];
×
743
                } else {
744
                    let result = await this.rokuAdapter.getLocalVariables(args.frameId);
×
745
                    if (!result) {
×
746
                        throw new Error(`Could not get scopes`);
×
747
                    }
748
                    v = this.getVariableFromResult(result, args.frameId);
×
749
                    //TODO - testing something, remove later
750
                    // eslint-disable-next-line camelcase
751
                    v.request_seq = response.request_seq;
×
752
                    v.frameId = args.frameId;
×
753
                }
754

755
                let scope = new Scope('Local', refId, false);
×
756
                scopes.push(scope);
×
757
            } else {
758
                // NOTE: Legacy telnet support
759
                scopes.push(new Scope('Local', this.variableHandles.create('local'), false));
1✔
760
            }
761

762
            response.body = {
1✔
763
                scopes: scopes
764
            };
765
            logger.debug('send response', { response });
1✔
766
            this.sendResponse(response);
1✔
767
            logger.info('end');
1✔
768
        } catch (error) {
769
            logger.error('Error getting scopes', { error, args });
×
770
        }
771
    }
772

773
    protected async continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
774
        this.logger.log('continueRequest');
×
775
        await this.rokuAdapter.continue();
×
776
        this.sendResponse(response);
×
777
    }
778

779
    protected async pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
780
        this.logger.log('pauseRequest');
×
781
        await this.rokuAdapter.pause();
×
782
        this.sendResponse(response);
×
783
    }
784

785
    protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments) {
786
        this.logger.log('reverseContinueRequest');
×
787
        this.sendResponse(response);
×
788
    }
789

790
    /**
791
     * Clicked the "Step Over" button
792
     * @param response
793
     * @param args
794
     */
795
    protected async nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
796
        this.logger.log('[nextRequest] begin');
×
797
        try {
×
798
            await this.rokuAdapter.stepOver(args.threadId);
×
799
            this.logger.info('[nextRequest] end');
×
800
        } catch (error) {
801
            this.logger.error(`[nextRequest] Error running '${BrightScriptDebugSession.prototype.nextRequest.name}()'`, error);
×
802
        }
803
        this.sendResponse(response);
×
804
    }
805

806
    protected async stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
807
        this.logger.log('[stepInRequest]');
×
808
        await this.rokuAdapter.stepInto(args.threadId);
×
809
        this.sendResponse(response);
×
810
        this.logger.info('[stepInRequest] end');
×
811
    }
812

813
    protected async stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
814
        this.logger.log('[stepOutRequest] begin');
×
815
        await this.rokuAdapter.stepOut(args.threadId);
×
816
        this.sendResponse(response);
×
817
        this.logger.info('[stepOutRequest] end');
×
818
    }
819

820
    protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments) {
821
        this.logger.log('[stepBackRequest] begin');
×
822
        this.sendResponse(response);
×
823
        this.logger.info('[stepBackRequest] end');
×
824
    }
825

826
    public async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments) {
827
        const logger = this.logger.createLogger('[variablesRequest]');
4✔
828
        try {
4✔
829
            logger.log('begin', { args });
4✔
830

831
            let childVariables: AugmentedVariable[] = [];
4✔
832
            //wait for any `evaluate` commands to finish so we have a higher likely hood of being at a debugger prompt
833
            await this.evaluateRequestPromise;
4✔
834
            if (!this.rokuAdapter.isAtDebuggerPrompt) {
4!
835
                logger.log('Skipped getting variables because the RokuAdapter is not accepting input at this time');
×
836
                response.success = false;
×
837
                response.message = 'Debug session is not paused';
×
838
                return this.sendResponse(response);
×
839
            }
840
            const reference = this.variableHandles.get(args.variablesReference);
4✔
841
            if (reference) {
4✔
842
                logger.log('reference', reference);
2✔
843
                // NOTE: Legacy telnet support for local vars
844
                if (this.launchConfiguration.enableVariablesPanel) {
2!
845
                    const vars = await (this.rokuAdapter as TelnetAdapter).getScopeVariables();
2✔
846

847
                    for (const varName of vars) {
2✔
848
                        let result = await this.rokuAdapter.getVariable(varName, -1);
6✔
849
                        let tempVar = this.getVariableFromResult(result, -1);
6✔
850
                        childVariables.push(tempVar);
6✔
851
                    }
852
                } else {
853
                    childVariables.push(new Variable('variables disabled by launch.json setting', 'enableVariablesPanel: false'));
×
854
                }
855
            } else {
856
                //find the variable with this reference
857
                let v = this.variables[args.variablesReference];
2✔
858
                if (!v) {
2!
859
                    response.success = false;
×
860
                    response.message = `Variable reference has expired`;
×
861
                    return this.sendResponse(response);
×
862
                }
863
                logger.log('variable', v);
2✔
864
                //query for child vars if we haven't done it yet.
865
                if (v.childVariables.length === 0) {
2!
866
                    let result = await this.rokuAdapter.getVariable(v.evaluateName, v.frameId);
×
867
                    let tempVar = this.getVariableFromResult(result, v.frameId);
×
868
                    tempVar.frameId = v.frameId;
×
869
                    v.childVariables = tempVar.childVariables;
×
870
                }
871
                childVariables = v.childVariables;
2✔
872
            }
873

874
            //if the variable is an array, send only the requested range
875
            if (Array.isArray(childVariables) && args.filter === 'indexed') {
4!
876
                //only send the variable range requested by the debugger
877
                childVariables = childVariables.slice(args.start, args.start + args.count);
×
878
            }
879

880
            let filteredChildVariables = this.launchConfiguration.showHiddenVariables !== true ? childVariables.filter(
4✔
881
                (child: AugmentedVariable) => !child.name.startsWith(this.tempVarPrefix)) : childVariables;
6✔
882

883
            response.body = {
4✔
884
                variables: filteredChildVariables
885
            };
886
        } catch (error) {
887
            logger.error('Error during variablesRequest', error, { args });
×
888
            response.success = false;
×
889
            response.message = error?.message ?? 'Error during variablesRequest';
×
890
        } finally {
891
            logger.info('end', { response });
4✔
892
        }
893
        this.sendResponse(response);
4✔
894
    }
895

896
    private evaluateRequestPromise = Promise.resolve();
28✔
897
    private evaluateVarIndexByFrameId = new Map<number, number>();
28✔
898

899
    private getNextVarIndex(frameId: number): number {
900
        if (!this.evaluateVarIndexByFrameId.has(frameId)) {
6✔
901
            this.evaluateVarIndexByFrameId.set(frameId, 0);
5✔
902
        }
903
        let value = this.evaluateVarIndexByFrameId.get(frameId);
6✔
904
        this.evaluateVarIndexByFrameId.set(frameId, value + 1);
6✔
905
        return value;
6✔
906
    }
907

908
    public async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
909
        let deferred = defer<void>();
15✔
910
        if (args.context === 'repl' && !this.enableDebugProtocol && args.expression.trim().startsWith('>')) {
15!
911
            this.clearState();
×
912
            const expression = args.expression.replace(/^\s*>\s*/, '');
×
913
            this.logger.log('Sending raw telnet command...I sure hope you know what you\'re doing', { expression });
×
914
            (this.rokuAdapter as TelnetAdapter).requestPipeline.client.write(`${expression}\r\n`);
×
915
            this.sendResponse(response);
×
916
            return deferred.promise;
×
917
        }
918

919
        try {
15✔
920
            this.evaluateRequestPromise = this.evaluateRequestPromise.then(() => {
15✔
921
                return deferred.promise;
15✔
922
            });
923

924
            //fix vscode hover bug that excludes closing quotemark sometimes.
925
            if (args.context === 'hover') {
15✔
926
                args.expression = util.ensureClosingQuote(args.expression);
4✔
927
            }
928

929
            if (!this.rokuAdapter.isAtDebuggerPrompt) {
15✔
930
                let message = 'Skipped evaluate request because RokuAdapter is not accepting requests at this time';
1✔
931
                if (args.context === 'repl') {
1!
932
                    this.sendEvent(new OutputEvent(message, 'stderr'));
1✔
933
                    response.body = {
1✔
934
                        result: 'invalid',
935
                        variablesReference: 0
936
                    };
937
                } else {
938
                    throw new Error(message);
×
939
                }
940

941
                //is at debugger prompt
942
            } else {
943
                let variablePath = util.getVariablePath(args.expression);
14✔
944
                if (!variablePath && util.isAssignableExpression(args.expression)) {
14✔
945
                    let varIndex = this.getNextVarIndex(args.frameId);
6✔
946
                    let arrayVarName = this.tempVarPrefix + 'eval';
6✔
947
                    if (varIndex === 0) {
6✔
948
                        const response = await this.rokuAdapter.evaluate(`${arrayVarName} = []`, args.frameId);
5✔
949
                        console.log(response);
5✔
950
                    }
951
                    let statement = `${arrayVarName}[${varIndex}] = ${args.expression}`;
6✔
952
                    args.expression = `${arrayVarName}[${varIndex}]`;
6✔
953
                    let commandResults = await this.rokuAdapter.evaluate(statement, args.frameId);
6✔
954
                    variablePath = [arrayVarName, varIndex.toString()];
6✔
955
                }
956

957
                //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`
958
                if (variablePath) {
14✔
959
                    let refId = this.getEvaluateRefId(args.expression, args.frameId);
11✔
960
                    let v: AugmentedVariable;
961
                    //if we already looked this item up, return it
962
                    if (this.variables[refId]) {
11✔
963
                        v = this.variables[refId];
1✔
964
                    } else {
965
                        let result = await this.rokuAdapter.getVariable(args.expression, args.frameId);
10✔
966
                        if (!result) {
10!
967
                            throw new Error('Error: unable to evaluate expression');
×
968
                        }
969

970
                        v = this.getVariableFromResult(result, args.frameId);
10✔
971
                        //TODO - testing something, remove later
972
                        // eslint-disable-next-line camelcase
973
                        v.request_seq = response.request_seq;
10✔
974
                        v.frameId = args.frameId;
10✔
975
                    }
976
                    response.body = {
11✔
977
                        result: v.value,
978
                        type: v.type,
979
                        variablesReference: v.variablesReference,
980
                        namedVariables: v.namedVariables || 0,
21✔
981
                        indexedVariables: v.indexedVariables || 0
21✔
982
                    };
983

984
                    //run an `evaluate` call
985
                } else {
986
                    let commandResults = await this.rokuAdapter.evaluate(args.expression, args.frameId);
3✔
987

988
                    commandResults.message = util.trimDebugPrompt(commandResults.message);
3✔
989
                    if (args.context !== 'watch') {
3!
990
                        //clear variable cache since this action could have side-effects
991
                        this.clearState();
3✔
992
                        this.sendInvalidatedEvent(null, args.frameId);
3✔
993
                    }
994
                    //if the adapter captured output (probably only telnet), print it to the vscode debug console
995
                    if (typeof commandResults.message === 'string') {
3✔
996
                        this.sendEvent(new OutputEvent(commandResults.message, commandResults.type === 'error' ? 'stderr' : 'stdio'));
1!
997
                    }
998

999
                    if (this.enableDebugProtocol || (typeof commandResults.message !== 'string')) {
3✔
1000
                        response.body = {
2✔
1001
                            result: 'invalid',
1002
                            variablesReference: 0
1003
                        };
1004
                    } else {
1005
                        response.body = {
1✔
1006
                            result: commandResults.message === '\r\n' ? 'invalid' : commandResults.message,
1!
1007
                            variablesReference: 0
1008
                        };
1009
                    }
1010
                }
1011
            }
1012
        } catch (error) {
1013
            this.logger.error('Error during variables request', error);
×
1014
            response.success = false;
×
1015
            response.message = error?.message ?? error;
×
1016
        }
1017
        try {
15✔
1018
            this.sendResponse(response);
15✔
1019
        } catch { }
1020
        deferred.resolve();
15✔
1021
    }
1022

1023
    /**
1024
     * Called when the host stops debugging
1025
     * @param response
1026
     * @param args
1027
     */
1028
    protected async disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request) {
1029
        if (this.rokuAdapter) {
×
1030
            await this.rokuAdapter.destroy();
×
1031
        }
1032
        //return to the home screen
1033
        if (!this.enableDebugProtocol) {
×
1034
            await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort);
×
1035
        }
1036
        this.componentLibraryServer.stop();
×
1037
        this.sendResponse(response);
×
1038
    }
1039

1040
    private createRokuAdapter(host: string) {
1041
        if (this.enableDebugProtocol) {
×
1042
            this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration, this.projectManager, this.breakpointManager);
×
1043
        } else {
1044
            this.rokuAdapter = new TelnetAdapter(this.launchConfiguration);
×
1045
        }
1046
    }
1047

1048
    protected async restartRequest(response: DebugProtocol.RestartResponse, args: DebugProtocol.RestartArguments, request?: DebugProtocol.Request) {
1049
        this.logger.log('[restartRequest] begin');
×
1050
        if (this.rokuAdapter) {
×
1051
            if (!this.enableDebugProtocol) {
×
1052
                this.rokuAdapter.removeAllListeners();
×
1053
            }
1054
            await this.rokuAdapter.destroy();
×
1055
            this.rokuAdapterDeferred = defer();
×
1056
        }
1057
        await this.launchRequest(response, args.arguments as LaunchConfiguration);
×
1058
    }
1059

1060
    /**
1061
     * Used to track whether the entry breakpoint has already been handled
1062
     */
1063
    private entryBreakpointWasHandled = false;
28✔
1064

1065
    /**
1066
     * Registers the main events for the RokuAdapter
1067
     */
1068
    private async connectRokuAdapter() {
1069
        this.rokuAdapter.on('start', () => {
×
1070
            if (!this.firstRunDeferred.isCompleted) {
×
1071
                this.firstRunDeferred.resolve();
×
1072
            }
1073
        });
1074

1075
        //when the debugger suspends (pauses for debugger input)
1076
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
1077
        this.rokuAdapter.on('suspend', async () => {
×
1078
            await this.onSuspend();
×
1079
        });
1080

1081
        //anytime the adapter encounters an exception on the roku,
1082
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
1083
        this.rokuAdapter.on('runtime-error', async (exception) => {
×
1084
            let rokuAdapter = await this.getRokuAdapter();
×
1085
            let threads = await rokuAdapter.getThreads();
×
1086
            let threadId = threads[0]?.threadId;
×
1087
            this.sendEvent(new StoppedEvent(StoppedEventReason.exception, threadId, exception.message));
×
1088
        });
1089

1090
        // If the roku says it can't continue, we are no longer able to debug, so kill the debug session
1091
        this.rokuAdapter.on('cannot-continue', () => {
×
1092
            this.sendEvent(new TerminatedEvent());
×
1093
        });
1094

1095
        //make the connection
1096
        await this.rokuAdapter.connect();
×
1097
        this.rokuAdapterDeferred.resolve(this.rokuAdapter);
×
1098
        return this.rokuAdapter;
×
1099
    }
1100

1101
    private async onSuspend() {
1102
        //clear the index for storing evalutated expressions
1103
        this.evaluateVarIndexByFrameId.clear();
1✔
1104

1105
        //sync breakpoints
1106
        await this.rokuAdapter?.syncBreakpoints();
1!
1107
        this.logger.info('received "suspend" event from adapter');
1✔
1108

1109
        const threads = await this.rokuAdapter.getThreads();
1✔
1110
        const activeThread = threads.find(x => x.isSelected);
1✔
1111

1112
        //TODO remove this once Roku fixes their threads off-by-one line number issues
1113
        //look up the correct line numbers for each thread from the StackTrace
1114
        await Promise.all(
1✔
1115
            threads.map(async (thread) => {
1116
                const stackTrace = await this.rokuAdapter.getStackTrace(thread.threadId);
×
1117
                const stackTraceLineNumber = stackTrace[0]?.lineNumber;
×
1118
                if (stackTraceLineNumber !== thread.lineNumber) {
×
1119
                    this.logger.warn(`Thread ${thread.threadId} reported incorrect line (${thread.lineNumber}). Using line from stack trace instead (${stackTraceLineNumber})`, thread, stackTrace);
×
1120
                    thread.lineNumber = stackTraceLineNumber;
×
1121
                }
1122
            })
1123
        );
1124

1125
        //if !stopOnEntry, and we haven't encountered a suspend yet, THIS is the entry breakpoint. auto-continue
1126
        if (!this.entryBreakpointWasHandled && !this.launchConfiguration.stopOnEntry) {
1!
1127
            this.entryBreakpointWasHandled = true;
1✔
1128
            //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
1129
            if (activeThread && !await this.breakpointManager.lineHasBreakpoint(this.projectManager.getAllProjects(), activeThread.filePath, activeThread.lineNumber - 1)) {
1!
1130
                this.logger.info('Encountered entry breakpoint and `stopOnEntry` is disabled. Continuing...');
×
1131
                return this.rokuAdapter.continue();
×
1132
            }
1133
        }
1134

1135
        this.clearState();
1✔
1136
        const event: StoppedEvent = new StoppedEvent(
1✔
1137
            StoppedEventReason.breakpoint,
1138
            //Not sure why, but sometimes there is no active thread. Just pick thread 0 to prevent the app from totally crashing
1139
            activeThread?.threadId ?? 0,
6!
1140
            '' //exception text
1141
        );
1142
        // Socket debugger will always stop all threads and supports multi thread inspection.
1143
        (event.body as any).allThreadsStopped = this.enableDebugProtocol;
1✔
1144
        this.sendEvent(event);
1✔
1145

1146
    }
1147

1148
    private getVariableFromResult(result: EvaluateContainer, frameId: number) {
1149
        let v: AugmentedVariable;
1150

1151
        if (result) {
20!
1152
            if (this.enableDebugProtocol) {
20!
1153
                let refId = this.getEvaluateRefId(result.evaluateName, frameId);
×
1154
                if (result.keyType) {
×
1155
                    // check to see if this is an dictionary or a list
1156
                    if (result.keyType === 'Integer') {
×
1157
                        // list type
1158
                        v = new Variable(result.name, result.type, refId, result.elementCount, 0);
×
1159
                        this.variables[refId] = v;
×
1160
                    } else if (result.keyType === 'String') {
×
1161
                        // dictionary type
1162
                        v = new Variable(result.name, result.type, refId, 0, result.elementCount);
×
1163
                    }
1164
                } else {
1165
                    let value: string;
1166
                    if (result.type === VariableType.Invalid) {
×
1167
                        value = result.value ?? 'Invalid';
×
1168
                    } else if (result.type === VariableType.Uninitialized) {
×
1169
                        value = 'Uninitialized';
×
1170
                    } else {
1171
                        value = `${result.value}`;
×
1172
                    }
1173
                    v = new Variable(result.name, value);
×
1174
                }
1175
                this.variables[refId] = v;
×
1176
            } else {
1177
                if (result.highLevelType === 'primative' || result.highLevelType === 'uninitialized') {
20✔
1178
                    v = new Variable(result.name, `${result.value}`);
18✔
1179
                } else if (result.highLevelType === 'array') {
2✔
1180
                    let refId = this.getEvaluateRefId(result.evaluateName, frameId);
1✔
1181
                    v = new Variable(result.name, result.type, refId, result.children?.length ?? 0, 0);
1!
1182
                    this.variables[refId] = v;
1✔
1183
                } else if (result.highLevelType === 'object') {
1!
1184
                    let refId = this.getEvaluateRefId(result.evaluateName, frameId);
1✔
1185
                    v = new Variable(result.name, result.type, refId, 0, result.children?.length ?? 0);
1!
1186
                    this.variables[refId] = v;
1✔
1187
                } else if (result.highLevelType === 'function') {
×
1188
                    v = new Variable(result.name, result.value);
×
1189
                } else {
1190
                    //all other cases, but mostly for HighLevelType.unknown
1191
                    v = new Variable(result.name, result.value);
×
1192
                }
1193
            }
1194

1195
            v.type = result.type;
20✔
1196
            v.evaluateName = result.evaluateName;
20✔
1197
            v.frameId = frameId;
20✔
1198
            v.type = result.type;
20✔
1199
            v.presentationHint = result.presentationHint ? { kind: result.presentationHint } : undefined;
20!
1200

1201
            if (result.children) {
20✔
1202
                let childVariables = [];
2✔
1203
                for (let childContainer of result.children) {
2✔
1204
                    let childVar = this.getVariableFromResult(childContainer, frameId);
4✔
1205
                    childVariables.push(childVar);
4✔
1206
                }
1207
                v.childVariables = childVariables;
2✔
1208
            }
1209
            // if the var is an array and debugProtocol is enabled, include the array size
1210
            if (this.enableDebugProtocol && v.type === VariableType.Array) {
20!
1211
                v.value = `${v.type}(${result.elementCount})` as any;
×
1212
            }
1213
        }
1214
        return v;
20✔
1215
    }
1216

1217

1218
    private getEvaluateRefId(expression: string, frameId: number) {
1219
        let evaluateRefId = `${expression}-${frameId}`;
15✔
1220
        if (!this.evaluateRefIdLookup[evaluateRefId]) {
15✔
1221
            this.evaluateRefIdLookup[evaluateRefId] = this.evaluateRefIdCounter++;
11✔
1222
        }
1223
        return this.evaluateRefIdLookup[evaluateRefId];
15✔
1224
    }
1225

1226
    private clearState() {
1227
        //erase all cached variables
1228
        this.variables = {};
4✔
1229
    }
1230

1231
    /**
1232
     * Tells the client to re-request all variables because we've invalidated them
1233
     * @param threadId
1234
     * @param stackFrameId
1235
     */
1236
    private sendInvalidatedEvent(threadId?: number, stackFrameId?: number) {
1237
        //if the client supports this request, send it
1238
        if (this.initRequestArgs.supportsInvalidatedEvent) {
3!
1239
            this.sendEvent(new InvalidatedEvent(['variables'], threadId, stackFrameId));
×
1240
        }
1241
    }
1242

1243
    /**
1244
     * If `stopOnEntry` is enabled, register the entry breakpoint.
1245
     */
1246
    public async handleEntryBreakpoint() {
1247
        if (!this.enableDebugProtocol) {
3!
1248
            this.entryBreakpointWasHandled = true;
3✔
1249
            if (this.launchConfiguration.stopOnEntry || this.launchConfiguration.deepLinkUrl) {
3✔
1250
                await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingFolderPath);
1✔
1251
            }
1252
        }
1253
    }
1254

1255
    /**
1256
     * Called when the debugger is terminated
1257
     */
1258
    public shutdown() {
1259
        //if configured, delete the staging directory
1260
        if (!this.launchConfiguration.retainStagingFolder) {
1!
1261
            let stagingFolderPaths = this.projectManager.getStagingFolderPaths();
1✔
1262
            for (let stagingFolderPath of stagingFolderPaths) {
1✔
1263
                try {
2✔
1264
                    fsExtra.removeSync(stagingFolderPath);
2✔
1265
                } catch (e) {
1266
                    util.log(`Error removing staging directory '${stagingFolderPath}': ${JSON.stringify(e)}`);
×
1267
                }
1268
            }
1269
        }
1270
        super.shutdown();
1✔
1271
    }
1272
}
1273

1274
interface AugmentedVariable extends DebugProtocol.Variable {
1275
    childVariables?: AugmentedVariable[];
1276
    // eslint-disable-next-line camelcase
1277
    request_seq?: number;
1278
    frameId?: number;
1279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc