roku-debug 0.22.6 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,7 @@ const ecpRegistryUtils_1 = require("./ecpRegistryUtils");
32
32
  const RokuECP_1 = require("../RokuECP");
33
33
  const Exceptions_1 = require("../Exceptions");
34
34
  const diagnosticSource = 'roku-debug';
35
- class BrightScriptDebugSession extends debugadapter_1.DebugSession {
35
+ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
36
36
  constructor() {
37
37
  super();
38
38
  this.logger = logging_1.logger.createLogger(`[session]`);
@@ -41,6 +41,8 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
41
41
  * A sequence used to help identify log statements for requests
42
42
  */
43
43
  this.idCounter = 1;
44
+ this.processErrorHandlersRegistered = false;
45
+ this.isCrashed = false;
44
46
  //set imports as class properties so they can be spied upon during testing
45
47
  this.rokuDeploy = roku_deploy_1.rokuDeploy;
46
48
  this.componentLibraryServer = new ComponentLibraryServer_1.ComponentLibraryServer();
@@ -87,6 +89,120 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
87
89
  });
88
90
  this.fileLoggingManager = new logging_1.FileLoggingManager();
89
91
  }
92
+ start(inStream, outStream) {
93
+ super.start(inStream, outStream);
94
+ // Set up DAP protocol logging as early as possible — immediately after start() so we capture
95
+ // the initialize request and all early DAP traffic before launchRequest config is available.
96
+ // The log file path is injected as ROKU_DAP_LOG_FILE by the extension's DebugAdapterDescriptorFactory,
97
+ // which resolves the path from the `brightscript.debug.debugAdapterProtocolLogging` workspace setting
98
+ // (or the equivalent launch.json property) before the debug adapter process is spawned.
99
+ const dapLogFile = process.env.ROKU_DAP_LOG_FILE;
100
+ if (dapLogFile) {
101
+ // Use LogLevel.Error (not Verbose) as the console threshold so DAP messages are written
102
+ // to the log file but are NOT forwarded to VS Code as OutputEvents, which would flood
103
+ // the debug console and break the extension's output parsing.
104
+ // Note: InternalLogger always writes ALL messages to the file stream regardless of level,
105
+ // so the log file will still contain everything.
106
+ debugadapter_1.logger.setup(debugadapter_1.Logger.LogLevel.Error, dapLogFile);
107
+ }
108
+ }
109
+ setupProcessErrorHandlers() {
110
+ if (this.processErrorHandlersRegistered) {
111
+ return;
112
+ }
113
+ this.processErrorHandlersRegistered = true;
114
+ const handleError = (type, error) => {
115
+ var _a, _b, _c, _d;
116
+ const logger = this.logger.createLogger(`${type}`);
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ const stack = error instanceof Error ? error.stack : undefined;
119
+ logger.error(message, stack);
120
+ let output;
121
+ try {
122
+ const debuggerVersion = fsExtra.readJsonSync(path.resolve(__dirname, '../../package.json')).version;
123
+ const clientName = (_b = (_a = this.initRequestArgs) === null || _a === void 0 ? void 0 : _a.clientName) !== null && _b !== void 0 ? _b : 'unknown';
124
+ const clientId = (_d = (_c = this.initRequestArgs) === null || _c === void 0 ? void 0 : _c.clientID) !== null && _d !== void 0 ? _d : 'unknown';
125
+ const issueBodyPrefix = [
126
+ `**Debugger version:** ${debuggerVersion}`,
127
+ `**Client:** ${clientName} (${clientId})`,
128
+ `**Error type:** ${type}`,
129
+ `**Message:** ${message}`,
130
+ '',
131
+ `**Steps to reproduce:**`,
132
+ `<!-- Please describe what you were doing when this crash occurred -->`,
133
+ '',
134
+ '**Stack trace:**',
135
+ '```',
136
+ ''
137
+ ].join('\n');
138
+ const issueBodySuffix = '\n```';
139
+ const issueTitle = encodeURIComponent(`[crash] ${type}: ${message}`);
140
+ const baseUrl = 'https://github.com/RokuCommunity/roku-debug/issues/new';
141
+ const maxUrlLength = 2000;
142
+ const urlOverhead = `${baseUrl}?title=${issueTitle}&body=`.length;
143
+ const bodyBudget = maxUrlLength - urlOverhead;
144
+ const encodedPrefix = encodeURIComponent(issueBodyPrefix);
145
+ const encodedSuffix = encodeURIComponent(issueBodySuffix);
146
+ const stackBudget = bodyBudget - encodedPrefix.length - encodedSuffix.length;
147
+ let truncatedStack;
148
+ if (!stack) {
149
+ truncatedStack = '(no stack trace)';
150
+ }
151
+ else if (encodeURIComponent(stack).length <= stackBudget) {
152
+ truncatedStack = stack;
153
+ }
154
+ else {
155
+ truncatedStack = decodeURIComponent(encodeURIComponent(stack).slice(0, stackBudget)) + '\n...(truncated)';
156
+ }
157
+ const issueUrl = `${baseUrl}?title=${issueTitle}&body=${encodedPrefix}${encodeURIComponent(truncatedStack)}${encodedSuffix}`;
158
+ output = [
159
+ '',
160
+ '================================================================',
161
+ ' BRIGHTSCRIPT DEBUGGER INTERNAL ERROR',
162
+ ' This is a crash in the debug adapter, not in your application.',
163
+ '================================================================',
164
+ ` Error type: ${type}`,
165
+ ` Message: ${message}`,
166
+ ` Debugger version: ${debuggerVersion}`,
167
+ ` Client: ${clientName} (${clientId})`,
168
+ '',
169
+ ' Stack trace:',
170
+ ...(stack !== null && stack !== void 0 ? stack : '(no stack trace)').split('\n').map(l => ` ${l}`),
171
+ '',
172
+ ' Please report this at:',
173
+ ` ${issueUrl}`,
174
+ '================================================================',
175
+ ''
176
+ ].join('\n');
177
+ }
178
+ catch (e) {
179
+ output = JSON.stringify({
180
+ name: e.name,
181
+ message: e.message,
182
+ stack: e.stack
183
+ });
184
+ }
185
+ void this.sendLogOutput(output).catch(() => { });
186
+ this.isCrashed = true;
187
+ this.sendEvent(new Events_1.ProcessCrashEvent({ type, message, stack }));
188
+ setTimeout(() => void this.shutdown(), 5000);
189
+ };
190
+ this._uncaughtExceptionHandler = (error) => handleError('uncaughtException', error);
191
+ this._unhandledRejectionHandler = (reason) => handleError('unhandledRejection', reason);
192
+ process.on('uncaughtException', this._uncaughtExceptionHandler);
193
+ process.on('unhandledRejection', this._unhandledRejectionHandler);
194
+ }
195
+ teardownProcessErrorHandlers() {
196
+ if (this._uncaughtExceptionHandler) {
197
+ process.removeListener('uncaughtException', this._uncaughtExceptionHandler);
198
+ this._uncaughtExceptionHandler = undefined;
199
+ }
200
+ if (this._unhandledRejectionHandler) {
201
+ process.removeListener('unhandledRejection', this._unhandledRejectionHandler);
202
+ this._unhandledRejectionHandler = undefined;
203
+ }
204
+ this.processErrorHandlersRegistered = false;
205
+ }
90
206
  onDeviceBreakpointsChanged(eventName, data) {
91
207
  this.logger.info('Sending verified device breakpoints to client', data);
92
208
  //send all verified breakpoints to the client
@@ -304,6 +420,7 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
304
420
  //send the response right away so the UI immediately shows the debugger toolbar
305
421
  this.sendResponse(response);
306
422
  this.launchConfiguration = this.normalizeLaunchConfig(config);
423
+ this.setupProcessErrorHandlers();
307
424
  //prebake some threads for our ProjectManager to use later on (1 for the main project, and 1 for every complib)
308
425
  BscProjectWorkerPool_1.bscProjectWorkerPool.preload(1 + ((_c = (_b = (_a = this.launchConfiguration) === null || _a === void 0 ? void 0 : _a.componentLibraries) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0));
309
426
  //set the logLevel provided by the launch config
@@ -343,6 +460,7 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
343
460
  this.logger.log('[launchRequest] Packaging and deploying to roku');
344
461
  try {
345
462
  const packageEnd = this.logger.timeStart('log', 'Packaging');
463
+ this.sendLaunchProgress('start', 'Packaging');
346
464
  //build the main project and all component libraries at the same time
347
465
  await Promise.all([
348
466
  this.prepareMainProject(),
@@ -433,12 +551,14 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
433
551
  });
434
552
  //profiling supports connecting to the socket BEFORE a channel is published, so go ahead and connect now
435
553
  await this.tryProfilingConnectOnStart();
554
+ this.sendLaunchProgress('update', 'Uploading to Roku');
436
555
  await this.publish();
437
556
  //hack for certain roku devices that lock up when this event is emitted (no idea why!).
438
557
  if (this.launchConfiguration.emitChannelPublishedEvent) {
439
558
  this.sendEvent(new Events_1.ChannelPublishedEvent(this.launchConfiguration));
440
559
  }
441
560
  //tell the adapter adapter that the channel has been launched.
561
+ this.sendLaunchProgress('update', 'Waiting on application');
442
562
  await this.rokuAdapter.activate();
443
563
  if (this.rokuAdapter.isDestroyed) {
444
564
  throw new Error('Debug session encountered an error');
@@ -461,7 +581,7 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
461
581
  throw error;
462
582
  }
463
583
  //at this point, the project has been deployed. If we need to use a deep link, launch it now.
464
- if (this.launchConfiguration.deepLinkUrl) {
584
+ if (this.launchConfiguration.deepLinkUrl && !this.enableDebugProtocol) {
465
585
  //wait until the first entry breakpoint has been hit
466
586
  await this.firstRunDeferred.promise;
467
587
  //if we are at a breakpoint, continue
@@ -484,6 +604,9 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
484
604
  const message = (e instanceof Exceptions_1.SocketConnectionInUseError) ? e.message : ((_f = e === null || e === void 0 ? void 0 : e.stack) !== null && _f !== void 0 ? _f : e);
485
605
  await this.shutdown(message, true);
486
606
  }
607
+ else {
608
+ this.sendLaunchProgress('end', 'Aborted (compile error)');
609
+ }
487
610
  }
488
611
  logEnd();
489
612
  }
@@ -491,7 +614,7 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
491
614
  * Activate all required functionality for profiling
492
615
  */
493
616
  async initializeProfiling() {
494
- var _a, _b, _c;
617
+ var _a, _b, _c, _d, _e;
495
618
  // Initialize PerfettoManager
496
619
  this.perfettoManager = new PerfettoManager_1.PerfettoManager({
497
620
  host: this.launchConfiguration.host,
@@ -521,14 +644,24 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
521
644
  error: event.error
522
645
  }));
523
646
  });
