roku-debug 0.8.6 → 0.9.2

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