roku-debug 0.8.5 → 0.9.1

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/dist/CompileErrorProcessor.d.ts +1 -0
  3. package/dist/CompileErrorProcessor.js +5 -3
  4. package/dist/CompileErrorProcessor.js.map +1 -1
  5. package/dist/LaunchConfiguration.d.ts +5 -0
  6. package/dist/SceneGraphDebugCommandController.d.ts +1 -0
  7. package/dist/SceneGraphDebugCommandController.js +11 -5
  8. package/dist/SceneGraphDebugCommandController.js.map +1 -1
  9. package/dist/adapters/DebugProtocolAdapter.d.ts +3 -11
  10. package/dist/adapters/DebugProtocolAdapter.js +32 -28
  11. package/dist/adapters/DebugProtocolAdapter.js.map +1 -1
  12. package/dist/adapters/TelnetAdapter.d.ts +10 -57
  13. package/dist/adapters/TelnetAdapter.js +230 -379
  14. package/dist/adapters/TelnetAdapter.js.map +1 -1
  15. package/dist/adapters/TelnetRequestPipeline.d.ts +51 -0
  16. package/dist/adapters/TelnetRequestPipeline.js +229 -0
  17. package/dist/adapters/TelnetRequestPipeline.js.map +1 -0
  18. package/dist/debugProtocol/Debugger.d.ts +1 -0
  19. package/dist/debugProtocol/Debugger.js +20 -19
  20. package/dist/debugProtocol/Debugger.js.map +1 -1
  21. package/dist/debugProtocol/responses/VariableResponse.d.ts +1 -1
  22. package/dist/debugProtocol/responses/VariableResponse.js.map +1 -1
  23. package/dist/debugSession/BrightScriptDebugSession.d.ts +6 -0
  24. package/dist/debugSession/BrightScriptDebugSession.js +91 -40
  25. package/dist/debugSession/BrightScriptDebugSession.js.map +1 -1
  26. package/dist/interfaces.d.ts +11 -0
  27. package/dist/interfaces.js +16 -0
  28. package/dist/interfaces.js.map +1 -0
  29. package/dist/logging.d.ts +11 -0
  30. package/dist/logging.js +14 -0
  31. package/dist/logging.js.map +1 -0
  32. package/dist/managers/FileManager.d.ts +1 -0
  33. package/dist/managers/FileManager.js +3 -1
  34. package/dist/managers/FileManager.js.map +1 -1
  35. package/dist/managers/ProjectManager.d.ts +2 -0
  36. package/dist/managers/ProjectManager.js +5 -2
  37. package/dist/managers/ProjectManager.js.map +1 -1
  38. package/dist/managers/SourceMapManager.d.ts +1 -0
  39. package/dist/managers/SourceMapManager.js +5 -5
  40. package/dist/managers/SourceMapManager.js.map +1 -1
  41. package/dist/util.d.ts +34 -4
  42. package/dist/util.js +94 -28
  43. package/dist/util.js.map +1 -1
  44. package/package.json +5 -2
  45. package/roku-debug-0.9.1.tgz +0 -0
  46. package/roku-debug-0.8.5.tgz +0 -0
@@ -1,15 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RequestPipeline = exports.PrimativeType = exports.KeyType = exports.HighLevelType = exports.EventName = exports.TelnetAdapter = void 0;
4
- const EventEmitter = require("events");
3
+ exports.PrimativeType = exports.KeyType = exports.EventName = exports.TelnetAdapter = void 0;
5
4
  const natural_orderby_1 = require("natural-orderby");
6
- const net = require("net");
5
+ const EventEmitter = require("eventemitter3");
6
+ const net_1 = require("net");
7
7
  const rokuDeploy = require("roku-deploy");
8
8
  const PrintedObjectParser_1 = require("../PrintedObjectParser");
9
9
  const CompileErrorProcessor_1 = require("../CompileErrorProcessor");
10
10
  const RendezvousTracker_1 = require("../RendezvousTracker");
11
11
  const ChanperfTracker_1 = require("../ChanperfTracker");
12
12
  const util_1 = require("../util");
13
+ const logging_1 = require("../logging");
14
+ const interfaces_1 = require("../interfaces");
15
+ const TelnetRequestPipeline_1 = require("./TelnetRequestPipeline");
13
16
  /**
14
17
  * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it.
15
18
  */
@@ -17,6 +20,7 @@ class TelnetAdapter {
17
20
  constructor(host, enableDebuggerAutoRecovery = false) {
18
21
  this.host = host;
19
22
  this.enableDebuggerAutoRecovery = enableDebuggerAutoRecovery;
23
+ this.logger = logging_1.logger.createLogger(`[${TelnetAdapter.name}]`);
20
24
  this.isNextBreakpointSkipped = false;
21
25
  this.cache = {};
22
26
  this.supportsMultipleRuns = true;
@@ -71,6 +75,7 @@ class TelnetAdapter {
71
75
  }, 0);
72
76
  }
73
77
  async activate() {
78
+ this.logger.log('Activate TelnetAdapter');
74
79
  this.isActivated = true;
75
80
  await this.handleStartupIfReady();
76
81
  }
@@ -78,14 +83,17 @@ class TelnetAdapter {
78
83
  await this.compileErrorProcessor.sendErrors();
79
84
  }
80
85
  async handleStartupIfReady() {
86
+ var _a;
81
87
  if (this.isActivated && this.isAppRunning) {
88
+ this.logger.log('Handling startup');
82
89
  this.emit('start');
83
90
  //if we are already sitting at a debugger prompt, we need to emit the first suspend event.
84
91
  //If not, then there are probably still messages being received, so let the normal handler
85
92
  //emit the suspend event when it's ready
86
93
  if (this.isAtDebuggerPrompt === true) {
94
+ this.logger.log(`At debug prompt, so trigger the 'suspend' event`);
87
95
  let threads = await this.getThreads();
88
- this.emit('suspend', threads[0].threadId);
96
+ this.emit('suspend', (_a = threads[0]) === null || _a === void 0 ? void 0 : _a.threadId);
89
97
  }
90
98
  }
91
99
  }