524
- //enable profiling on the device right away if tracing is enabled in the launch config (this doesn't actually start the trace, it just enables the ability to start a trace from the UI or automatically based on the config)
647
+ //tracing is explicitly enabled. Turn it on
525
648
  if (((_c = (_b = this.launchConfiguration.profiling) === null || _b === void 0 ? void 0 : _b.tracing) === null || _c === void 0 ? void 0 : _c.enable) && this.supportsPerfettoTracing) {
649
+ this.logger.info('Enabling perfetto tracing because it is supported by the device and enabled in the launch configuration');
526
650
  try {
527
651
  await this.perfettoManager.enableTracing();
528
652
  }
529
653
  catch (e) {
530
654
  this.logger.error('Failed to enable perfetto tracing', e);
531
655
  }
656
+ //tracing is expicitly DISabled. turn it off
657
+ }
658
+ else if (((_e = (_d = this.launchConfiguration.profiling) === null || _d === void 0 ? void 0 : _d.tracing) === null || _e === void 0 ? void 0 : _e.enable) === false && this.supportsPerfettoTracing) {
659
+ this.logger.info('Disabling perfetto tracing because it is disabled in the launch configuration');
660
+ //TODO implement a way to disable perfetto tracing on the device
661
+ //profiling.tracing.enabled is set to `undefined`, which means we should do nothing
662
+ }
663
+ else {
664
+ this.logger.info('Skipping perfetto initalization because `profiling.tracing.enable` is not defined in the launch configuration');
532
665
  }
533
666
  }
534
667
  /**
@@ -611,6 +744,7 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
611
744
  //find the first compile error (i.e. first DiagnosticSeverity.Error) if there is one
612
745
  this.compileError = diagnostics.find(x => x.severity === brighterscript_1.DiagnosticSeverity.Error);
613
746
  if (this.compileError) {
747
+ this.sendLaunchProgress('end', 'Aborted (compile error)');
614
748
  this.sendEvent(new debugadapter_1.StoppedEvent(Events_1.StoppedEventReason.exception, this.COMPILE_ERROR_THREAD_ID, `CompileError: ${this.compileError.message}`));
615
749
  }
616
750
  this.sendEvent(new Events_1.DiagnosticsEvent(diagnostics));
@@ -693,6 +827,9 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
693
827
  * @param logOutput
694
828
  */
695
829
  sendLogOutput(logOutput) {
830
+ if (this.isCrashed) {
831
+ return Promise.resolve();
832
+ }
696
833
  this.fileLoggingManager.writeRokuDeviceLog(logOutput);
697
834
  this.pendingSendLogPromise = this.pendingSendLogPromise.then(async () => {
698
835
  logOutput = await this.convertBacktracePaths(logOutput);
@@ -2035,10 +2172,14 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
2035
2172
  */
2036
2173
  async connectRokuAdapter() {
2037
2174
  this.rokuAdapter.on('start', () => {
2175
+ this.sendLaunchProgress('end', 'Complete');
2038
2176
  if (!this.firstRunDeferred.isCompleted) {
2039
2177
  this.firstRunDeferred.resolve();
2040
2178
  }
2041
2179
  });
2180
+ this.rokuAdapter.on('launch-status', (message) => {
2181
+ this.sendLaunchProgress('update', message);
2182
+ });
2042
2183
  //when the debugger suspends (pauses for debugger input)
2043
2184
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
2044
2185
  this.rokuAdapter.on('suspend', async () => {
@@ -2295,6 +2436,35 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
2295
2436
  //erase all cached variables
2296
2437
  this.variables = {};
2297
2438
  }
2439
+ /**
2440
+ * Sends a launch progress event to the client if the client supports progress reporting.
2441
+ * - `'start'`: begins a new progress bar with the given message. Assigns a new progressId.
2442
+ * - `'update'`: updates the message on the active progress bar.
2443
+ * - `'end'`: dismisses the active progress bar with an optional final message.
2444
+ */
2445
+ sendLaunchProgress(type, message) {
2446
+ var _a;
2447
+ if (!((_a = this.initRequestArgs) === null || _a === void 0 ? void 0 : _a.supportsProgressReporting)) {
2448
+ return;
2449
+ }
2450
+ if (type === 'start') {
2451
+ this.launchProgressId = `rokudebug-launch-${this.idCounter++}`;
2452
+ this.sendEvent(new debugadapter_1.ProgressStartEvent(this.launchProgressId, 'Launching', `${message}...`));
2453
+ }
2454
+ else if (this.launchProgressId) {
2455
+ if (type === 'update') {
2456
+ this.sendEvent(new debugadapter_1.ProgressUpdateEvent(this.launchProgressId, `${message}...`));
2457
+ }
2458
+ else {
2459
+ const lastId = this.launchProgressId;
2460
+ this.sendEvent(new debugadapter_1.ProgressUpdateEvent(lastId, message));
2461
+ setTimeout(() => {
2462
+ this.sendEvent(new debugadapter_1.ProgressEndEvent(lastId, message));
2463
+ }, 1000); // add a slight delay before ending the progress to improve UX
2464
+ this.launchProgressId = undefined;
2465
+ }
2466
+ }
2467
+ }
2298
2468
  /**
2299
2469
  * Tells the client to re-request all variables because we've invalidated them
2300
2470
  * @param threadId
@@ -2379,6 +2549,8 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
2379
2549
  }
2380
2550
  async _shutdown(errorMessage, modal = false) {
2381
2551
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
2552
+ // Ensure any active launch progress bar is dismissed before showing error messages or the terminated event.
2553
+ this.sendLaunchProgress('end', 'Complete');
2382
2554
  //send the message FIRST before anything else. This improves the chances that the message will be displayed to the user
2383
2555
  try {
2384
2556
  if (errorMessage) {
@@ -2474,6 +2646,12 @@ class BrightScriptDebugSession extends debugadapter_1.DebugSession {
2474
2646
  catch (e) {
2475
2647
  this.logger.error(e);
2476
2648
  }
2649
+ try {
2650
+ this.teardownProcessErrorHandlers();
2651
+ }
2652
+ catch (e) {
2653
+ this.logger.error(e);
2654
+ }
2477
2655
  }
2478
2656
  }
2479
2657
  exports.BrightScriptDebugSession = BrightScriptDebugSession;