@@ -96,19 +104,22 @@ class TelnetAdapter {
96
104
  * @param maxWaitMilliseconds
97
105
  */
98
106
  settle(client, name, maxWaitMilliseconds = 400) {
107
+ const startTime = new Date();
108
+ this.logger.log('Waiting for telnet client to settle');
99
109
  return new Promise((resolve) => {
100
110
  let callCount = -1;
101
- function handler() {
111
+ const handler = () => {
102
112
  callCount++;
103
113
  let myCallCount = callCount;
104
114
  setTimeout(() => {
105
115
  //if no other calls have been made since the timeout started, then the listener has settled
106
116
  if (myCallCount === callCount) {
107
117
  client.removeListener(name, handler);
118
+ this.logger.log(`Telnet client has settled after ${new Date().getTime() - startTime.getTime()} milliseconds`);
108
119
  resolve(callCount);
109
120
  }
110
121
  }, maxWaitMilliseconds);
111
- }
122
+ };
112
123
  client.addListener(name, handler);
113
124
  //call the handler immediately so we have a timeout
114
125
  handler();
@@ -134,35 +145,38 @@ class TelnetAdapter {
134
145
  }
135
146
  }
136
147
  }
137
- return text;
138
148
  }
139
149
  /**
140
150
  * Connect to the telnet session. This should be called before the channel is launched.
141
151
  */
142
152
  async connect() {
153
+ this.logger.log('Establishing telnet connection');
143
154
  let deferred = (0, util_1.defer)();
144
155
  this.isInMicroDebugger = false;
145
156
  this.isNextBreakpointSkipped = false;
146
157
  try {
158
+ this.logger.log('Pressing home button');
147
159
  //force roku to return to home screen. This gives the roku adapter some security in knowing new messages won't be appearing during initialization
148
160
  await rokuDeploy.pressHomeButton(this.host);
149
- let client = new net.Socket();
150
- client.connect(8085, this.host, () => {
151
- console.log(`+++++++++++ CONNECTED TO DEVICE ${this.host} +++++++++++`);
152
- this.connected = true;
153
- this.emit('connected', this.connected);
154
- });
161
+ let client = new net_1.Socket();
155
162
  //listen for the close event
156
163
  client.addListener('close', (err, data) => {
157
164
  this.emit('close');
158
165
  });
159
166
  //if the connection fails, reject the connect promise
160
167
  client.addListener('error', (err) => {
161
- deferred.reject(new Error(`Error with connection to: ${this.host} \n\n ${err.message}`));
168
+ deferred.reject(new Error(`Error with connection to: ${this.host} \n\n ${err.message} `));
169
+ });
170
+ const settlePromise = this.settle(client, 'data');
171
+ client.connect(8085, this.host, () => {
172
+ this.logger.log(`Telnet connection established to ${this.host}`);
173
+ this.connected = true;
174
+ this.emit('connected', this.connected);
162
175
  });
163
- await this.settle(client, 'data');
176
+ await settlePromise;
164
177
  //hook up the pipeline to the socket
165
- this.requestPipeline = new RequestPipeline(client);
178
+ this.requestPipeline = new TelnetRequestPipeline_1.TelnetRequestPipeline(client);
179
+ this.requestPipeline.connect();
166
180
  //forward all raw console output
167
181
  this.requestPipeline.on('console-output', (output) => {
168
182
  this.processBreakpoints(output);
@@ -188,13 +202,14 @@ class TelnetAdapter {
188
202
  }
189
203
  // short circuit after the output has been sent as console output
190
204
  if (hasRuntimeError) {
191
- console.debug('hasRuntimeError!!');
205
+ this.logger.log('Detected runtime error in output', { responseText });
192
206
  this.isAtDebuggerPrompt = true;
193
207
  return;
194
208
  }
195
- this.processUnhandledLines(responseText);
209
+ this.compileErrorProcessor.processUnhandledLines(responseText);
196
210
  let match;
197
211
  if (this.isAtCannotContinue(responseText)) {
212
+ this.logger.log('is at cannot continue');
198
213
  this.isAtDebuggerPrompt = true;
199
214
  return;
200
215
  }
@@ -203,6 +218,7 @@ class TelnetAdapter {
203
218
  // eslint-disable-next-line no-cond-assign
204
219
  if (match = /\[scrpt.ctx.run.enter\]/i.exec(responseText.trim())) {
205
220
  this.isAppRunning = true;
221
+ this.logger.log('Running beacon detected', { responseText });
206
222
  void this.handleStartupIfReady();
207
223
  }
208
224
  //watch for the end of the program
@@ -211,26 +227,29 @@ class TelnetAdapter {
211
227
  this.beginAppExit();
212
228
  }
213
229
  //watch for debugger prompt output
214
- // eslint-disable-next-line no-cond-assign
215
- if (match = /Brightscript\s*Debugger>\s*$/i.exec(responseText.trim())) {
230
+ if (util_1.util.endsWithDebuggerPrompt(responseText)) {
231
+ this.logger.log('Debugger prompt detected in', { responseText });
216
232
  //if we are activated AND this is the first time seeing the debugger prompt since a continue/step action
217
233
  if (this.isNextBreakpointSkipped) {
218
- console.log('this breakpoint is flagged to be skipped');
234
+ this.logger.log('This debugger is flagged to be skipped');
219
235
  this.isInMicroDebugger = false;
220
236
  this.isNextBreakpointSkipped = false;
221
- void this.requestPipeline.executeCommand('c', false, false, false);
237
+ void this.requestPipeline.executeCommand('c', { waitForPrompt: false, insertAtFront: true });
222
238
  }
223
239
  else {
224
240
  if (this.isActivated && this.isAtDebuggerPrompt === false) {
225
241
  this.isAtDebuggerPrompt = true;
242
+ this.logger.log('Sending the "suspend" event to the client');
226
243
  this.emit('suspend');
227
244
  }
228
245
  else {
246
+ this.logger.log('Skipping "suspend" event because we are already suspended');
229
247
  this.isAtDebuggerPrompt = true;
230
248
  }
231
249
  }
232
250
  }
233
251
  else {
252
+ this.logger.debug('responseText does not end with debugger prompt. isAtDebuggerPrompt = false', { responseText });
234
253
  this.isAtDebuggerPrompt = false;
235
254
  }
236
255
  }
@@ -244,7 +263,9 @@ class TelnetAdapter {
244
263
  return deferred.promise;
245
264
  }
246
265
  beginAppExit() {
266
+ this.logger.log('Beginning app exit');
247
267
  this.compileErrorProcessor.compileErrorTimer = setTimeout(() => {
268
+ this.logger.info('emitting app-exit');
248
269
  this.isAppRunning = false;
249
270
  this.emit('app-exit');
250
271
  }, 200);
@@ -285,43 +306,46 @@ class TelnetAdapter {
285
306
  return false;
286
307
  }
287
308
  }
288
- processUnhandledLines(responseText) {
289
- this.compileErrorProcessor.processUnhandledLines(responseText);
290
- }
291
309
  /**
292
310
  * Send command to step over
293
311
  */
294
312
  stepOver() {
313
+ this.logger.log('stepOver');
295
314
  this.clearCache();
296
- return this.requestPipeline.executeCommand('over', false);
315
+ return this.requestPipeline.executeCommand('over', { waitForPrompt: false, insertAtFront: true });
297
316
  }
298
317
  stepInto() {
318
+ this.logger.log('stepInto');
299
319
  this.clearCache();
300
- return this.requestPipeline.executeCommand('step', false);
320
+ return this.requestPipeline.executeCommand('step', { waitForPrompt: false, insertAtFront: true });
301
321
  }
302
322
  stepOut() {
323
+ this.logger.log('stepOut');
303
324
  this.clearCache();
304
- return this.requestPipeline.executeCommand('out', false);
325
+ return this.requestPipeline.executeCommand('out', { waitForPrompt: false, insertAtFront: true });
305
326
  }
306
327
  /**
307
328
  * Tell the brightscript program to continue (i.e. resume program)
308
329
  */
309
330
  continue() {
331
+ this.logger.log('continue');
310
332
  this.clearCache();
311
- return this.requestPipeline.executeCommand('c', false);
333
+ return this.requestPipeline.executeCommand('c', { waitForPrompt: false, insertAtFront: true });
312
334
  }
313
335
  /**
314
336
  * Tell the brightscript program to pause (fall into debug mode)
315
337
  */
316
338
  pause() {
339
+ this.logger.log('pause');
317
340
  this.clearCache();
318
- //send the kill signal, which breaks into debugger mode
319
- return this.requestPipeline.executeCommand('\x03;', false, true);
341
+ //send the kill signal, which breaks into debugger mode. This gets written immediately, regardless of debugger prompt status.
342
+ this.requestPipeline.write('\x03;');
320
343
  }
321
344
  /**
322
345
  * Clears the state, which means that everything will be retrieved fresh next time it is requested
323
346
  */
324
347
  clearCache() {
348
+ this.logger.info('Clearing TelnetAdapter cache');
325
349
  this.cache = {};
326
350
  this.isAtDebuggerPrompt = false;
327
351
  }
@@ -330,24 +354,26 @@ class TelnetAdapter {
330
354
  * @param command the command to execute. If the command does not start with `print` the command will be prefixed with `print ` because
331
355
  */
332
356
  async evaluate(command) {
357
+ this.logger.log('evaluate ', { command });
333
358
  if (!this.isAtDebuggerPrompt) {
334
359
  throw new Error('Cannot run evaluate: debugger is not paused');
335
360
  }
336
361
  //clear the cache (we don't know what command the user entered)
337
362
  this.clearCache();
338
363
  //don't wait for the output...we don't know what command the user entered
339
- let responseText = await this.requestPipeline.executeCommand(command, true);
364
+ let responseText = await this.requestPipeline.executeCommand(command, { waitForPrompt: true });
340
365
  //we know that if we got a response, we are back at a debugger prompt
341
366
  this.isAtDebuggerPrompt = true;
342
367
  return responseText;
343
368
  }
344
369
  async getStackTrace() {
370
+ this.logger.log(TelnetAdapter.prototype.getStackTrace.name);
345
371
  if (!this.isAtDebuggerPrompt) {
346
372
  throw new Error('Cannot get stack trace: debugger is not paused');
347
373
  }
348
374
  return this.resolve('stackTrace', async () => {
349
375
  //perform a request to load the stack trace
350
- let responseText = await this.requestPipeline.executeCommand('bt', true);
376
+ let responseText = (await this.requestPipeline.executeCommand('bt', { waitForPrompt: true })).trim();
351
377
  let regexp = /#(\d+)\s+(?:function|sub)\s+([\$\w\d]+).*\s+file\/line:\s+(.*)\((\d+)\)/ig;
352
378
  let matches;
353
379
  let frames = [];
@@ -374,16 +400,6 @@ class TelnetAdapter {
374
400
  return frames;
375
401
  });
376
402
  }
377
- /**
378
- * Runs a regex to get the content between telnet commands
379
- * @param value
380
- */
381
- getExpressionDetails(value) {
382
- const match = /(.*?)\r?\nBrightscript Debugger>\s*/is.exec(value);
383
- if (match) {
384
- return match[1];
385
- }
386
- }
387
403
  /**
388
404
  * Runs a regex to check if the target is an object and get the type if it is
389
405
  * @param value
@@ -409,14 +425,15 @@ class TelnetAdapter {
409
425
  * @param scope
410
426
  */
411
427
  async getScopeVariables(scope) {
428
+ this.logger.log('getScopeVariables', { scope });
412
429
  if (!this.isAtDebuggerPrompt) {
413
430
  throw new Error('Cannot resolve variable: debugger is not paused');
414
431
  }
415
432
  return this.resolve(`Scope Variables`, async () => {
416
433
  let data;
417
434
  let vars = [];
418
- data = await this.requestPipeline.executeCommand(`var`, true);
419
- let splitData = data.split('\n');
435
+ data = await this.requestPipeline.executeCommand(`var`, { waitForPrompt: true });
436
+ let splitData = data.trim().split('\n');
420
437
  for (const line of splitData) {
421
438
  let match;
422
439
  if (!line.includes('Brightscript Debugger') && (match = this.getFirstWord(line))) {
@@ -435,122 +452,141 @@ class TelnetAdapter {
435
452
  * @param expression
436
453
  */
437
454
  async getVariable(expression) {
455
+ const logger = this.logger.createLogger('[getVariable]');
456
+ logger.info('begin', { expression });
438
457
  if (!this.isAtDebuggerPrompt) {
439
458
  throw new Error('Cannot resolve variable: debugger is not paused');
440
459
  }
441
- return this.resolve(`variable: ${expression}`, async () => {
442
- let expressionType = await this.getVariableType(expression);
443
- let lowerExpressionType = expressionType ? expressionType.toLowerCase() : null;
444
- let data;
445
- //if the expression type is a string, we need to wrap the expression in quotes BEFORE we run the print so we can accurately capture the full string value
446
- if (lowerExpressionType === 'string' || lowerExpressionType === 'rostring') {
447
- data = await this.requestPipeline.executeCommand(`print "--string-wrap--" + ${expression} + "--string-wrap--"`, true);
448
- //write a for loop to print every value from the array. This gets around the `...` after the 100th item issue in the roku print call
449
- }
450
- else if (['roarray', 'rolist', 'roxmllist', 'robytearray'].includes(lowerExpressionType)) {
451
- const command = [
452
- `for each vscodeLoopItem in ${expression} : print ` +
453
- ` "vscode_type_start:" + type(vscodeLoopItem) + ":vscode_type_stop "`,
454
- ` "vscode_is_string:"; (invalid <> GetInterface(vscodeLoopItem, "ifString"))`,
455
- ` vscodeLoopItem :` +
456
- ` end for`
457
- ].join(';');
458
- data = await this.requestPipeline.executeCommand(command, true);
459
- }
460
- else if (['roassociativearray', 'rosgnode'].includes(lowerExpressionType)) {
461
- const command = [
462
- `for each vscodeLoopKey in ${expression}.keys(): print` +
463
- ` "vscode_key_start:" + vscodeLoopKey + ":vscode_key_stop "`,
464
- ` "vscode_type_start:" + type(${expression}[vscodeLoopKey]) + ":vscode_type_stop "`,
465
- ` "vscode_is_string:"; (invalid <> GetInterface(${expression}[vscodeLoopKey], "ifString"))`,
466
- ` ${expression}[vscodeLoopKey] :` +
467
- ' end for'
468
- ].join(';');
469
- data = await this.requestPipeline.executeCommand(command, true);
470
- }
471
- else {
472
- data = await this.requestPipeline.executeCommand(`print ${expression}`, true);
473
- }
474
- let match = this.getExpressionDetails(data);
475
- if (match !== undefined) {
476
- let value = match;
477
- if (lowerExpressionType === 'string' || lowerExpressionType === 'rostring') {
478
- value = value.trim().replace(/--string-wrap--/g, '');
479
- //add an escape character in front of any existing quotes
480
- value = value.replace(/"/g, '\\"');
481
- //wrap the string value with literal quote marks
482
- value = '"' + value + '"';
483
- }
484
- let highLevelType = this.getHighLevelType(expressionType);
485
- let children;
486
- if (highLevelType === HighLevelType.array || ['roassociativearray', 'rosgnode', 'roxmllist', 'robytearray'].includes(lowerExpressionType)) {
487
- //the print statment will always have 1 trailing newline, so remove that.
488
- value = util_1.util.removeTrailingNewline(value);
489
- //the array/associative array print is a loop of every value, so handle that
490
- children = this.getForLoopPrintedChildren(expression, value);
491
- }
492
- else if (highLevelType === HighLevelType.object) {
493
- children = this.getObjectChildren(expression, value.trim());
494
- }
495
- if (['rostring', 'roint', 'rointeger', 'rolonginteger', 'rofloat', 'rodouble', 'roboolean', 'rointrinsicdouble'].includes(lowerExpressionType)) {
496
- return {
497
- name: expression,
498
- value: util_1.util.removeTrailingNewline(value),
499
- type: expressionType,
500
- highLevelType: HighLevelType.primative,
501
- evaluateName: expression,
502
- children: []
503
- };
504
- }
505
- //add a computed `[[children]]` property to allow expansion of node children
506
- if (lowerExpressionType === 'rosgnode') {
507
- let nodeChildren = {
508
- name: '[[children]]',
509
- type: 'roArray',
510
- highLevelType: 'array',
511
- evaluateName: `${expression}.getChildren(-1,0)`,
512
- children: []
513
- };
514
- children.push(nodeChildren);
515
- }
516
- //xml elements won't display on their own, so we need to create some sub elements
517
- if (lowerExpressionType === 'roxmlelement') {
518
- //add a computed `[[children]]` property to allow expansion of node children
519
- children.push({
520
- name: '[[children]]',
521
- type: 'roArray',
522
- highLevelType: HighLevelType.array,
523
- evaluateName: `${expression}.GetChildNodes()`,
524
- children: []
525
- });
526
- children.push({
527
- name: '[[attributes]]',
528
- type: 'roArray',
529
- highLevelType: HighLevelType.array,
530
- evaluateName: `${expression}.GetAttributes()`,
531
- children: []
532
- });
533
- //look up the element name right now
534
- const container = await this.getVariable(`${expression}.GetName()`);
535
- container.name = '[[name]]';
536
- children.push(container);
537
- }
538
- //if this item is an array or a list, add the item count to the end of the type
539
- if (highLevelType === HighLevelType.array) {
540
- //TODO re-enable once we find how to refresh watch/variables panel, since lazy loaded arrays can't show a length
541
- //expressionType += `(${children.length})`;
542
- }
543
- let container = {
544
- name: expression,
545
- evaluateName: expression,
546
- type: expressionType,
547
- value: value.trim(),
548
- highLevelType: highLevelType,
549
- children: children
550
- };
551
- return container;
552
- }
553
- });
460
+ let expressionType = await this.getVariableType(expression);
461
+ let lowerExpressionType = expressionType ? expressionType.toLowerCase() : null;
462
+ let data;
463
+ //if the expression type is a string, we need to wrap the expression in quotes BEFORE we run the print so we can accurately capture the full string value
464
+ if (lowerExpressionType === 'string' || lowerExpressionType === 'rostring') {
465
+ data = await this.requestPipeline.executeCommand(`print "--string-wrap--" + ${expression} + "--string-wrap--"`, { waitForPrompt: true });
466
+ //write a for loop to print every value from the array. This gets around the `...` after the 100th item issue in the roku print call
467
+ }
468
+ else if (['roarray', 'rolist', 'roxmllist', 'robytearray'].includes(lowerExpressionType)) {
469
+ const command = [
470
+ `for each vscodeLoopItem in ${expression} : print ` +
471
+ ` "vscode_type_start:" + type(vscodeLoopItem) + ":vscode_type_stop "`,
472
+ ` "vscode_is_string:"; (invalid <> GetInterface(vscodeLoopItem, "ifString"))`,
473
+ ` vscodeLoopItem :` +
474
+ ` end for`
475
+ ].join(';');
476
+ data = await this.requestPipeline.executeCommand(command, { waitForPrompt: true });
477
+ }
478
+ else if (['roassociativearray', 'rosgnode'].includes(lowerExpressionType)) {
479
+ const command = [
480
+ `for each vscodeLoopKey in ${expression}.keys(): print` +
481
+ ` "vscode_key_start:" + vscodeLoopKey + ":vscode_key_stop "`,
482
+ ` "vscode_type_start:" + type(${expression}[vscodeLoopKey]) + ":vscode_type_stop "`,
483
+ ` "vscode_is_string:"; (invalid <> GetInterface(${expression}[vscodeLoopKey], "ifString"))`,
484
+ ` ${expression}[vscodeLoopKey] :` +
485
+ ' end for'
486
+ ].join(';');
487
+ data = await this.requestPipeline.executeCommand(command, { waitForPrompt: true });
488
+ }
489
+ else {
490
+ data = await this.requestPipeline.executeCommand(`print ${expression}`, { waitForPrompt: true });
491
+ }
492
+ logger.info('expression details', { data });
493
+ //remove excess whitespace
494
+ data = data.trim();
495
+ if (lowerExpressionType === 'string' || lowerExpressionType === 'rostring') {
496
+ data = data.trim().replace(/--string-wrap--/g, '');
497
+ //add an escape character in front of any existing quotes
498
+ data = data.replace(/"/g, '\\"');
499
+ //wrap the string value with literal quote marks
500
+ data = '"' + data + '"';
501
+ }
502
+ let highLevelType = this.getHighLevelType(expressionType);
503
+ let children;
504
+ if (highLevelType === interfaces_1.HighLevelType.array || ['roassociativearray', 'rosgnode', 'roxmllist', 'robytearray'].includes(lowerExpressionType)) {
505
+ //the print statment will always have 1 trailing newline, so remove that.
506
+ data = util_1.util.removeTrailingNewline(data);
507
+ //the array/associative array print is a loop of every value, so handle that
508
+ children = this.getForLoopPrintedChildren(expression, data);
509
+ children.push({
510
+ name: '[[count]]',
511
+ value: children.length.toString(),
512
+ type: 'integer',
513
+ highLevelType: interfaces_1.HighLevelType.primative,
514
+ evaluateName: children.length.toString(),
515
+ presentationHint: 'virtual',
516
+ keyType: KeyType.legacy,
517
+ children: undefined
518
+ });
519
+ }
520
+ else if (highLevelType === interfaces_1.HighLevelType.object) {
521
+ children = this.getObjectChildren(expression, data.trim());
522
+ }
523
+ else if (highLevelType === interfaces_1.HighLevelType.unknown) {
524
+ logger.warn('there was an issue evaluating this variable', { expression });
525
+ data = '<UNKNOWN>';
526
+ }
527
+ if (['rostring', 'roint', 'rointeger', 'rolonginteger', 'rofloat', 'rodouble', 'roboolean', 'rointrinsicdouble'].includes(lowerExpressionType)) {
528
+ return {
529
+ name: expression,
530
+ value: util_1.util.removeTrailingNewline(data),
531
+ type: expressionType,
532
+ highLevelType: interfaces_1.HighLevelType.primative,
533
+ evaluateName: expression,
534
+ children: []
535
+ };
536
+ }
537
+ //add a computed `[[children]]` property to allow expansion of node children
538
+ if (lowerExpressionType === 'rosgnode') {
539
+ let nodeChildren = {
540
+ name: '[[children]]',
541
+ type: 'roArray',
542
+ highLevelType: 'array',
543
+ presentationHint: 'virtual',
544
+ evaluateName: `${expression}.getChildren(-1, 0)`,
545
+ children: []
546
+ };
547
+ children.push(nodeChildren);
548
+ }
549
+ //xml elements won't display on their own, so we need to create some sub elements
550
+ if (lowerExpressionType === 'roxmlelement') {
551
+ children.push({
552
+ //look up the name of the xml element
553
+ ...await this.getVariable(`${expression}.GetName()`),
554
+ name: '[[name]]',
555
+ presentationHint: 'virtual'
556
+ });
557
+ children.push({
558
+ name: '[[attributes]]',
559
+ type: 'roAssociativeArray',
560
+ highLevelType: interfaces_1.HighLevelType.array,
561
+ evaluateName: `${expression}.GetAttributes()`,
562
+ presentationHint: 'virtual',
563
+ children: []
564
+ });
565
+ //add a computed `[[children]]` property to allow expansion of child elements
566
+ children.push({
567
+ name: '[[children]]',
568
+ type: 'roArray',
569
+ highLevelType: interfaces_1.HighLevelType.array,
570
+ evaluateName: `${expression}.GetChildNodes()`,
571
+ presentationHint: 'virtual',
572
+ children: []
573
+ });
574
+ }
575
+ //if this item is an array or a list, add the item count to the end of the type
576
+ if (highLevelType === interfaces_1.HighLevelType.array) {
577
+ //TODO re-enable once we find how to refresh watch/variables panel, since lazy loaded arrays can't show a length
578
+ //expressionType += `(${children.length})`;
579
+ }
580
+ let container = {
581
+ name: expression,
582
+ evaluateName: expression,
583
+ type: expressionType,
584
+ value: data.trim(),
585
+ highLevelType: highLevelType,
586
+ children: children
587
+ };
588
+ logger.info('end', { container });
589
+ return container;
554
590
  }
555
591
  /**
556
592
  * In order to get around the `...` issue in printed arrays, `getVariable` now prints every value from an array or associative array in a for loop.
@@ -625,22 +661,22 @@ class TelnetAdapter {
625
661
  let collectionEnd;
626
662
  if (line.includes('<Component: roList>')) {
627
663
  collectionEnd = ')';
628
- child.highLevelType = HighLevelType.array;
664
+ child.highLevelType = interfaces_1.HighLevelType.array;
629
665
  child.type = objectType;
630
666
  }
631
667
  else if (line.includes('<Component: roArray>')) {
632
668
  collectionEnd = ']';
633
- child.highLevelType = HighLevelType.array;
669
+ child.highLevelType = interfaces_1.HighLevelType.array;
634
670
  child.type = this.getObjectType(line);
635
671
  }
636
672
  else if (line.includes('<Component: roByteArray>')) {
637
673
  collectionEnd = ']';
638
- child.highLevelType = HighLevelType.array;
674
+ child.highLevelType = interfaces_1.HighLevelType.array;
639
675
  child.type = this.getObjectType(line);
640
676
  }
641
677
  else if (line.includes('<Component: roAssociativeArray>') || isRoSGNode) {
642
678
  collectionEnd = '}';
643
- child.highLevelType = HighLevelType.object;
679
+ child.highLevelType = interfaces_1.HighLevelType.object;
644
680
  child.type = this.getObjectType(line);
645
681
  }
646
682
  let collectionLineList = [line];
@@ -654,34 +690,24 @@ class TelnetAdapter {
654
690
  }
655
691
  //we have reached the end of the collection. scrap children because they need evaluated in a separate call to compute their types
656
692
  child.children = [];
657
- if (isRoSGNode) {
658
- let nodeChildrenProperty = {
659
- name: '[[children]]',
660
- type: 'roArray',
661
- highLevelType: 'array',
662
- evaluateName: `${child.evaluateName}.getChildren(-1,0)`,
663
- children: []
664
- };
665
- child.children.push(nodeChildrenProperty);
666
- }
667
693
  //this if block must pre-seek the `line.indexOf('<Component') > -1` line because roInvalid is a component too.
668
694
  }
669
695
  else if (objectType === 'roInvalid') {
670
- child.highLevelType = HighLevelType.uninitialized;
696
+ child.highLevelType = interfaces_1.HighLevelType.uninitialized;
671
697
  child.type = 'roInvalid';
672
698
  child.value = 'roInvalid';
673
699
  child.children = undefined;
674
700
  }
675
701
  else if (line.includes('<Component:')) {
676
702
  //handle things like nodes
677
- child.highLevelType = HighLevelType.object;
703
+ child.highLevelType = interfaces_1.HighLevelType.object;
678
704
  child.type = objectType;
679
705
  }
680
706
  else {
681
707
  //is some primative type
682
708
  child.type = type;
683
709
  child.value = line.trim();
684
- child.highLevelType = HighLevelType.primative;
710
+ child.highLevelType = interfaces_1.HighLevelType.primative;
685
711
  child.children = undefined;
686
712
  }
687
713
  children.push(child);
@@ -727,7 +753,7 @@ class TelnetAdapter {
727
753
  else {
728
754
  child.type = this.getPrimativeTypeFromValue(line);
729
755
  child.value = line;
730
- child.highLevelType = HighLevelType.primative;
756
+ child.highLevelType = interfaces_1.HighLevelType.primative;
731
757
  }
732
758
  children.push(child);
733
759
  arrayIndex++;
@@ -772,7 +798,7 @@ class TelnetAdapter {
772
798
  child = {
773
799
  name: line,
774
800
  type: '<ERROR>',
775
- highLevelType: HighLevelType.uninitialized,
801
+ highLevelType: interfaces_1.HighLevelType.uninitialized,
776
802
  evaluateName: undefined,
777
803
  variablePath: [],
778
804
  elementCount: -1,
@@ -797,7 +823,7 @@ class TelnetAdapter {
797
823
  else {
798
824
  child.type = this.getPrimativeTypeFromValue(trimmedLine);
799
825
  child.value = lineParseResult.value;
800
- child.highLevelType = HighLevelType.primative;
826
+ child.highLevelType = interfaces_1.HighLevelType.primative;
801
827
  }
802
828
  }
803
829
  children.push(child);
@@ -805,7 +831,7 @@ class TelnetAdapter {
805
831
  return children;
806
832
  }
807
833
  catch (e) {
808
- throw new Error(`Unable to parse BrightScript object: ${e.message}. Data: ${data}`);
834
+ throw new Error(`Unable to parse BrightScript object: ${JSON.stringify(e.message)}. Data: ${data}`);
809
835
  }
810
836
  }
811
837
  /**
@@ -814,24 +840,24 @@ class TelnetAdapter {
814
840
  */
815
841
  getHighLevelType(expressionType) {
816
842
  if (!expressionType) {
817
- throw new Error(`Unknown expression type: ${expressionType}`);
843
+ return interfaces_1.HighLevelType.unknown;
818
844
  }
819
845
  expressionType = expressionType.toLowerCase();
820
846
  let primativeTypes = ['boolean', 'integer', 'longinteger', 'float', 'double', 'string', 'rostring', 'invalid'];
821
847
  if (primativeTypes.includes(expressionType)) {
822
- return HighLevelType.primative;
848
+ return interfaces_1.HighLevelType.primative;
823
849
  }
824
850
  else if (expressionType === 'roarray' || expressionType === 'rolist') {
825
- return HighLevelType.array;
851
+ return interfaces_1.HighLevelType.array;
826
852
  }
827
853
  else if (expressionType === 'function') {
828
- return HighLevelType.function;
854
+ return interfaces_1.HighLevelType.function;
829
855
  }
830
856
  else if (expressionType === '<uninitialized>') {
831
- return HighLevelType.uninitialized;
857
+ return interfaces_1.HighLevelType.uninitialized;
832
858
  }
833
859
  else {
834
- return HighLevelType.object;
860
+ return interfaces_1.HighLevelType.object;
835
861
  }
836
862
  }
837
863
  /**
@@ -844,17 +870,10 @@ class TelnetAdapter {
844
870
  }
845
871
  expression = `Type(${expression})`;
846
872
  return this.resolve(`${expression}`, async () => {
847
- let data = await this.requestPipeline.executeCommand(`print ${expression}`, true);
848
- let match = this.getExpressionDetails(data);
849
- if (match) {
850
- let typeValue = match;
851
- //remove whitespace
852
- typeValue = typeValue.trim();
853
- return typeValue;
854
- }
855
- else {
856
- return null;
857
- }
873
+ var _a;
874
+ let data = await this.requestPipeline.executeCommand(`print ${expression}`, { waitForPrompt: true });
875
+ //remove whitespace
876
+ return (_a = data === null || data === void 0 ? void 0 : data.trim()) !== null && _a !== void 0 ? _a : null;
858
877
  });
859
878
  }
860
879
  /**
@@ -865,11 +884,15 @@ class TelnetAdapter {
865
884
  resolve(key, factory) {
866
885
  try {
867
886
  if (this.cache[key]) {
887
+ this.logger.debug(`resolve cache "${key}": already exists`);
888
+ return this.cache[key];
889
+ }
890
+ else {
891
+ this.logger.debug(`resolve cache "${key}": calling factory`);
892
+ const result = factory();
893
+ this.cache[key] = Promise.resolve(result);
868
894
  return this.cache[key];
869
895
  }
870
- const result = factory();
871
- this.cache[key] = Promise.resolve(result);
872
- return this.cache[key];
873
896
  }
874
897
  catch (e) {
875
898
  return Promise.reject(e);
@@ -879,12 +902,14 @@ class TelnetAdapter {
879
902
  * Get a list of threads. The first thread in the list is the active thread
880
903
  */
881
904
  async getThreads() {
905
+ this.logger.log('getThreads');
882
906
  if (!this.isAtDebuggerPrompt) {
883
- throw new Error('Cannot get threads: debugger is not paused');
907
+ this.logger.log('Cannot get threads: debugger is not paused');
908
+ return [];
884
909
  }
885
910
  return this.resolve('threads', async () => {
886
- let data = await this.requestPipeline.executeCommand('threads', true);
887
- let dataString = data.toString();
911
+ let data = await this.requestPipeline.executeCommand('threads', { waitForPrompt: true });
912
+ let dataString = data.toString().trim();
888
913
  let matches = /^\s+(\d+\*)\s+(.*)\((\d+)\)\s+(.*)/gm.exec(dataString);
889
914
  let threads = [];
890
915
  if (matches) {
@@ -915,9 +940,8 @@ class TelnetAdapter {
915
940
  /**
916
941
  * Disconnect from the telnet session and unset all objects
917
942
  */
918
- async destroy() {
943
+ destroy() {
919
944
  if (this.requestPipeline) {
920
- await this.exitActiveBrightscriptDebugger();
921
945
  this.requestPipeline.destroy();
922
946
  }
923
947
  this.requestPipeline = undefined;
@@ -926,20 +950,8 @@ class TelnetAdapter {
926
950
  this.emitter.removeAllListeners();
927
951
  }
928
952
  this.emitter = undefined;
929
- }
930
- /**
931
- * Make sure any active Brightscript Debugger threads are exited
932
- */
933
- async exitActiveBrightscriptDebugger() {
934
- if (this.requestPipeline) {
935
- let commandsExecuted = 0;
936
- do {
937
- let data = await this.requestPipeline.executeCommand(`exit`, false);
938
- // This seems to work without the delay but I wonder about slower devices
939
- // await setTimeout[Object.getOwnPropertySymbols(setTimeout)[0]](100);
940
- commandsExecuted++;
941
- } while (commandsExecuted < 10);
942
- }
953
+ //needs to be async to match the DebugProtocolAdapter implementation
954
+ return Promise.resolve();
943
955
  }
944
956
  // #region Rendezvous Tracker pass though functions
945
957
  /**
@@ -974,14 +986,6 @@ var EventName;
974
986
  (function (EventName) {
975
987
  EventName["suspend"] = "suspend";
976
988
  })(EventName = exports.EventName || (exports.EventName = {}));
977
- var HighLevelType;
978
- (function (HighLevelType) {
979
- HighLevelType["primative"] = "primative";
980
- HighLevelType["array"] = "array";
981
- HighLevelType["function"] = "function";
982
- HighLevelType["object"] = "object";
983
- HighLevelType["uninitialized"] = "uninitialized";
984
- })(HighLevelType = exports.HighLevelType || (exports.HighLevelType = {}));
985
989
  var KeyType;
986
990
  (function (KeyType) {
987
991
  KeyType["string"] = "String";
@@ -996,157 +1000,4 @@ var PrimativeType;
996
1000
  PrimativeType["integer"] = "Integer";
997
1001
  PrimativeType["float"] = "Float";
998
1002
  })(PrimativeType = exports.PrimativeType || (exports.PrimativeType = {}));
999
- class RequestPipeline {
1000
- constructor(client) {
1001
- this.client = client;
1002
- this.requests = [];
1003
- this.isAtDebuggerPrompt = false;
1004
- this.currentRequest = undefined;
1005
- this.emitter = new EventEmitter();
1006
- this.debuggerLineRegex = /Brightscript\s+Debugger>\s*$/i;
1007
- this.connect();
1008
- }
1009
- get isProcessing() {
1010
- return this.currentRequest !== undefined;
1011
- }
1012
- get hasRequests() {
1013
- return this.requests.length > 0;
1014
- }
1015
- on(eventName, handler) {
1016
- this.emitter.on(eventName, handler);
1017
- return () => {
1018
- this.emitter.removeListener(eventName, handler);
1019
- };
1020
- }
1021
- emit(eventName, data) {
1022
- this.emitter.emit(eventName, data);
1023
- }
1024
- connect() {
1025
- let allResponseText = '';
1026
- let lastPartialLine = '';
1027
- this.client.addListener('data', (data) => {
1028
- let responseText = data.toString();
1029
- const cumulative = lastPartialLine + responseText;
1030
- if (!cumulative.endsWith('\n') && !this.checkForDebuggerPrompt(cumulative)) {
1031
- // buffer was split and was not the result of a prompt, save the partial line
1032
- lastPartialLine += responseText;
1033
- return;
1034
- }
1035
- if (lastPartialLine) {
1036
- // there was leftover lines, join the partial lines back together
1037
- responseText = lastPartialLine + responseText;
1038
- lastPartialLine = '';
1039
- }
1040
- //forward all raw console output
1041
- this.emit('console-output', responseText);
1042
- allResponseText += responseText;
1043
- let foundDebuggerPrompt = this.checkForDebuggerPrompt(allResponseText);
1044
- //if we are not processing, immediately broadcast the latest data
1045
- if (!this.isProcessing) {
1046
- this.emit('unhandled-console-output', allResponseText);
1047
- allResponseText = '';
1048
- if (foundDebuggerPrompt) {
1049
- this.isAtDebuggerPrompt = true;
1050
- if (this.hasRequests) {
1051
- // There are requests waiting to be processed
1052
- this.process();
1053
- }
1054
- }
1055
- }
1056
- else {
1057
- //if responseText produced a prompt, return the responseText
1058
- if (foundDebuggerPrompt) {
1059
- //resolve the command's promise (if it cares)
1060
- this.isAtDebuggerPrompt = true;
1061
- this.currentRequest.onComplete(allResponseText);
1062
- allResponseText = '';
1063
- this.currentRequest = undefined;
1064
- //try to run the next request
1065
- this.process();
1066
- }
1067
- }
1068
- });
1069
- }
1070
- /**
1071
- * Checks the supplied string for the debugger input prompt
1072
- * @param responseText
1073
- */
1074
- checkForDebuggerPrompt(responseText) {
1075
- let match = this.debuggerLineRegex.exec(responseText.trim());
1076
- return (match);
1077
- }
1078
- /**
1079
- * Schedule a command to be run. Resolves with the result once the command finishes
1080
- * @param commandFunction
1081
- * @param waitForPrompt - if true, the promise will wait until we find a prompt, and return all output in between. If false, the promise will immediately resolve
1082
- * @param forceExecute - if true, it is assumed the command can be run at any time and will be executed immediately
1083
- * @param silent - if true, the command will be hidden from the output
1084
- */
1085
- executeCommand(command, waitForPrompt, forceExecute = false, silent = false) {
1086
- console.debug(`Execute command (and ${waitForPrompt ? 'do' : 'do not'} wait for prompt):`, command);
1087
- return new Promise((resolve, reject) => {
1088
- let executeCommand = () => {
1089
- let commandText = `${command}\r\n`;
1090
- if (!silent) {
1091
- this.emit('console-output', command);
1092
- }
1093
- this.client.write(commandText);
1094
- if (waitForPrompt) {
1095
- // The act of executing this command means we are no longer at the debug prompt
1096
- this.isAtDebuggerPrompt = false;
1097
- }
1098
- };
1099
- let request = {
1100
- executeCommand: executeCommand,
1101
- onComplete: (data) => {
1102
- console.debug(`Command finished (${waitForPrompt ? 'after waiting for prompt' : 'did not wait for prompt'}`, command);
1103
- console.debug('Data:', data);
1104
- resolve(data);
1105
- },
1106
- waitForPrompt: waitForPrompt
1107
- };
1108
- if (!waitForPrompt) {
1109
- if (!this.isProcessing || forceExecute) {
1110
- //fire and forget the command
1111
- request.executeCommand();
1112
- //the command doesn't care about the output, resolve it immediately
1113
- request.onComplete(undefined);
1114
- }
1115
- else {
1116
- // Skip this request as the device is not ready to accept the command or it can not be run at any time
1117
- }
1118
- }
1119
- else {
1120
- this.requests.push(request);
1121
- if (this.isAtDebuggerPrompt) {
1122
- //start processing since we are already at a debug prompt (safe to call multiple times)
1123
- this.process();
1124
- }
1125
- else {
1126
- // do not run the command until the device is at a debug prompt.
1127
- // this will be detected in the data listener in the connect function
1128
- }
1129
- }
1130
- });
1131
- }
1132
- /**
1133
- * Internal request processing function
1134
- */
1135
- process() {
1136
- if (this.isProcessing || !this.hasRequests) {
1137
- return;
1138
- }
1139
- //get the oldest command
1140
- let nextRequest = this.requests.shift();
1141
- this.currentRequest = nextRequest;
1142
- //run the request. the data listener will handle launching the next request once this one has finished processing
1143
- nextRequest.executeCommand();
1144
- }
1145
- destroy() {
1146
- this.client.removeAllListeners();
1147
- this.client.destroy();
1148
- this.client = undefined;
1149
- }
1150
- }
1151
- exports.RequestPipeline = RequestPipeline;
1152
1003
  //# sourceMappingURL=TelnetAdapter.js.map