From 1d66fc3f64eef565e90bd3003eb71ad5e8d19513 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 17 May 2022 10:17:50 -0400 Subject: [PATCH 001/197] Adds BSChannelPublishedEvent custom event (#81) --- src/debugSession/BrightScriptDebugSession.ts | 5 ++++- src/debugSession/Events.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 3b593236..54e76a7e 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -36,7 +36,8 @@ import { CompileFailureEvent, StoppedEventReason, ChanperfEvent, - DebugServerLogOutputEvent + DebugServerLogOutputEvent, + ChannelPublishedEvent } from './Events'; import type { LaunchConfiguration, ComponentLibraryConfiguration } from '../LaunchConfiguration'; import { FileManager } from '../managers/FileManager'; @@ -297,6 +298,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { //publish the package to the target Roku await this.rokuDeploy.publish(this.launchConfiguration as any as RokuDeployOptions); + this.sendEvent(new ChannelPublishedEvent()); + if (this.enableDebugProtocol) { //connect to the roku debug via sockets await this.connectRokuAdapter(); diff --git a/src/debugSession/Events.ts b/src/debugSession/Events.ts index c7039d93..581e7b39 100644 --- a/src/debugSession/Events.ts +++ b/src/debugSession/Events.ts @@ -70,6 +70,19 @@ export class LaunchStartEvent implements DebugProtocol.Event { public type: string; } +export class ChannelPublishedEvent implements DebugProtocol.Event { + constructor() { + this.body = {}; + this.event = 'BSChannelPublishedEvent'; + } + + public body: any; + public event: string; + public seq: number; + public type: string; +} + + export enum StoppedEventReason { step = 'step', breakpoint = 'breakpoint', From ca7d4f2c109ce0872bb9d807282a24631e5e2303 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 17 May 2022 10:19:47 -0400 Subject: [PATCH 002/197] update changelog for v0.12.0 --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c0d148..bf10bf28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.12.0](https://github.com/rokucommunity/roku-debug/compare/v0.11.0...v0.12.0) - 2022-05-17 +### Added + - `BSChannelPublishedEvent` custom event to allow clients to handle when the channel has been uploaded to a Roku ([#81](https://github.com/rokucommunity/roku-debug/pull/81)) + + + ## [0.11.0](https://github.com/rokucommunity/roku-debug/compare/v0.10.5...v0.11.0) - 2022-05-05 -### Changed - - upgrade to [brighterscript@0.49.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0490---2022-05-02) ### Added - `brightScriptConsolePort` option. Utilize `remotePort` in more places ([#79](https://github.com/rokucommunity/roku-debug/pull/79)) -basic breakpoint logic for debug protocol (only useful for direct API access at the moment) ([#77](https://github.com/rokucommunity/roku-debug/pull/77)) + ### Changed + - upgrade to [brighterscript@0.49.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0490---2022-05-02) ### Fixed - fix RDB path bug on windows ([#76](https://github.com/rokucommunity/roku-debug/pull/76)) From dd0bb78ba6a140c410ba88c87890c2de012c1857 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 17 May 2022 10:20:11 -0400 Subject: [PATCH 003/197] 0.12.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e2c4653..b986f314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.11.0", + "version": "0.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.11.0", + "version": "0.12.0", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index 7e133e7c..f5c0c87a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.11.0", + "version": "0.12.0", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From c1fad7cd426a485a3f3f9e1ed92326bbd1e77828 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 17 May 2022 15:30:28 -0400 Subject: [PATCH 004/197] Prevent crash during rendezvous tracking (#82) * Prevent crash during rendezvous tracking * Add tests --- src/managers/ProjectManager.spec.ts | 7 ++++++- src/managers/ProjectManager.ts | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 8fe1b2e4..edf8aff5 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -216,6 +216,12 @@ describe('ProjectManager', () => { }); describe('getSourceLocation', () => { + it(`does not crash when file is missing`, async () => { + manager.mainProject.fileMappings = []; + let sourceLocation = await manager.getSourceLocation('pkg:/source/file-we-dont-know-about.brs', 1); + expect(n(sourceLocation.filePath)).to.equal(n(`${stagingFolderPath}/source/file-we-dont-know-about.brs`)); + }); + it('handles truncated paths', async () => { //mock fsExtra so we don't have to create actual files sinon.stub(fsExtra as any, 'pathExists').callsFake((filePath: string) => { @@ -275,7 +281,6 @@ describe('ProjectManager', () => { sourceLocation = await manager.getSourceLocation('pkg:/source/file2.brs', 1); expect(n(sourceLocation.filePath)).to.equal(n(`${rootDir}/source/file2.brs`)); }); - }); }); diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 12efb00b..3b756062 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -118,15 +118,15 @@ export class ProjectManager { }); //if sourcemaps are disabled, account for the breakpoint offsets - if (this.launchConfiguration?.enableSourceMaps === false) { + if (sourceLocation && this.launchConfiguration?.enableSourceMaps === false) { sourceLocation.lineNumber = this.getLineNumberOffsetByBreakpoints(sourceLocation.filePath, sourceLocation.lineNumber); } - if (!sourceLocation.filePath) { + if (!sourceLocation?.filePath) { //couldn't find a source location. At least send back the staging file information so the user can still debug return { filePath: stagingFileInfo.absolutePath, - lineNumber: sourceLocation.lineNumber || debuggerLineNumber, + lineNumber: sourceLocation?.lineNumber || debuggerLineNumber, columnIndex: 0 } as SourceLocation; } else { From 681d462df190dccd82b203c3fba81235e8ccb887 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 19 May 2022 11:29:29 -0400 Subject: [PATCH 005/197] Adds `launchConfiguration` to ChannelPublishedEvent (#83) --- src/debugSession/BrightScriptDebugSession.ts | 4 +++- src/debugSession/Events.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 54e76a7e..7b30fb2f 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -298,7 +298,9 @@ export class BrightScriptDebugSession extends BaseDebugSession { //publish the package to the target Roku await this.rokuDeploy.publish(this.launchConfiguration as any as RokuDeployOptions); - this.sendEvent(new ChannelPublishedEvent()); + this.sendEvent(new ChannelPublishedEvent({ + launchConfiguration: this.launchConfiguration + })); if (this.enableDebugProtocol) { //connect to the roku debug via sockets diff --git a/src/debugSession/Events.ts b/src/debugSession/Events.ts index 581e7b39..16f91d26 100644 --- a/src/debugSession/Events.ts +++ b/src/debugSession/Events.ts @@ -71,8 +71,12 @@ export class LaunchStartEvent implements DebugProtocol.Event { } export class ChannelPublishedEvent implements DebugProtocol.Event { - constructor() { - this.body = {}; + constructor( + body: { + launchConfiguration: LaunchConfiguration; + } + ) { + this.body = body ?? {}; this.event = 'BSChannelPublishedEvent'; } From 0f01e653a9accdbee5fccee6d18fa6e92e8af54b Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 20 May 2022 13:06:39 -0400 Subject: [PATCH 006/197] brighterscript@0.50.1 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b986f314..ea182d01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.49.0", + "brighterscript": "^0.50.1", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.49.0.tgz", - "integrity": "sha512-B4xVu6O6+F2FhHSa+dtRs18krUitp7bRm25xpiHsnSNg+G+mxmh9ALbd0iNX9PCPstknEVO9uC6dsDF43XcOEg==", + "version": "0.50.1", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.1.tgz", + "integrity": "sha512-hOTBGruwO8abMeBOU2mTc3Wew4Lb3cXDtHj/JFfcypPr/NOWa5W6a+4T0p/wwxYxUQUR8C+QDV/uRqLWZGM2bA==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5537,9 +5537,9 @@ } }, "brighterscript": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.49.0.tgz", - "integrity": "sha512-B4xVu6O6+F2FhHSa+dtRs18krUitp7bRm25xpiHsnSNg+G+mxmh9ALbd0iNX9PCPstknEVO9uC6dsDF43XcOEg==", + "version": "0.50.1", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.1.tgz", + "integrity": "sha512-hOTBGruwO8abMeBOU2mTc3Wew4Lb3cXDtHj/JFfcypPr/NOWa5W6a+4T0p/wwxYxUQUR8C+QDV/uRqLWZGM2bA==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", diff --git a/package.json b/package.json index f5c0c87a..890e0a05 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.49.0", + "brighterscript": "^0.50.1", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From 1f91b9ae6022b0cc63186e098ba7b18f443dacc0 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 20 May 2022 13:06:43 -0400 Subject: [PATCH 007/197] update changelog for v0.12.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf10bf28..30b442e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.12.1](https://github.com/rokucommunity/roku-debug/compare/v0.12.0...v0.12.1) - 2022-05-20 +### Changed + - add `launchConfiguration` to the `ChannelPublishedEvent` ([#83](https://github.com/rokucommunity/roku-debug/pull/83)) + ### Fixed + - crash during rendezvous tracking ([#82](https://github.com/rokucommunity/roku-debug/pull/82)) + + + ## [0.12.0](https://github.com/rokucommunity/roku-debug/compare/v0.11.0...v0.12.0) - 2022-05-17 ### Added - `BSChannelPublishedEvent` custom event to allow clients to handle when the channel has been uploaded to a Roku ([#81](https://github.com/rokucommunity/roku-debug/pull/81)) From 77bf60945d565fff9f18f9c8b498e491e5d292c3 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 20 May 2022 13:07:20 -0400 Subject: [PATCH 008/197] 0.12.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea182d01..9e307302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.12.0", + "version": "0.12.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.12.0", + "version": "0.12.1", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index 890e0a05..3a34a217 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.12.0", + "version": "0.12.1", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From d1848e6e45e130d76cf86a78a61519ea818040fa Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 23 May 2022 10:16:47 -0400 Subject: [PATCH 009/197] roku-deploy@3.7.0 --- package-lock.json | 41 ++++++++++++++++---- package.json | 2 +- src/FileUtils.ts | 4 +- src/adapters/TelnetAdapter.ts | 2 +- src/debugSession/BrightScriptDebugSession.ts | 2 +- src/managers/ProjectManager.spec.ts | 2 +- src/managers/ProjectManager.ts | 4 +- 7 files changed, 42 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e307302..1896d19b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.6.0", + "roku-deploy": "^3.7.0", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", @@ -1665,6 +1665,11 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + }, "node_modules/debounce-promise": { "version": "3.1.2", "license": "MIT" @@ -3961,12 +3966,13 @@ } }, "node_modules/roku-deploy": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.6.0.tgz", - "integrity": "sha512-kfgM/EOhM/X1wXOx+8hGCIH9o/eu5Vtw8qmhnHA0EIIyRhHQPy+W4iKcab6fPbibLKo4f0V47GKO1TTKNR+6kA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.0.tgz", + "integrity": "sha512-+MBppc3q7ZEhzXu86O43mITxa+ro4hkCCXt4UhsWDqvCNx7w4b6CHzjrRAXYtdGQoDe6xqZjJhqCqC/YytJqWA==", "dependencies": { "chalk": "^2.4.2", "dateformat": "^3.0.3", + "dayjs": "^1.11.0", "fast-glob": "^3.2.11", "fs-extra": "^7.0.1", "is-glob": "^4.0.3", @@ -3976,6 +3982,7 @@ "moment": "^2.29.1", "parse-ms": "^2.1.0", "request": "^2.88.0", + "temp-dir": "^2.0.0", "xml2js": "^0.4.23" }, "bin": { @@ -4328,6 +4335,14 @@ "url": "https://paypal.me/kozjak" } }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "engines": { + "node": ">=8" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "dev": true, @@ -5854,6 +5869,11 @@ "dateformat": { "version": "4.6.3" }, + "dayjs": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + }, "debounce-promise": { "version": "3.1.2" }, @@ -7291,12 +7311,13 @@ } }, "roku-deploy": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.6.0.tgz", - "integrity": "sha512-kfgM/EOhM/X1wXOx+8hGCIH9o/eu5Vtw8qmhnHA0EIIyRhHQPy+W4iKcab6fPbibLKo4f0V47GKO1TTKNR+6kA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.0.tgz", + "integrity": "sha512-+MBppc3q7ZEhzXu86O43mITxa+ro4hkCCXt4UhsWDqvCNx7w4b6CHzjrRAXYtdGQoDe6xqZjJhqCqC/YytJqWA==", "requires": { "chalk": "^2.4.2", "dateformat": "^3.0.3", + "dayjs": "^1.11.0", "fast-glob": "^3.2.11", "fs-extra": "^7.0.1", "is-glob": "^4.0.3", @@ -7306,6 +7327,7 @@ "moment": "^2.29.1", "parse-ms": "^2.1.0", "request": "^2.88.0", + "temp-dir": "^2.0.0", "xml2js": "^0.4.23" }, "dependencies": { @@ -7528,6 +7550,11 @@ "bluebird": "^3.5.4" } }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==" + }, "test-exclude": { "version": "6.0.0", "dev": true, diff --git a/package.json b/package.json index 3a34a217..4e4ff03a 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.6.0", + "roku-deploy": "^3.7.0", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", diff --git a/src/FileUtils.ts b/src/FileUtils.ts index be8103bf..042ed09a 100644 --- a/src/FileUtils.ts +++ b/src/FileUtils.ts @@ -3,7 +3,7 @@ import * as fsExtra from 'fs-extra'; import * as glob from 'glob'; import * as path from 'path'; import { promisify } from 'util'; -import * as rokuDeploy from 'roku-deploy'; +import { util as rokuDeployUtil } from 'roku-deploy'; const globp = promisify(glob); export class FileUtils { @@ -262,7 +262,7 @@ export class FileUtils { } } let relativePath = fileUtils.removeLeadingSlash( - rokuDeploy.util.stringReplaceInsensitive(entryPath, projectPath, '') + rokuDeployUtil.stringReplaceInsensitive(entryPath, projectPath, '') ); return { diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 72b15521..bb839151 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -1,7 +1,7 @@ import { orderBy } from 'natural-orderby'; import * as EventEmitter from 'eventemitter3'; import { Socket } from 'net'; -import * as rokuDeploy from 'roku-deploy'; +import { rokuDeploy } from 'roku-deploy'; import { PrintedObjectParser } from '../PrintedObjectParser'; import { CompileErrorProcessor } from '../CompileErrorProcessor'; import type { RendezvousHistory } from '../RendezvousTracker'; diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 7b30fb2f..c73a9460 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -2,7 +2,7 @@ import * as fsExtra from 'fs-extra'; import { orderBy } from 'natural-orderby'; import * as path from 'path'; import * as request from 'request'; -import * as rokuDeploy from 'roku-deploy'; +import { rokuDeploy } from 'roku-deploy'; import type { RokuDeploy, RokuDeployOptions } from 'roku-deploy'; import { DebugSession as BaseDebugSession, diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index edf8aff5..20f7753e 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; import * as path from 'path'; -import * as rokuDeploy from 'roku-deploy'; +import { rokuDeploy } from 'roku-deploy'; import * as sinonActual from 'sinon'; import { fileUtils, standardizePath as s } from '../FileUtils'; import type { ComponentLibraryConstructorParams } from './ProjectManager'; diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 3b756062..eeb8eed4 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import * as fsExtra from 'fs-extra'; import * as path from 'path'; -import * as rokuDeploy from 'roku-deploy'; +import { rokuDeploy, RokuDeploy } from 'roku-deploy'; import type { FileEntry } from 'roku-deploy'; import * as glob from 'glob'; import { promisify } from 'util'; @@ -247,7 +247,7 @@ export class Project { private logger = logger.createLogger(`[${ProjectManager.name}]`); public async stage() { - let rd = new rokuDeploy.RokuDeploy(); + let rd = new RokuDeploy(); if (!this.fileMappings) { this.fileMappings = await this.getFileMappings(); } From 38aba693f64774536ad71b1a3f8ab9ea5ef2dc58 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 23 May 2022 10:17:51 -0400 Subject: [PATCH 010/197] brighterscript@0.50.2 --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1896d19b..7785ba55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.50.1", + "brighterscript": "^0.50.2", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.50.1", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.1.tgz", - "integrity": "sha512-hOTBGruwO8abMeBOU2mTc3Wew4Lb3cXDtHj/JFfcypPr/NOWa5W6a+4T0p/wwxYxUQUR8C+QDV/uRqLWZGM2bA==", + "version": "0.50.2", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.2.tgz", + "integrity": "sha512-PNW25VbP9CKNY1SXJxqxPBsr4M6QdrwfEBdjno3i4HAfOLTCTYExxVO0Xu4xMzwlyFB3tAPV2fnlH+KqRmwocQ==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -1196,7 +1196,7 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", - "roku-deploy": "^3.5.4", + "roku-deploy": "^3.7.0", "serialize-error": "^7.0.1", "source-map": "^0.7.3", "vscode-languageserver": "7.0.0", @@ -5552,9 +5552,9 @@ } }, "brighterscript": { - "version": "0.50.1", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.1.tgz", - "integrity": "sha512-hOTBGruwO8abMeBOU2mTc3Wew4Lb3cXDtHj/JFfcypPr/NOWa5W6a+4T0p/wwxYxUQUR8C+QDV/uRqLWZGM2bA==", + "version": "0.50.2", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.2.tgz", + "integrity": "sha512-PNW25VbP9CKNY1SXJxqxPBsr4M6QdrwfEBdjno3i4HAfOLTCTYExxVO0Xu4xMzwlyFB3tAPV2fnlH+KqRmwocQ==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5576,7 +5576,7 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", - "roku-deploy": "^3.5.4", + "roku-deploy": "^3.7.0", "serialize-error": "^7.0.1", "source-map": "^0.7.3", "vscode-languageserver": "7.0.0", diff --git a/package.json b/package.json index 4e4ff03a..2939faa1 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.50.1", + "brighterscript": "^0.50.2", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From 683587a08c67cbafb204246235cb3612dd08cfbb Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 26 May 2022 13:46:39 -0400 Subject: [PATCH 011/197] Fix lint issues, remove some disable comments (#85) --- src/CompileErrorProcessor.ts | 3 +-- src/adapters/TelnetAdapter.ts | 5 +---- src/debugProtocol/Debugger.ts | 27 +++++++++++++-------------- src/managers/BreakpointManager.ts | 3 +-- src/managers/FileManager.ts | 3 +-- src/managers/LocationManager.spec.ts | 3 +-- src/managers/ProjectManager.ts | 3 +-- src/util.spec.ts | 1 - src/util.ts | 3 +-- 9 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/CompileErrorProcessor.ts b/src/CompileErrorProcessor.ts index de933a9a..dfc3ec94 100644 --- a/src/CompileErrorProcessor.ts +++ b/src/CompileErrorProcessor.ts @@ -181,8 +181,7 @@ export class CompileErrorProcessor { let errors: BrightScriptDebugCompileError[] = []; let getFileInfoRegEx = /^--- Line (\d*): (.*)$/gim; let match: RegExpExecArray; - // eslint-disable-next-line no-cond-assign - while (match = getFileInfoRegEx.exec(fileErrorText)) { + while ((match = getFileInfoRegEx.exec(fileErrorText))) { let lineNumber = parseInt(match[1]); // 1-based let errorText = 'ERR_COMPILE:'; let message = this.sanitizeCompilePath(match[2]); diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 72b15521..4e5db71a 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -296,7 +296,6 @@ export class TelnetAdapter { if (this.isActivated) { //watch for the start of the program - // eslint-disable-next-line no-cond-assign if (/\[scrpt.ctx.run.enter\]/i.exec(responseText.trim())) { this.isAppRunning = true; this.logger.log('Running beacon detected', { responseText }); @@ -304,7 +303,6 @@ export class TelnetAdapter { } //watch for the end of the program - // eslint-disable-next-line no-cond-assign if (/\[beacon.report\] \|AppExitComplete/i.exec(responseText.trim())) { this.beginAppExit(); } @@ -471,8 +469,7 @@ export class TelnetAdapter { let regexp = /#(\d+)\s+(?:function|sub)\s+([\$\w\d]+).*\s+file\/line:\s+(.*)\((\d+)\)/ig; let matches: RegExpExecArray; let frames: StackFrame[] = []; - // eslint-disable-next-line no-cond-assign - while (matches = regexp.exec(responseText)) { + while ((matches = regexp.exec(responseText))) { //the first index is the whole string //then the matches should be in pairs for (let i = 1; i < matches.length; i += 4) { diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 578a0e93..272e9e8b 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -100,7 +100,7 @@ export class Debugger { } private emit( - /* eslint-disable */ + /* eslint-disable @typescript-eslint/indent */ eventName: 'app-exit' | 'cannot-continue' | @@ -113,7 +113,7 @@ export class Debugger { 'runtime-error' | 'start' | 'suspend', - /* eslint-disable */ + /* eslint-enable @typescript-eslint/indent */ data? ) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues @@ -183,12 +183,10 @@ export class Debugger { } public async continue() { - let result; if (this.stopped) { this.stopped = false; - result = this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.CONTINUE); + return this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.CONTINUE); } - return result; } public async pause(force = false) { @@ -327,7 +325,7 @@ export class Debugger { let unsubscribe = this.on('data', (data) => { if (data.requestId === requestId) { unsubscribe(); - resolve(data); + resolve(data as T); } }); @@ -350,7 +348,7 @@ export class Debugger { let packetLength = debuggerRequestResponse.packetLength; let slicedBuffer = packetLength ? buffer.slice(4) : buffer; - this.logger.log('incoming data - ', `bytes: ${buffer.length}`, debuggerRequestResponse) + this.logger.log('incoming data - ', `bytes: ${buffer.length}`, debuggerRequestResponse); if (debuggerRequestResponse.success) { if (debuggerRequestResponse.requestId > this.totalRequests) { this.removedProcessedBytes(debuggerRequestResponse, slicedBuffer, packetLength); @@ -364,7 +362,7 @@ export class Debugger { } if (debuggerRequestResponse.updateType > 0) { - this.logger.log('Update Type:', UPDATE_TYPES[debuggerRequestResponse.updateType]) + this.logger.log('Update Type:', UPDATE_TYPES[debuggerRequestResponse.updateType]); switch (debuggerRequestResponse.updateType) { case UPDATE_TYPES.IO_PORT_OPENED: return this.connectToIoPort(new ConnectIOPortResponse(slicedBuffer), buffer, packetLength); @@ -372,18 +370,19 @@ export class Debugger { case UPDATE_TYPES.THREAD_ATTACHED: let debuggerUpdateThreads = new UpdateThreadsResponse(slicedBuffer); if (debuggerUpdateThreads.success) { - this.handleThreadsUpdate(debuggerUpdateThreads); + //TODO should we be awaiting this? + void this.handleThreadsUpdate(debuggerUpdateThreads); this.removedProcessedBytes(debuggerUpdateThreads, slicedBuffer, packetLength); return true; } - return false + return false; case UPDATE_TYPES.UNDEF: return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); default: return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); } } else { - this.logger.log('Command Type:', COMMANDS[this.activeRequests[debuggerRequestResponse.requestId].commandType]) + this.logger.log('Command Type:', COMMANDS[this.activeRequests[debuggerRequestResponse.requestId].commandType]); switch (this.activeRequests[debuggerRequestResponse.requestId].commandType) { case COMMANDS.STOP: case COMMANDS.CONTINUE: @@ -431,7 +430,7 @@ export class Debugger { return false; } - private checkResponse(responseClass: { requestId: number, readOffset: number, success: boolean }, unhandledData: Buffer, packetLength = 0) { + private checkResponse(responseClass: { requestId: number; readOffset: number; success: boolean }, unhandledData: Buffer, packetLength = 0) { if (responseClass.success) { this.removedProcessedBytes(responseClass, unhandledData, packetLength); return true; @@ -441,7 +440,7 @@ export class Debugger { return false; } - private removedProcessedBytes(responseHandler: { requestId: number, readOffset: number }, unhandledData: Buffer, packetLength = 0) { + private removedProcessedBytes(responseHandler: { requestId: number; readOffset: number }, unhandledData: Buffer, packetLength = 0) { if (responseHandler.requestId > 0 && this.activeRequests[responseHandler.requestId]) { delete this.activeRequests[responseHandler.requestId]; } @@ -541,7 +540,7 @@ export class Debugger { this.removedProcessedBytes(connectIoPortResponse, unhandledData, packetLength); return true; } - return false + return false; } private async handleThreadsUpdate(update: UpdateThreadsResponse) { diff --git a/src/managers/BreakpointManager.ts b/src/managers/BreakpointManager.ts index 6a6e80ff..fe2e438f 100644 --- a/src/managers/BreakpointManager.ts +++ b/src/managers/BreakpointManager.ts @@ -332,8 +332,7 @@ export class BreakpointManager { let match: RegExpExecArray; // Get all the value to evaluate as expressions - // eslint-disable-next-line no-cond-assign - while (match = expressionsCheck.exec(logMessage)) { + while ((match = expressionsCheck.exec(logMessage))) { logMessage = logMessage.replace(match[0], `"; ${match[1]};"`); } diff --git a/src/managers/FileManager.ts b/src/managers/FileManager.ts index b46d686d..02198a3d 100644 --- a/src/managers/FileManager.ts +++ b/src/managers/FileManager.ts @@ -108,8 +108,7 @@ export class FileManager { let result = {}; //create a cache of all function names in this file - // eslint-disable-next-line no-cond-assign - while (match = regexp.exec(fileContents)) { + while ((match = regexp.exec(fileContents))) { let correctFunctionName = match[1]; result[correctFunctionName.toLowerCase()] = correctFunctionName; } diff --git a/src/managers/LocationManager.spec.ts b/src/managers/LocationManager.spec.ts index 2718f90f..8387a665 100644 --- a/src/managers/LocationManager.spec.ts +++ b/src/managers/LocationManager.spec.ts @@ -17,8 +17,7 @@ const sourceDirs = [ describe('LocationManager', () => { let locationManager: LocationManager; let sourceMapManager: SourceMapManager; - // eslint-disable-next-line prefer-arrow-callback - beforeEach(function beforeEach() { + beforeEach(() => { sourceMapManager = new SourceMapManager(); locationManager = new LocationManager(sourceMapManager); fsExtra.removeSync(tempDir); diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 3b756062..e2fe8707 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -499,8 +499,7 @@ export class ComponentLibraryProject extends Project { let manifestValues: Record; // search the outFile for replaceable values such as ${title} - // eslint-disable-next-line no-cond-assign - while (renamingMatch = regexp.exec(this.outFile)) { + while ((renamingMatch = regexp.exec(this.outFile))) { if (!manifestValues) { // The first time a value is found we need to get the manifest values manifestValues = await util.convertManifestToObject(manifestPath); diff --git a/src/util.spec.ts b/src/util.spec.ts index 421e5211..474c73d3 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable camelcase */ import * as assert from 'assert'; import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; diff --git a/src/util.ts b/src/util.ts index 4537ca64..e3f9d512 100644 --- a/src/util.ts +++ b/src/util.ts @@ -194,8 +194,7 @@ class Util { const regexp = /^((.*?)Brightscript\s+Debugger>\s*)(.*?)$/gm; let match: RegExpExecArray; const splitIndexes = [] as number[]; - // eslint-disable-next-line no-cond-assign - while (match = regexp.exec(text)) { + while ((match = regexp.exec(text))) { const leadingAndBeaconText = match[1]; const leadingText = match[2]; const trailingText = match[3]; From e33e26c3e3eb4a82295d05498cf194a7af690238 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 31 May 2022 09:08:14 -0400 Subject: [PATCH 012/197] Line number and thread hopping fixes (#86) * Fix lint issues, remove some disable comments * Use line number from stack frame. Clean up some event emitter stuff * Better thread hop tracking. --- src/adapters/DebugProtocolAdapter.ts | 52 ++++++++------------ src/debugProtocol/Debugger.ts | 46 ++++++----------- src/debugSession/BrightScriptDebugSession.ts | 24 +++++++-- 3 files changed, 56 insertions(+), 66 deletions(-) diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 04c0e796..46d971ab 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -8,7 +8,7 @@ import { RendezvousTracker } from '../RendezvousTracker'; import type { ChanperfData } from '../ChanperfTracker'; import { ChanperfTracker } from '../ChanperfTracker'; import type { SourceLocation } from '../managers/LocationManager'; -import { ERROR_CODES, PROTOCOL_ERROR_CODES } from '../debugProtocol/Constants'; +import { ERROR_CODES, PROTOCOL_ERROR_CODES, STOP_REASONS } from '../debugProtocol/Constants'; import { defer, util } from '../util'; import { logger } from '../logging'; import * as semver from 'semver'; @@ -96,25 +96,9 @@ export class DebugProtocolAdapter { }; } - private emit( - /* eslint-disable */ - eventName: - 'app-exit' | - 'cannot-continue' | - 'chanperf' | - 'close' | - 'compile-errors' | - 'connected' | - 'console-output' | - 'protocol-version' | - 'rendezvous' | - 'runtime-error' | - 'start' | - 'suspend' | - 'unhandled-console-output', - /* eslint-enable */ - data? - ) { + private emit(eventName: 'suspend'); + private emit(eventName: 'app-exit' | 'cannot-continue' | 'chanperf' | 'close' | 'compile-errors' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output', data?); + private emit(eventName: string, data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues setTimeout(() => { //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists @@ -135,16 +119,16 @@ export class DebugProtocolAdapter { */ public isAppRunning = false; - public async activate() { + public activate() { this.isActivated = true; - await this.handleStartupIfReady(); + this.handleStartupIfReady(); } public async sendErrors() { await this.compileErrorProcessor.sendErrors(); } - private async handleStartupIfReady() { + private handleStartupIfReady() { if (this.isActivated && this.isAppRunning) { this.emit('start'); @@ -152,8 +136,7 @@ export class DebugProtocolAdapter { //If not, then there are probably still messages being received, so let the normal handler //emit the suspend event when it's ready if (this.isAtDebuggerPrompt === true) { - let threads = await this.getThreads(); - this.emit('suspend', threads[0].threadId); + this.emit('suspend'); } } } @@ -242,14 +225,14 @@ export class DebugProtocolAdapter { this.socketDebugger.on('suspend', (data) => { this.clearCache(); - this.emit('suspend', data); + this.emit('suspend'); }); this.socketDebugger.on('runtime-error', (data) => { console.debug('hasRuntimeError!!', data); this.emit('runtime-error', { message: data.data.stopReasonDetail, - errorCode: data.data.stopReason + errorCode: STOP_REASONS[data.data.stopReason] }); }); @@ -428,17 +411,22 @@ export class DebugProtocolAdapter { let frameData = stackTraceData.entries[i]; let stackFrame: StackFrame = { frameId: this.nextFrameId++, - frameIndex: stackTraceData.stackSize - i - 1, // frame index is the reverse of the returned order. + // frame index is the reverse of the returned order. + frameIndex: stackTraceData.stackSize - i - 1, threadIndex: threadId, - // eslint-disable-next-line no-nested-ternary - filePath: i === 0 ? (frameData.fileName) ? frameData.fileName : thread.filePath : frameData.fileName, - lineNumber: i === 0 ? thread.lineNumber : frameData.lineNumber, + filePath: frameData.fileName, + lineNumber: frameData.lineNumber, // eslint-disable-next-line no-nested-ternary functionIdentifier: this.cleanUpFunctionName(i === 0 ? (frameData.functionName) ? frameData.functionName : thread.functionName : frameData.functionName) }; this.stackFramesCache[stackFrame.frameId] = stackFrame; frames.push(stackFrame); } + //if the first frame is missing any data, suppliment with thread information + if (frames[0]) { + frames[0].filePath ??= thread.filePath; + frames[0].lineNumber ??= thread.lineNumber; + } return frames; }); @@ -571,7 +559,7 @@ export class DebugProtocolAdapter { } /** - * Get a list of threads. The first thread in the list is the active thread + * Get a list of threads. The active thread will always be first in the list. */ public async getThreads() { if (!this.isAtDebuggerPrompt) { diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 272e9e8b..d5cf5ac3 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -85,7 +85,8 @@ export class Debugger { * Subscribe to various events */ public on(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start', handler: () => void); - public on(eventName: 'data' | 'runtime-error' | 'suspend', handler: (data: any) => void); + public on(eventName: 'data', handler: (data: any) => void); + public on(eventName: 'runtime-error' | 'suspend', handler: (data: UpdateThreadsResponse) => void); public on(eventName: 'connected', handler: (connected: boolean) => void); public on(eventName: 'io-output', handler: (output: string) => void); public on(eventName: 'protocol-version', handler: (data: ProtocolVersionDetails) => void); @@ -99,29 +100,13 @@ export class Debugger { }; } - private emit( - /* eslint-disable @typescript-eslint/indent */ - eventName: - 'app-exit' | - 'cannot-continue' | - 'close' | - 'connected' | - 'data' | - 'handshake-verified' | - 'io-output' | - 'protocol-version' | - 'runtime-error' | - 'start' | - 'suspend', - /* eslint-enable @typescript-eslint/indent */ - data? - ) { + private emit(eventName: 'suspend' | 'runtime-error', data: UpdateThreadsResponse); + private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'connected' | 'data' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'start', data?); + private emit(eventName: string, data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues setTimeout(() => { //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists - if (this.emitter) { - this.emitter.emit(eventName, data); - } + this.emitter?.emit(eventName, data); }, 0); } @@ -233,15 +218,16 @@ export class Debugger { public async threads() { if (this.stopped) { let result = await this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.THREADS); - if (result.errorCode === ERROR_CODES.OK) { - for (let i = 0; i < result.threadsCount; i++) { - let thread = result.threads[i]; - if (thread.isPrimary) { - this.primaryThread = i; - break; - } - } - } + //TODO uncomment this once the device starts correctly reporting `isPrimary`. Right now our logic is better at tracking the primary thread. + // if (result.errorCode === ERROR_CODES.OK) { + // for (let i = 0; i < result.threadsCount; i++) { + // let thread = result.threads[i]; + // if (thread.isPrimary) { + // this.primaryThread = i; + // break; + // } + // } + // } return result; } } diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 7b30fb2f..5f70c603 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -977,12 +977,28 @@ export class BrightScriptDebugSession extends BaseDebugSession { // eslint-disable-next-line @typescript-eslint/no-misused-promises this.rokuAdapter.on('suspend', async () => { this.logger.info('received "suspend" event from adapter'); - let threads = await this.rokuAdapter.getThreads(); - let threadId = threads[0]?.threadId; + const threads = await this.rokuAdapter.getThreads(); + const activeThread = threads.find(x => x.isSelected); + + //TODO remove this once Roku fixes their threads off-by-one line number issues + //look up the correct line numbers for each thread from the StackTrace + await Promise.all( + threads.map(async (thread) => { + const stackTrace = await this.rokuAdapter.getStackTrace(thread.threadId); + const stackTraceLineNumber = stackTrace[0]?.lineNumber; + if (stackTraceLineNumber !== thread.lineNumber) { + this.logger.warn(`Thread ${thread.threadId} reported incorrect line (${thread.lineNumber}). Using line from stack trace instead (${stackTraceLineNumber})`, thread, stackTrace); + thread.lineNumber = stackTraceLineNumber; + } + }) + ); this.clearState(); - let exceptionText = ''; - const event: StoppedEvent = new StoppedEvent(StoppedEventReason.breakpoint, threadId, exceptionText); + const event: StoppedEvent = new StoppedEvent( + StoppedEventReason.breakpoint, + activeThread.threadId, + '' //exception text + ); // Socket debugger will always stop all threads and supports multi thread inspection. (event.body as any).allThreadsStopped = this.enableDebugProtocol; this.sendEvent(event); From 46252774f6775eb26a3255c59b59553ea613e0c5 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 31 May 2022 12:52:50 -0400 Subject: [PATCH 013/197] brighterscript@0.51.3 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7785ba55..2a449016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.50.2", + "brighterscript": "^0.51.3", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.50.2", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.2.tgz", - "integrity": "sha512-PNW25VbP9CKNY1SXJxqxPBsr4M6QdrwfEBdjno3i4HAfOLTCTYExxVO0Xu4xMzwlyFB3tAPV2fnlH+KqRmwocQ==", + "version": "0.51.3", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.51.3.tgz", + "integrity": "sha512-K0LqG9ADnsrX2T6WMyHZFK9r+9f4bjZCWBNlDwYGI4S++SvF9l0JSlSCaVU+iNQE8u5VPu7gRBoA+Qdib3ovng==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5552,9 +5552,9 @@ } }, "brighterscript": { - "version": "0.50.2", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.50.2.tgz", - "integrity": "sha512-PNW25VbP9CKNY1SXJxqxPBsr4M6QdrwfEBdjno3i4HAfOLTCTYExxVO0Xu4xMzwlyFB3tAPV2fnlH+KqRmwocQ==", + "version": "0.51.3", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.51.3.tgz", + "integrity": "sha512-K0LqG9ADnsrX2T6WMyHZFK9r+9f4bjZCWBNlDwYGI4S++SvF9l0JSlSCaVU+iNQE8u5VPu7gRBoA+Qdib3ovng==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", diff --git a/package.json b/package.json index 2939faa1..dbd98fe8 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.50.2", + "brighterscript": "^0.51.3", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From 1741b11a37a9342003538aa22197c259c341bfa0 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 31 May 2022 12:55:14 -0400 Subject: [PATCH 014/197] update changelog for v0.12.2 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b442e4..45b8bac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.12.2](https://github.com/rokucommunity/roku-debug/compare/v0.12.1...v0.12.2) - 2022-05-31 +### Changed + - upgrade to [brighterscript@0.51.3](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0513---2022-05-31) + - upgrade to [roku-deploy@3.7.0](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#370---2022-05-23) +### Fixed + - line number and thread hopping fixes ([#86](https://github.com/rokucommunity/roku-debug/pull/86)) + + + ## [0.12.1](https://github.com/rokucommunity/roku-debug/compare/v0.12.0...v0.12.1) - 2022-05-20 ### Changed - add `launchConfiguration` to the `ChannelPublishedEvent` ([#83](https://github.com/rokucommunity/roku-debug/pull/83)) From 87d07cfe836b6c59424fcb3b3cd9fdb85710087f Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 31 May 2022 12:55:49 -0400 Subject: [PATCH 015/197] 0.12.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a449016..8989333e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.12.1", + "version": "0.12.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.12.1", + "version": "0.12.2", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index dbd98fe8..ea1540c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.12.1", + "version": "0.12.2", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From e9a8e47ecd8035f9416faf6b8d467e250f7c1e9a Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 7 Jun 2022 14:39:43 -0400 Subject: [PATCH 016/197] Adds support for dynamic breakpoints when using Debug Protocol (#84) * Add `getDiff()` breakpointManager function * make `enableDebugProtocol` a getter * Better breakpoint management. * sync breakpoint verification to vscode * Better breakpoint handling * fix unit tests * Mitigate breakpoint sync edge case. Prevent duplicate `getDiff` bp manager calls * Fix lint issues, remove some disable comments * Use line number from stack frame. Clean up some event emitter stuff * Better thread hop tracking. * handle entry breakpoint logic and sending breakpoints to device at launch. * add comments. * Fix stopOnEntry + user breakpoint at entry bug --- src/FileUtils.ts | 7 +- src/adapters/DebugProtocolAdapter.spec.ts | 10 +- src/adapters/DebugProtocolAdapter.ts | 65 ++- src/adapters/TelnetAdapter.ts | 19 + src/debugProtocol/Debugger.ts | 17 +- .../responses/ListBreakpointsResponse.ts | 4 +- .../BrightScriptDebugSession.spec.ts | 105 ++-- src/debugSession/BrightScriptDebugSession.ts | 89 ++-- src/interfaces.ts | 4 + src/managers/ActionQueue.ts | 38 ++ src/managers/BreakpointManager.spec.ts | 397 +++++++++++--- src/managers/BreakpointManager.ts | 497 ++++++++++++++---- src/managers/LocationManager.ts | 2 +- src/managers/ProjectManager.spec.ts | 9 +- src/managers/ProjectManager.ts | 16 +- src/testHelpers.spec.ts | 41 ++ tsconfig.json | 2 +- 17 files changed, 1034 insertions(+), 288 deletions(-) create mode 100644 src/managers/ActionQueue.ts diff --git a/src/FileUtils.ts b/src/FileUtils.ts index 042ed09a..a9b3e40b 100644 --- a/src/FileUtils.ts +++ b/src/FileUtils.ts @@ -232,13 +232,12 @@ export class FileUtils { */ public async findEntryPoint(projectPath: string) { let results = { - + ...await findInFiles.find({ term: 'sub\\s+RunScreenSaver\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), + ...await findInFiles.find({ term: 'function\\s+RunScreenSaver\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), ...await findInFiles.find({ term: 'sub\\s+RunUserInterface\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), ...await findInFiles.find({ term: 'function\\s+RunUserInterface\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), ...await findInFiles.find({ term: 'sub\\s+main\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'function\\s+main\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'sub\\s+RunScreenSaver\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/), - ...await findInFiles.find({ term: 'function\\s+RunScreenSaver\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/) + ...await findInFiles.find({ term: 'function\\s+main\\s*\\(', flags: 'ig' }, projectPath, /.*\.brs/) }; let keys = Object.keys(results); if (keys.length === 0) { diff --git a/src/adapters/DebugProtocolAdapter.spec.ts b/src/adapters/DebugProtocolAdapter.spec.ts index ccce0c05..d723565c 100644 --- a/src/adapters/DebugProtocolAdapter.spec.ts +++ b/src/adapters/DebugProtocolAdapter.spec.ts @@ -13,9 +13,13 @@ describe('DebugProtocolAdapter', () => { let socketDebugger: Debugger; beforeEach(() => { - adapter = new DebugProtocolAdapter({ - host: '127.0.0.1' - }); + adapter = new DebugProtocolAdapter( + { + host: '127.0.0.1' + }, + undefined, + undefined + ); socketDebugger = new Debugger(undefined); adapter['socketDebugger'] = socketDebugger; }); diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 46d971ab..2b8ad6a5 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -13,13 +13,18 @@ import { defer, util } from '../util'; import { logger } from '../logging'; import * as semver from 'semver'; import type { AdapterOptions, HighLevelType, RokuAdapterEvaluateResponse } from '../interfaces'; +import type { BreakpointManager } from '../managers/BreakpointManager'; +import type { ProjectManager } from '../managers/ProjectManager'; +import { ActionQueue } from '../managers/ActionQueue'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. */ export class DebugProtocolAdapter { constructor( - private options: AdapterOptions + private options: AdapterOptions, + private projectManager: ProjectManager, + private breakpointManager: BreakpointManager ) { util.normalizeAdapterOptions(this.options); this.emitter = new EventEmitter(); @@ -170,7 +175,7 @@ export class DebugProtocolAdapter { } public get isAtDebuggerPrompt() { - return this.socketDebugger ? this.socketDebugger.isStopped : false; + return this.socketDebugger?.isStopped ?? false; } /** @@ -653,6 +658,62 @@ export class DebugProtocolAdapter { this.chanperfTracker.clearHistory(); } // #endregion + + public async syncBreakpoints() { + //we can't send breakpoints unless we're stopped. So...if we're not stopped, quit now. (we'll get called again when the stop event happens) + if (!this.isAtDebuggerPrompt) { + return; + } + //compute breakpoint changes since last sync + const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects()); + + //delete these breakpoints + if (diff.removed.length > 0) { + await this.actionQueue.run(async () => { + const response = await this.socketDebugger.removeBreakpoints( + diff.removed.map(x => x.deviceId) + ); + //return true to mark this action as complete, or false to retry the task again in the future + return response.success && response.errorCode === ERROR_CODES.OK; + }); + } + + if (diff.added.length > 0) { + const breakpointsToSendToDevice = diff.added.map(breakpoint => { + const hitCount = parseInt(breakpoint.hitCondition); + return { + filePath: breakpoint.pkgPath, + lineNumber: breakpoint.line, + hitCount: !isNaN(hitCount) ? hitCount : undefined, + key: breakpoint.hash + }; + }); + + //send these new breakpoints to the device + await this.actionQueue.run(async () => { + const response = await this.socketDebugger.addBreakpoints(breakpointsToSendToDevice); + if (response.errorCode === ERROR_CODES.OK) { + //mark the breakpoints as verified + for (let i = 0; i < response.breakpoints.length; i++) { + const deviceBreakpoint = response.breakpoints[i]; + if (deviceBreakpoint.isVerified) { + this.breakpointManager.verifyBreakpoint( + breakpointsToSendToDevice[i].key, + deviceBreakpoint.breakpointId + ); + } + } + //return true to mark this action as complete + return true; + } else { + //this action is not yet complete. it should be retried + return false; + } + }); + } + } + + private actionQueue = new ActionQueue(); } export interface StackFrame { diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index ac320cfb..67a5608b 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -1062,6 +1062,10 @@ export class TelnetAdapter { this.chanperfTracker.clearHistory(); } // #endregion + + public async syncBreakpoints() { + //we can't send dynamic breakpoints to the server...so just do nothing + } } export interface StackFrame { @@ -1095,10 +1099,25 @@ export enum KeyType { } export interface Thread { + /** + * Is this thread selected + */ isSelected: boolean; + /** + * The 1-based line number + */ lineNumber: number; + /** + * The pkgPath to the file on-device + */ filePath: string; + /** + * The contents of the line (i.e. the code for the line) + */ lineContents: string; + /** + * The id of this thread + */ threadId: number; } diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index d5cf5ac3..1f7181bb 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -44,7 +44,6 @@ export class Debugger { this.options = { controllerPort: 8081, host: undefined, - stopOnEntry: false, //override the defaults with the options from parameters ...options ?? {} }; @@ -63,7 +62,6 @@ export class Debugger { private controllerClient: Net.Socket; private ioClient: Net.Socket; private unhandledData: Buffer; - private firstRunContinueFired = false; private stopped = false; private totalRequests = 0; private activeRequests = {}; @@ -356,8 +354,7 @@ export class Debugger { case UPDATE_TYPES.THREAD_ATTACHED: let debuggerUpdateThreads = new UpdateThreadsResponse(slicedBuffer); if (debuggerUpdateThreads.success) { - //TODO should we be awaiting this? - void this.handleThreadsUpdate(debuggerUpdateThreads); + this.handleThreadsUpdate(debuggerUpdateThreads); this.removedProcessedBytes(debuggerUpdateThreads, slicedBuffer, packetLength); return true; } @@ -529,17 +526,13 @@ export class Debugger { return false; } - private async handleThreadsUpdate(update: UpdateThreadsResponse) { + private handleThreadsUpdate(update: UpdateThreadsResponse) { this.stopped = true; let stopReason = update.data.stopReason; let eventName: 'runtime-error' | 'suspend' = stopReason === STOP_REASONS.RUNTIME_ERROR ? 'runtime-error' : 'suspend'; if (update.updateType === UPDATE_TYPES.ALL_THREADS_STOPPED) { - if (!this.firstRunContinueFired && !this.options.stopOnEntry) { - this.logger.log('Sending first run continue command'); - await this.continue(); - this.firstRunContinueFired = true; - } else if (stopReason === STOP_REASONS.RUNTIME_ERROR || stopReason === STOP_REASONS.BREAK || stopReason === STOP_REASONS.STOP_STATEMENT) { + if (stopReason === STOP_REASONS.RUNTIME_ERROR || stopReason === STOP_REASONS.BREAK || stopReason === STOP_REASONS.STOP_STATEMENT) { this.primaryThread = (update.data as ThreadsStopped).primaryThreadIndex; this.stackFrameIndex = 0; this.emit(eventName, update); @@ -597,10 +590,6 @@ export interface ConstructorOptions { * The host/ip address of the Roku */ host: string; - /** - * If true, the application being debugged will stop on the first line of the program. - */ - stopOnEntry?: boolean; /** * The port number used to send all debugger commands. This is static/unchanging for Roku devices, * but is configurable here to support unit testing or alternate runtimes (i.e. https://www.npmjs.com/package/brs) diff --git a/src/debugProtocol/responses/ListBreakpointsResponse.ts b/src/debugProtocol/responses/ListBreakpointsResponse.ts index ee98307f..ca17cd3c 100644 --- a/src/debugProtocol/responses/ListBreakpointsResponse.ts +++ b/src/debugProtocol/responses/ListBreakpointsResponse.ts @@ -12,7 +12,7 @@ export class ListBreakpointsResponse { // Any request id less then one is an update and we should not process it here if (this.requestId > 0) { - this.errorCode = ERROR_CODES[bufferReader.readUInt32LE()]; + this.errorCode = bufferReader.readUInt32LE(); this.numBreakpoints = bufferReader.readUInt32LE(); // num_breakpoints - The number of breakpoints in the breakpoints array. // build the list of BreakpointInfo @@ -38,7 +38,7 @@ export class ListBreakpointsResponse { public numBreakpoints: number; public breakpoints = [] as BreakpointInfo[]; public data = -1; - public errorCode: string; + public errorCode: ERROR_CODES; } export class BreakpointInfo { diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index ca3d106e..f79219ee 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -4,21 +4,24 @@ import * as fsExtra from 'fs-extra'; import * as path from 'path'; import * as sinonActual from 'sinon'; import type { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { DebugSession } from 'vscode-debugadapter'; +import { Breakpoint, DebugSession } from 'vscode-debugadapter'; import { BrightScriptDebugSession } from './BrightScriptDebugSession'; import { fileUtils } from '../FileUtils'; -import type { EvaluateContainer } from '../adapters/TelnetAdapter'; +import type { EvaluateContainer, TelnetAdapter } from '../adapters/TelnetAdapter'; import { PrimativeType } from '../adapters/TelnetAdapter'; import { defer } from '../util'; import { HighLevelType } from '../interfaces'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { SinonStub } from 'sinon'; +import { standardizePath as s } from 'brighterscript'; +import { DefaultFiles } from 'roku-deploy'; +import type { DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter'; let sinon = sinonActual.createSandbox(); -let cwd = fileUtils.standardizePath(process.cwd()); -let outDir = fileUtils.standardizePath(`${cwd}/outDir`); -let stagingFolderPath = fileUtils.standardizePath(`${outDir}/stagingDir`); -const rootDir = path.normalize(path.dirname(__dirname)); +const tempDir = s`${process.cwd()}/.tmp`; +const rootDir = s`${tempDir}/rootDir`; +const outDir = s`${tempDir}/out`; +const stagingFolderPath = s`${tempDir}/stagingDir`; describe('BrightScriptDebugSession', () => { let responseDeferreds = []; @@ -34,15 +37,8 @@ describe('BrightScriptDebugSession', () => { let launchConfiguration: LaunchConfiguration; let initRequestArgs: DebugProtocol.InitializeRequestArguments; - let rokuAdapter: any = { - on: () => { - return () => { - }; - }, - activate: () => Promise.resolve(), - registerSourceLocator: (a, b) => { }, - setConsoleOutput: (a) => { } - }; + let rokuAdapter: ReturnType; + beforeEach(() => { sinon.restore(); @@ -55,10 +51,18 @@ describe('BrightScriptDebugSession', () => { (session as any).sendErrorResponse = (...args: string[]) => { throw new Error(args[2]); }; - launchConfiguration = {} as any; + launchConfiguration = { + rootDir: rootDir, + outDir: outDir, + stagingFolderPath: stagingFolderPath, + files: DefaultFiles + } as any; session['launchConfiguration'] = launchConfiguration; + session.projectManager.launchConfiguration = launchConfiguration; + session.breakpointManager.launchConfiguration = launchConfiguration; initRequestArgs = {} as any; session['initRequestArgs'] = initRequestArgs; + //mock the rokuDeploy module with promises so we can have predictable tests session.rokuDeploy = { prepublishToStaging: () => { @@ -84,17 +88,7 @@ describe('BrightScriptDebugSession', () => { getFilePaths: () => { } }; - rokuAdapter = { - on: () => { - return () => { - }; - }, - activate: () => Promise.resolve(), - registerSourceLocator: (a, b) => { }, - setConsoleOutput: (a) => { }, - evaluate: () => { }, - getVariable: () => { } - }; + rokuAdapter = createRokuAdapter(); (session as any).rokuAdapter = rokuAdapter; //mock the roku adapter (session as any).connectRokuAdapter = () => { @@ -122,6 +116,22 @@ describe('BrightScriptDebugSession', () => { }); }); + function createRokuAdapter() { + return { + on: () => { + return () => { + }; + }, + activate: () => Promise.resolve(), + registerSourceLocator: (a, b) => { }, + setConsoleOutput: (a) => { }, + evaluate: (() => { }) as (...args) => any, + syncBreakpoints: () => { }, + getVariable: (() => Promise.resolve()) as () => any, + isAtDebuggerPrompt: false + }; + } + describe('initializeRequest', () => { it('does not throw', () => { assert.doesNotThrow(() => { @@ -340,7 +350,7 @@ describe('BrightScriptDebugSession', () => { describe('setBreakPointsRequest', () => { let response; - let args; + let args: DebugProtocol.SetBreakpointsArguments; beforeEach(() => { response = undefined; //intercept the sent response @@ -350,31 +360,35 @@ describe('BrightScriptDebugSession', () => { args = { source: { - path: path.normalize(`${rootDir}/dest/some/file.brs`) + path: s`${rootDir}/dest/some/file.brs` }, breakpoints: [] }; }); - it('returns correct results', () => { + it('returns correct results', async () => { + args.source.path = s`${rootDir}/source/main.brs`; + + fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); + fsExtra.outputFileSync(s`${rootDir}/source/main.brs`, 'sub main()\nend sub'); args.breakpoints = [{ line: 1 }]; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect(response.body.breakpoints[0]).to.deep.include({ line: 1, - verified: true + verified: false }); - //mark debugger as 'launched' which should change the behavior of breakpoints. - session.breakpointManager.lockBreakpoints(); + //simulate "launch" + await session.prepareMainProject(); - //remove the breakpoint breakpoint (it should not remove the breakpoint because it was already verified) + //remove the breakpoint args.breakpoints = []; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect(response.body.breakpoints).to.be.lengthOf(0); - //add breakpoint during live debug session. one was there before, the other is new. Only one will be verified + //add breakpoint during live debug session. one was there before, the other is new. Neither will be verified right now args.breakpoints = [{ line: 1 }, { line: 2 }]; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect( response.body.breakpoints.map(x => ({ line: x.line, verified: x.verified })) ).to.eql([{ @@ -386,18 +400,21 @@ describe('BrightScriptDebugSession', () => { }]); }); - it('supports breakpoints within xml files', () => { + it('supports breakpoints within xml files', async () => { args.source.path = `${rootDir}/some/xml-file.xml`; args.breakpoints = [{ line: 1 }]; - session.setBreakPointsRequest({}, args); - //breakpoint should be disabled - expect(response.body.breakpoints[0]).to.deep.include({ line: 1, verified: true }); + await session.setBreakPointsRequest({}, args); + //breakpoint should be unverified by default + expect(response.body.breakpoints[0]).to.deep.include({ + line: 1, + verified: false + }); }); - it('handles breakpoints for non-brightscript files', () => { + it('handles breakpoints for non-brightscript files', async () => { args.source.path = `${rootDir}/some/xml-file.jpg`; args.breakpoints = [{ line: 1 }]; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect(response.body.breakpoints).to.be.lengthOf(1); //breakpoint should be disabled expect(response.body.breakpoints[0]).to.deep.include({ line: 1, verified: false }); diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index cf7e0d81..85845b3c 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -5,6 +5,7 @@ import * as request from 'request'; import { rokuDeploy } from 'roku-deploy'; import type { RokuDeploy, RokuDeployOptions } from 'roku-deploy'; import { + BreakpointEvent, DebugSession as BaseDebugSession, Handles, InitializedEvent, @@ -43,6 +44,7 @@ import type { LaunchConfiguration, ComponentLibraryConfiguration } from '../Laun import { FileManager } from '../managers/FileManager'; import { SourceMapManager } from '../managers/SourceMapManager'; import { LocationManager } from '../managers/LocationManager'; +import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager'; import { BreakpointManager } from '../managers/BreakpointManager'; import type { LogMessage } from '../logging'; import { logger, debugServerLogOutputEventTransport } from '../logging'; @@ -61,9 +63,28 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.sourceMapManager = new SourceMapManager(); this.locationManager = new LocationManager(this.sourceMapManager); this.breakpointManager = new BreakpointManager(this.sourceMapManager, this.locationManager); + //send newly-verified breakpoints to vscode + this.breakpointManager.on('breakpoints-verified', (data) => this.onDeviceVerifiedBreakpoints(data)); this.projectManager = new ProjectManager(this.breakpointManager, this.locationManager); } + private onDeviceVerifiedBreakpoints(data: { breakpoints: AugmentedSourceBreakpoint[] }) { + this.logger.info('Sending verified device breakpoints to client', data); + //send all verified breakpoints to the client + for (const breakpoint of data.breakpoints) { + const event: DebugProtocol.Breakpoint = { + line: breakpoint.line, + column: breakpoint.column, + verified: true, + id: breakpoint.id, + source: { + path: breakpoint.srcPath + } + }; + this.sendEvent(new BreakpointEvent('changed', event)); + } + } + public logger = logger.createLogger(`[${BrightScriptDebugSession.name}]`); /** @@ -100,7 +121,9 @@ export class BrightScriptDebugSession extends BaseDebugSession { private variableHandles = new Handles(); private rokuAdapter: DebugProtocolAdapter | TelnetAdapter; - private enableDebugProtocol: boolean; + private get enableDebugProtocol() { + return this.launchConfiguration.enableDebugProtocol; + } private getRokuAdapter() { return this.rokuAdapterDeferred.promise; @@ -166,8 +189,6 @@ export class BrightScriptDebugSession extends BaseDebugSession { logger.logLevel = this.launchConfiguration.logLevel; } - this.enableDebugProtocol = this.launchConfiguration.enableDebugProtocol; - //do a DNS lookup for the host to fix issues with roku rejecting ECP this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host); @@ -450,11 +471,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { //add breakpoint lines to source files and then publish util.log('Adding stop statements for active breakpoints'); - //prevent new breakpoints from being verified - this.breakpointManager.lockBreakpoints(); + //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol) + if (!this.enableDebugProtocol) { - //write all `stop` statements to the files in the staging folder - await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject); + await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject); + } //create zip package from staging folder util.log('Creating zip archive from project sources'); @@ -512,8 +533,10 @@ export class BrightScriptDebugSession extends BaseDebugSession { // Add breakpoint lines to the staging files and before publishing util.log('Adding stop statements for active breakpoints in Component Libraries'); - //write the `stop` statements to every file that has breakpoints - await this.breakpointManager.writeBreakpointsForProject(compLibProject); + //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol) + if (!this.enableDebugProtocol) { + await this.breakpointManager.writeBreakpointsForProject(compLibProject); + } await compLibProject.postfixFiles(); @@ -553,32 +576,17 @@ export class BrightScriptDebugSession extends BaseDebugSession { /** * Called every time a breakpoint is created, modified, or deleted, for each file. This receives the entire list of breakpoints every time. */ - public setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { + public async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { let sanitizedBreakpoints = this.breakpointManager.replaceBreakpoints(args.source.path, args.breakpoints); //sort the breakpoints - let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]) - //filter out the inactive breakpoints - .filter(x => x.isHidden === false); + let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]); response.body = { breakpoints: sortedAndFilteredBreakpoints }; this.sendResponse(response); - //set a small timeout so the user sees the breakpoints disappear before reappearing - //This is disabled because I'm not sure anyone actually wants this functionality, but I didn't want to lose it. - // setTimeout(() => { - // //notify the client about every other breakpoint that was not explicitly requested here - // //(basically force to re-enable the `stop` breakpoints that were written into the source code by the debugger) - // var otherBreakpoints = sanitizedBreakpoints.filter(x => sortedAndFilteredBreakpoints.indexOf(x) === -1); - // for (var breakpoint of otherBreakpoints) { - // this.sendEvent(new BreakpointEvent('new', { - // line: breakpoint.line, - // verified: true, - // source: args.source - // })); - // } - // }, 100); + await this.rokuAdapter?.syncBreakpoints(); } protected exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, args: DebugProtocol.ExceptionInfoArguments) { @@ -945,7 +953,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { private createRokuAdapter(host: string) { if (this.enableDebugProtocol) { - this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration); + this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration, this.projectManager, this.breakpointManager); } else { this.rokuAdapter = new TelnetAdapter(this.launchConfiguration); } @@ -963,6 +971,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { await this.launchRequest(response, args.arguments as LaunchConfiguration); } + /** + * Used to track whether the entry breakpoint has already been handled + */ + private entryBreakpointWasHandled = false; + /** * Registers the main events for the RokuAdapter */ @@ -976,7 +989,10 @@ export class BrightScriptDebugSession extends BaseDebugSession { //when the debugger suspends (pauses for debugger input) // eslint-disable-next-line @typescript-eslint/no-misused-promises this.rokuAdapter.on('suspend', async () => { + //sync breakpoints + await this.rokuAdapter?.syncBreakpoints(); this.logger.info('received "suspend" event from adapter'); + const threads = await this.rokuAdapter.getThreads(); const activeThread = threads.find(x => x.isSelected); @@ -993,6 +1009,16 @@ export class BrightScriptDebugSession extends BaseDebugSession { }) ); + //if !stopOnEntry, and we haven't encountered a suspend yet, THIS is the entry breakpoint. auto-continue + if (!this.entryBreakpointWasHandled && !this.launchConfiguration.stopOnEntry) { + this.entryBreakpointWasHandled = true; + //if there's a user-defined breakpoint at this exact position, it needs to be handled like a regular breakpoint (i.e. suspend). So only auto-continue if there's no breakpoint here + if (!await this.breakpointManager.lineHasBreakpoint(this.projectManager.getAllProjects(), activeThread.filePath, activeThread.lineNumber - 1)) { + this.logger.info('Encountered entry breakpoint and `stopOnEntry` is disabled. Continuing...'); + return this.rokuAdapter.continue(); + } + } + this.clearState(); const event: StoppedEvent = new StoppedEvent( StoppedEventReason.breakpoint, @@ -1109,8 +1135,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { * If `stopOnEntry` is enabled, register the entry breakpoint. */ public async handleEntryBreakpoint() { - if (this.launchConfiguration.stopOnEntry && !this.enableDebugProtocol) { - await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingFolderPath); + if (!this.enableDebugProtocol) { + this.entryBreakpointWasHandled = true; + if (this.launchConfiguration.stopOnEntry) { + await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingFolderPath); + } } } diff --git a/src/interfaces.ts b/src/interfaces.ts index 6ad7666b..7505dca4 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -19,4 +19,8 @@ export interface AdapterOptions { host: string; brightScriptConsolePort?: number; remotePort?: number; + /** + * If true, the application being debugged will stop on the first line of the program. + */ + stopOnEntry?: boolean; } diff --git a/src/managers/ActionQueue.ts b/src/managers/ActionQueue.ts new file mode 100644 index 00000000..e716c298 --- /dev/null +++ b/src/managers/ActionQueue.ts @@ -0,0 +1,38 @@ +import type { Deferred } from '../util'; +import { defer } from '../util'; + +/** + * A runner that will keep retrying an action until it succeeds, while also queueing up future actions. + * Will run until all pending actions are complete. + */ +export class ActionQueue { + + private queueItems: Array<{ + action: () => Promise; + deferred: Deferred; + }> = []; + + public async run(action: () => Promise) { + this.queueItems.push({ + action: action, + deferred: defer() + }); + await this._runActions(); + } + + private async _runActions() { + while (this.queueItems.length > 0) { + const queueItem = this.queueItems[0]; + try { + const isFinished = await queueItem.action(); + if (isFinished) { + this.queueItems.shift(); + queueItem.deferred.resolve(); + } + } catch (error) { + this.queueItems.shift(); + queueItem.deferred.reject(error); + } + } + } +} diff --git a/src/managers/BreakpointManager.spec.ts b/src/managers/BreakpointManager.spec.ts index ac3caa27..616e6be5 100644 --- a/src/managers/BreakpointManager.spec.ts +++ b/src/managers/BreakpointManager.spec.ts @@ -1,14 +1,15 @@ import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; import { SourceMapConsumer, SourceNode } from 'source-map'; - +import type { BreakpointWorkItem } from './BreakpointManager'; import { BreakpointManager } from './BreakpointManager'; import { fileUtils, standardizePath as s } from '../FileUtils'; -import { Project } from './ProjectManager'; +import { ComponentLibraryProject, Project, ProjectManager } from './ProjectManager'; let n = fileUtils.standardizePath.bind(fileUtils); import type { SourceLocation } from '../managers/LocationManager'; import { LocationManager } from '../managers/LocationManager'; import { SourceMapManager } from './SourceMapManager'; +import { expectPickEquals, pickArray } from '../testHelpers.spec'; describe('BreakpointManager', () => { let cwd = fileUtils.standardizePath(process.cwd()); @@ -19,14 +20,16 @@ describe('BreakpointManager', () => { let distDir = s`${tmpDir}/dist`; let srcDir = s`${tmpDir}/src`; let outDir = s`${tmpDir}/out`; + const srcPath = s`${rootDir}/source/main.brs`; + const complib1RootDir = s`${tmpDir}/complib1/rootDir`; + const complib1OutDir = s`${tmpDir}/complib1/outDir`; let bpManager: BreakpointManager; let locationManager: LocationManager; let sourceMapManager: SourceMapManager; - //cast the manager as any to simplify some of the tests - let b: any; + let projectManager: ProjectManager; + beforeEach(() => { - fsExtra.ensureDirSync(tmpDir); fsExtra.emptyDirSync(tmpDir); fsExtra.ensureDirSync(`${rootDir}/source`); fsExtra.ensureDirSync(`${stagingDir}/source`); @@ -37,7 +40,21 @@ describe('BreakpointManager', () => { sourceMapManager = new SourceMapManager(); locationManager = new LocationManager(sourceMapManager); bpManager = new BreakpointManager(sourceMapManager, locationManager); - b = bpManager; + projectManager = new ProjectManager(bpManager, locationManager); + projectManager.mainProject = new Project({ + rootDir: rootDir, + files: [], + outDir: s`${outDir}/mainProject` + }); + projectManager.addComponentLibraryProject( + new ComponentLibraryProject({ + rootDir: complib1RootDir, + files: [], + libraryIndex: 0, + outDir: complib1OutDir, + outFile: s`${complib1OutDir}/complib1.zip` + }) + ); }); afterEach(() => { @@ -49,7 +66,7 @@ describe('BreakpointManager', () => { expect(bpManager.sanitizeSourceFilePath('a/b/c')).to.equal(s`a/b/c`); }); it('returns the the found key when it already exists', () => { - b.breakpointsByFilePath[s`A/B/C`] = []; + bpManager['breakpointsByFilePath'].set(s`A/B/C`, []); expect(bpManager.sanitizeSourceFilePath('a/b/c')).to.equal(s`A/B/C`); }); }); @@ -177,32 +194,8 @@ describe('BreakpointManager', () => { }); }); - describe('setBreakpointsForFile', () => { - it('verifies all breakpoints before launch', () => { - let breakpoints = bpManager.replaceBreakpoints(n(`${cwd}/file.brs`), [{ - line: 0, - column: 0 - }, { - line: 1, - column: 0 - }]); - expect(breakpoints).to.be.lengthOf(2); - expect(breakpoints[0]).to.include({ - line: 0, - column: 0, - verified: true, - wasAddedBeforeLaunch: true - }); - expect(breakpoints[1]).to.include({ - line: 1, - column: 0, - verified: true, - wasAddedBeforeLaunch: true - }); - }); - + describe('replaceBreakpoints', () => { it('does not verify breakpoints after launch', () => { - bpManager.lockBreakpoints(); let breakpoints = bpManager.replaceBreakpoints(n(`${cwd}/file.brs`), [{ line: 0, column: 0 @@ -211,50 +204,67 @@ describe('BreakpointManager', () => { expect(breakpoints[0]).to.deep.include({ line: 0, column: 0, - verified: false, - wasAddedBeforeLaunch: false + verified: false }); }); - it('re-verifies breakpoint after launch toggle', () => { + it('re-verifies breakpoint after launch toggle', async () => { //set the breakpoint before launch - let breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + let breakpoints = bpManager.replaceBreakpoints(s`${rootDir}/file.brs`, [{ line: 2 }]); expect(breakpoints).to.be.lengthOf(1); expect(breakpoints[0]).to.deep.include({ line: 2, column: 0, - verified: true, - isHidden: false + verified: false }); - //launch - bpManager.lockBreakpoints(); + //write the breakpoints to the files + await projectManager.breakpointManager.writeBreakpointsForProject(projectManager.mainProject); - //simulate user deleting all breakpoints - breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, []); - - expect(breakpoints).to.be.lengthOf(1); expect(breakpoints[0]).to.deep.include({ line: 2, - verified: true, - isHidden: true + column: 0, + verified: true }); + //simulate user deleting all breakpoints + breakpoints = bpManager.replaceBreakpoints(s`${rootDir}/file.brs`, []); + + expect(breakpoints).to.be.lengthOf(0); + //simulate user adding a breakpoint to the same place it had been before - breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + breakpoints = bpManager.replaceBreakpoints(s`${rootDir}/file.brs`, [{ line: 2 }]); expect(breakpoints).to.be.lengthOf(1); expect(breakpoints[0]).to.deep.include({ line: 2, column: 0, - verified: true, - wasAddedBeforeLaunch: true, - isHidden: false + verified: true }); }); + + it('retains breakpoint data for breakpoints that did not change', () => { + //set the breakpoint before launch + let breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + line: 2 + }, { + line: 4, + condition: 'true' + }]).map(x => ({ ...x })); + + const replacedBreakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + line: 2 + }, { + line: 4, + condition: 'true' + }]).map(x => ({ ...x })); + + //the breakpoints should be identical + expect(breakpoints).to.eql(replacedBreakpoints); + }); }); describe('writeBreakpointsForProject', () => { @@ -289,9 +299,6 @@ describe('BreakpointManager', () => { //copy the file to staging fsExtra.copyFileSync(`${rootDir}/source/main.brs`, `${stagingDir}/source/main.brs`); - //launch - bpManager.lockBreakpoints(); - //file was copied to staging expect(fsExtra.pathExistsSync(`${stagingDir}/source/main.brs`)).to.be.true; //sourcemap was not yet created @@ -333,9 +340,6 @@ describe('BreakpointManager', () => { column: 0 }]); - //launch - bpManager.lockBreakpoints(); - //sourcemap was not yet created expect(fsExtra.pathExistsSync(`${stagingDir}/source/main.brs.map`)).to.be.false; @@ -377,9 +381,6 @@ describe('BreakpointManager', () => { column: 0 }]); - //launch - bpManager.lockBreakpoints(); - await bpManager.writeBreakpointsForProject( new Project({ rootDir: rootDir, @@ -423,9 +424,6 @@ describe('BreakpointManager', () => { hitCondition: '3' }]); - //launch - bpManager.lockBreakpoints(); - await bpManager.writeBreakpointsForProject( new Project({ rootDir: rootDir, @@ -468,9 +466,6 @@ describe('BreakpointManager', () => { column: 0 }]); - //launch - bpManager.lockBreakpoints(); - await bpManager.writeBreakpointsForProject( new Project({ rootDir: rootDir, @@ -520,10 +515,10 @@ describe('BreakpointManager', () => { fsExtra.writeFileSync(s`${stagingDir}/main.brs.map`, result.map.toString()); //set a few breakpoints in the source files - bpManager.registerBreakpoint(src, { + bpManager.setBreakpoint(src, { line: 5 }); - bpManager.registerBreakpoint(src, { + bpManager.setBreakpoint(src, { line: 7 }); @@ -629,7 +624,7 @@ describe('BreakpointManager', () => { column: 4 }); - bpManager.registerBreakpoint(sourceFilePath, { + bpManager.setBreakpoint(sourceFilePath, { line: 3, column: 0 }); @@ -690,7 +685,7 @@ describe('BreakpointManager', () => { ]); //write breakpoints - bpManager.registerBreakpoint(sourceFilePath, { + bpManager.setBreakpoint(sourceFilePath, { line: 4, column: 0 }); @@ -724,7 +719,7 @@ describe('BreakpointManager', () => { `); //write breakpoints - bpManager.registerBreakpoint(baseFilePath, { + bpManager.setBreakpoint(baseFilePath, { line: 2, column: 0 }); @@ -750,4 +745,266 @@ describe('BreakpointManager', () => { baseFilePath ]); }); + + it('adds breakpoint keys', () => { + expect( + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2 + }, { + line: 3, + condition: 'true' + }, { + line: 4, + hitCondition: '2' + }, { + line: 5, + column: 12 + }, { + line: 6, + logMessage: 'hello world' + }]).map(x => x.hash).sort() + ).to.eql([ + s`${rootDir}/source/main.brs:2:0-standard`, + s`${rootDir}/source/main.brs:3:0-condition=true`, + s`${rootDir}/source/main.brs:4:0-hitCondition=2`, + s`${rootDir}/source/main.brs:5:12-standard`, + s`${rootDir}/source/main.brs:6:0-logMessage=hello world` + ].sort()); + }); + + it('does not duplicate breakpoints that have the same key', () => { + const pkgPath = s`${rootDir}/source/main.brs`; + bpManager.setBreakpoint(pkgPath, { + line: 2 + }); + bpManager.setBreakpoint(pkgPath, { + line: 2 + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + ).to.eql([ + s`${pkgPath}:2:0-standard` + ]); + }); + + it('replaces breakpoints with distinct attributes', () => { + const pkgPath = s`${rootDir}/source/main.brs`; + + bpManager.setBreakpoint(pkgPath, { + line: 2 + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + ).to.eql([ + s`${pkgPath}:2:0-standard` + ]); + + bpManager.setBreakpoint(pkgPath, { + line: 2, + condition: 'true' + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + ).to.eql([ + s`${pkgPath}:2:0-condition=true` + ]); + + bpManager.setBreakpoint(pkgPath, { + line: 2, + hitCondition: '4' + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + ).to.eql([ + s`${pkgPath}:2:0-hitCondition=4` + ]); + }); + + it('keeps breakpoints verified if they did not change', () => { + let breakpoints = bpManager.replaceBreakpoints(srcPath, [{ + line: 10 + }]); + //mark this breakpoint as verified + breakpoints[0].verified = true; + + breakpoints = bpManager.replaceBreakpoints(srcPath, [{ + line: 10 + }, { + line: 11 + }]); + + expectPickEquals(breakpoints, [{ + line: 10, + verified: true + }, { + line: 11, + verified: false + }]); + }); + + describe('getDiff', () => { + async function testDiffEquals( + expected?: { + added?: Array>; + removed?: Array>; + unchanged?: Array>; + }, + projects = [projectManager.mainProject, ...projectManager.componentLibraryProjects] + ) { + const diff = await bpManager.getDiff(projects); + //filter the result by the list of properties from each test value + expected = { + added: [], + removed: [], + unchanged: [], + ...expected ?? {} + }; + const actual = { + added: pickArray(diff.added, expected.added), + removed: pickArray(diff.removed, expected.removed), + unchanged: pickArray(diff.unchanged, expected.unchanged) + }; + + expect(actual).to.eql(expected); + } + + it('returns empty diff when no projects are present', async () => { + await testDiffEquals({ added: [], removed: [], unchanged: [] }, []); + }); + + it('returns empty diff when no breakpoints are registered', async () => { + await testDiffEquals({ added: [], removed: [], unchanged: [] }); + }); + + it('handles breakpoint flow', async () => { + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2 + }]); + //breakpoint should show up first time + await testDiffEquals({ + added: [{ + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //should show as "unchanged" now + await testDiffEquals({ + unchanged: [{ + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //remove the breakpoint + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, []); + + //breakpoint should be in the "removed" bucket + await testDiffEquals({ + removed: [{ + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //there should be no breakpoint changes + await testDiffEquals(); + }); + + it('detects hitCount change', async () => { + //add breakpoint with hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + hitCondition: '2' + }]); + + await testDiffEquals({ + added: [{ + line: 2, + hitCondition: '2' + }] + }); + + //change the breakpoint hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + hitCondition: '1' + }]); + + await testDiffEquals({ + removed: [{ + line: 2, + hitCondition: '2' + }], + added: [{ + line: 2, + hitCondition: '1' + }] + }); + }); + + it('detects column number change (roku does not support this yet, but we might as well...)', async () => { + //add breakpoint with hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 4 + }]); + + await testDiffEquals({ + added: [{ + line: 2, + column: 4 + }] + }); + + //change the breakpoint hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 8 + }]); + + await testDiffEquals({ + removed: [{ + line: 2, + column: 4 + }], + added: [{ + line: 2, + column: 8 + }] + }); + }); + + it('maintains breakpoint IDs', async () => { + //add breakpoint with hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 4 + }]); + + await testDiffEquals({ + added: [{ + line: 2, + column: 4 + }] + }); + + //change the breakpoint hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 8 + }]); + + await testDiffEquals({ + removed: [{ + line: 2, + column: 4 + }], + added: [{ + line: 2, + column: 8 + }] + }); + }); + }); }); diff --git a/src/managers/BreakpointManager.ts b/src/managers/BreakpointManager.ts index fe2e438f..0c1c2ac7 100644 --- a/src/managers/BreakpointManager.ts +++ b/src/managers/BreakpointManager.ts @@ -3,12 +3,14 @@ import { orderBy } from 'natural-orderby'; import type { CodeWithSourceMap } from 'source-map'; import { SourceNode } from 'source-map'; import type { DebugProtocol } from 'vscode-debugprotocol'; -import { fileUtils } from '../FileUtils'; +import { fileUtils, standardizePath } from '../FileUtils'; import type { Project } from './ProjectManager'; import { standardizePath as s } from 'roku-deploy'; import type { SourceMapManager } from './SourceMapManager'; import type { LocationManager } from './LocationManager'; import { util } from '../util'; +import { nextTick } from 'process'; +import { EventEmitter } from 'eventemitter3'; export class BreakpointManager { @@ -25,19 +27,23 @@ export class BreakpointManager { enableSourceMaps?: boolean; }; - /** - * Tell the breakpoint manager that no new breakpoints can be verified - * (most likely due to the app being launched and roku not supporting dynamic breakpoints) - */ - public lockBreakpoints() { - this.areBreakpointsLocked = true; + private emitter = new EventEmitter(); + + private emit(eventName: 'breakpoints-verified', data: { breakpoints: AugmentedSourceBreakpoint[] }); + private emit(eventName: string, data: any) { + this.emitter.emit(eventName, data); } /** - * Indicates whether the app has been launched or not. - * This will determine whether the breakpoints should be written to the files, or marked as not verified (greyed out in vscode) + * Subscribe to an event */ - private areBreakpointsLocked = false; + public on(eventName: 'breakpoints-verified', handler: (data: { breakpoints: AugmentedSourceBreakpoint[] }) => any); + public on(eventName: string, handler: (data: any) => any) { + this.emitter.on(eventName, handler); + return () => { + this.emitter.off(eventName, handler); + }; + } /** * A map of breakpoints by what file they were set in. @@ -45,107 +51,179 @@ export class BreakpointManager { * These breakpoints are all set before launch, and then this list is not changed again after that. * (this concept may need to be modified once we get live breakpoint support) */ - private breakpointsByFilePath = {} as Record; + private breakpointsByFilePath = new Map(); - public static breakpointIdSequence = 1; + /** + * A sequence used to generate unique client breakpoint IDs + */ + private breakpointIdSequence = 1; /** * breakpoint lines are 1-based, and columns are zero-based */ - public registerBreakpoint(sourceFilePath: string, breakpoint: AugmentedSourceBreakpoint | DebugProtocol.SourceBreakpoint) { - sourceFilePath = this.sanitizeSourceFilePath(sourceFilePath); - //get the breakpoints array (and optionally initialize it if not set) - let breakpointsArray = this.breakpointsByFilePath[sourceFilePath] ?? []; - this.breakpointsByFilePath[sourceFilePath] = breakpointsArray; - - let existingBreakpoint = breakpointsArray.find(x => x.line === breakpoint.line); + public setBreakpoint(srcPath: string, breakpoint: AugmentedSourceBreakpoint | DebugProtocol.SourceBreakpoint) { + srcPath = this.sanitizeSourceFilePath(srcPath); + + //if a breakpoint gets set in rootDir, and we have sourceDirs, convert the rootDir path to sourceDirs path + //so the breakpoint gets moved into the source file instead of the output file + if (this.launchConfiguration?.sourceDirs && this.launchConfiguration.sourceDirs.length > 0) { + let lastWorkingPath = ''; + for (const sourceDir of this.launchConfiguration.sourceDirs) { + srcPath = srcPath.replace(this.launchConfiguration.rootDir, sourceDir); + if (fsExtra.pathExistsSync(srcPath)) { + lastWorkingPath = srcPath; + } + } + srcPath = this.sanitizeSourceFilePath(lastWorkingPath); + } - let bp = Object.assign(existingBreakpoint || {}, breakpoint); + //get the breakpoints array (and optionally initialize it if not set) + let breakpointsArray = this.getBreakpointsForFile(srcPath, true); + + //only a single breakpoint can be defined per line. So, if we find one on this line, we'll augment that breakpoint rather than builiding a new one + const existingBreakpoint = breakpointsArray.find(x => x.line === breakpoint.line); + + let bp = Object.assign(existingBreakpoint ?? {}, { + //remove common attributes from any existing breakpoint so we don't end up with more info than we need + ...{ + //default to 0 if the breakpoint is missing `column` + column: 0, + condition: undefined, + hitCondition: undefined, + logMessage: undefined + }, + ...breakpoint, + srcPath: srcPath, + //assign a hash-like key to this breakpoint (so we can match against other similar breakpoints in the future) + hash: this.getBreakpointKey(srcPath, breakpoint) + }) as AugmentedSourceBreakpoint; + + //generate a new id for this breakpoint if one does not exist + bp.id ??= this.breakpointIdSequence++; + + //all breakpoints default to false if not already set to true + bp.verified ??= false; + + //if the breakpoint hash changed, mark the breakpoint as unverified + if (existingBreakpoint?.hash !== bp.hash) { + bp.verified = false; + } - //set column=0 if the breakpoint is missing that field - bp.column = bp.column ?? 0; + //if this is a new breakpoint, add it to the list. (otherwise, the existing breakpoint is edited in-place) + if (!existingBreakpoint) { + breakpointsArray.push(bp); + } - bp.wasAddedBeforeLaunch = bp.wasAddedBeforeLaunch ?? this.areBreakpointsLocked === false; + //if this is one of the permanent breakpoints, mark it as verified immediately (only applicable to telnet sessions) + if (this.getPermanentBreakpoint(bp.hash)) { + this.verifyBreakpoint(bp.hash, bp.id); + } + return bp; + } - //set an id if one does not already exist (used for pushing breakpoints to the client) - bp.id = bp.id ?? BreakpointManager.breakpointIdSequence++; + /** + * Find a breakpoint by its hash + * @returns the breakpoint, or undefined if not found + */ + private getBreakpointByHash(hash: string) { + return this.getBreakpointsByHashes([hash])[0]; + } - //any breakpoint set in this function is not hidden - bp.isHidden = false; + /** + * Find a list of breakpoints by their hashes + * @returns the breakpoint, or undefined if not found + */ + private getBreakpointsByHashes(hashes: string[]) { + const result = [] as AugmentedSourceBreakpoint[]; + for (const [, breakpoints] of this.breakpointsByFilePath) { + for (const breakpoint of breakpoints) { + if (hashes.includes(breakpoint.hash)) { + result.push(breakpoint); + } + } + } + return result; + } - //mark non-supported breakpoint as NOT verified, since we don't support debugging non-brightscript files - if (!fileUtils.hasAnyExtension(sourceFilePath, ['.brs', '.bs', '.xml'])) { - bp.verified = false; + /** + * Mark this breakpoint as verified + */ + public verifyBreakpoint(hash: string, deviceId: number) { + const breakpoint = this.getBreakpointByHash(hash); + if (breakpoint) { + breakpoint.verified = true; + breakpoint.deviceId = deviceId; + } + this.queueVerifyEvent(hash); + } - //debug session is not launched yet, all of these breakpoints are treated as verified - } else if (this.areBreakpointsLocked === false) { - //confirm that breakpoint is at a valid location. TODO figure out how to determine valid locations... - bp.verified = true; + /** + * Whenever breakpoints get verified, they need to be synced back to vscode. + * This queues up a future function that will emit a batch of all verified breakpoints. + * @param hash the breakpoint hash that identifies this specific breakpoint based on its features + */ + private queueVerifyEvent(hash: string) { + this.verifiedBreakpointKeys.push(hash); + if (!this.isVerifyEventQueued) { + this.isVerifyEventQueued = true; + + process.nextTick(() => { + this.isVerifyEventQueued = false; + const breakpoints = this.getBreakpointsByHashes( + this.verifiedBreakpointKeys.map(x => x) + ); + this.verifiedBreakpointKeys = []; + this.emit('breakpoints-verified', { + breakpoints: breakpoints + }); + }); + } + } + private verifiedBreakpointKeys: string[] = []; + private isVerifyEventQueued = false; - //a debug session is currently running - } else { - //TODO use the standard reverse-lookup logic for converting the rootDir or stagingDir paths into sourceDirs - - //if a breakpoint gets set in rootDir, and we have sourceDirs, convert the rootDir path to sourceDirs path - //so the breakpoint gets moved into the source file instead of the output file - if (this.launchConfiguration?.sourceDirs && this.launchConfiguration.sourceDirs.length > 0) { - let lastWorkingPath = ''; - for (const sourceDir of this.launchConfiguration.sourceDirs) { - sourceFilePath = sourceFilePath.replace(this.launchConfiguration.rootDir, sourceDir); - if (fsExtra.pathExistsSync(sourceFilePath)) { - lastWorkingPath = sourceFilePath; - } - } - sourceFilePath = lastWorkingPath; + /** + * Generate a key based on the features of the breakpoint. Every breakpoint that exists at the same location + * and has the same features should have the same key. + */ + public getBreakpointKey(filePath: string, breakpoint: DebugProtocol.SourceBreakpoint | AugmentedSourceBreakpoint) { + const key = `${standardizePath(filePath)}:${breakpoint.line}:${breakpoint.column ?? 0}`; - } - //new breakpoints will be verified=false, but breakpoints that were removed and then added again should be verified=true - if (breakpointsArray.find(x => x.wasAddedBeforeLaunch && x.line === bp.line)) { - bp.verified = true; - bp.wasAddedBeforeLaunch = true; - } else { - bp.verified = false; - bp.wasAddedBeforeLaunch = false; - } + const condition = breakpoint.condition?.trim(); + if (condition) { + return `${key}-condition=${condition}`; } - //if we already have a breakpoint for this exact line, don't add another one - if (breakpointsArray.find(x => x.line === breakpoint.line)) { + const hitCondition = parseInt(breakpoint.hitCondition?.trim()); + if (!isNaN(hitCondition)) { + return `${key}-hitCondition=${hitCondition}`; + } - } else { - //add the breakpoint to the list - breakpointsArray.push(bp); + if (breakpoint.logMessage) { + return `${key}-logMessage=${breakpoint.logMessage}`; } + + return `${key}-standard`; } /** * Set/replace/delete the list of breakpoints for this file. - * @param sourceFilePath + * @param srcPath * @param allBreakpointsForFile */ - public replaceBreakpoints(sourceFilePath: string, allBreakpointsForFile: DebugProtocol.SourceBreakpoint[]): AugmentedSourceBreakpoint[] { - sourceFilePath = this.sanitizeSourceFilePath(sourceFilePath); + public replaceBreakpoints(srcPath: string, allBreakpointsForFile: DebugProtocol.SourceBreakpoint[]): AugmentedSourceBreakpoint[] { + srcPath = this.sanitizeSourceFilePath(srcPath); - if (this.areBreakpointsLocked) { - //keep verified breakpoints, but toss the rest - this.breakpointsByFilePath[sourceFilePath] = this.getBreakpointsForFile(sourceFilePath) - .filter(x => x.verified); + const currentBreakpoints = allBreakpointsForFile.map(breakpoint => this.setBreakpoint(srcPath, breakpoint)); - //hide all of the breakpoints (the active ones will be reenabled later in this method) - for (let bp of this.breakpointsByFilePath[sourceFilePath]) { - bp.isHidden = true; - } - } else { - //we're not debugging erase all of the breakpoints - this.breakpointsByFilePath[sourceFilePath] = []; - } - - for (let breakpoint of allBreakpointsForFile) { - this.registerBreakpoint(sourceFilePath, breakpoint); - } + //delete all breakpoints from the file that are not currently in this list + this.breakpointsByFilePath.set( + srcPath, + this.getBreakpointsForFile(srcPath).filter(x => currentBreakpoints.includes(x)) + ); //get the final list of breakpoints - return this.getBreakpointsForFile(sourceFilePath); + return currentBreakpoints; } /** @@ -156,9 +234,7 @@ export class BreakpointManager { let result = {} as Record>; //iterate over every file that contains breakpoints - for (let sourceFilePath in this.breakpointsByFilePath) { - let breakpoints = this.breakpointsByFilePath[sourceFilePath]; - + for (let [sourceFilePath, breakpoints] of this.breakpointsByFilePath) { for (let breakpoint of breakpoints) { //get the list of locations in staging that this breakpoint should be written to. //if none are found, then this breakpoint is ignored @@ -167,7 +243,7 @@ export class BreakpointManager { breakpoint.line, breakpoint.column, [ - ...project.sourceDirs, + ...project?.sourceDirs ?? [], project.rootDir ], project.stagingFolderPath, @@ -182,16 +258,29 @@ export class BreakpointManager { ), '' ); + const pkgPath = 'pkg:/' + fileUtils + //replace staging folder path with nothing (so we can build a pkg path) + .replaceCaseInsensitive( + s`${stagingLocation.filePath}`, + s`${project.stagingFolderPath}`, + '' + ) + //force to unix path separators + .replace(/[\/\\]+/g, '/') + //remove leading slash + .replace(/^\//, ''); + let obj: BreakpointWorkItem = { //add the breakpoint info ...breakpoint, //add additional info - sourceFilePath: sourceFilePath, + srcPath: sourceFilePath, rootDirFilePath: s`${project.rootDir}/${relativeStagingPath}`, line: stagingLocation.lineNumber, column: stagingLocation.columnIndex, stagingFilePath: stagingLocation.filePath, - type: stagingLocationsResult.type + type: stagingLocationsResult.type, + pkgPath: pkgPath }; if (!result[stagingLocation.filePath]) { result[stagingLocation.filePath] = []; @@ -233,12 +322,36 @@ export class BreakpointManager { let promises = [] as Promise[]; for (let stagingFilePath in breakpointsByStagingFilePath) { - promises.push(this.writeBreakpointsToFile(stagingFilePath, breakpointsByStagingFilePath[stagingFilePath])); + const breakpoints = breakpointsByStagingFilePath[stagingFilePath]; + promises.push(this.writeBreakpointsToFile(stagingFilePath, breakpoints)); + for (const breakpoint of breakpoints) { + //mark this breakpoint as verified + this.verifyBreakpoint(breakpoint.hash, breakpoint.id); + //add this breakpoint to the list of "permanent" breakpoints + this.registerPermanentBreakpoint(breakpoint); + } } await Promise.all(promises); + + //sort all permanent breakpoints by line and column + for (const [key, breakpoints] of this.permanentBreakpointsBySrcPath) { + this.permanentBreakpointsBySrcPath.set(key, orderBy(breakpoints, [x => x.line, x => x.column])); + } + } + + private registerPermanentBreakpoint(breakpoint: BreakpointWorkItem) { + const collection = this.permanentBreakpointsBySrcPath.get(breakpoint.srcPath) ?? []; + //clone the breakpoint so future updates don't mutate it. + collection.push({ ...breakpoint }); + this.permanentBreakpointsBySrcPath.set(breakpoint.srcPath, collection); } + /** + * The list of breakpoints that were permanently written to a file at the start of a debug session. Used for line offset calculations. + */ + private permanentBreakpointsBySrcPath = new Map(); + /** * Write breakpoints to the specified file, and update the sourcemaps to match */ @@ -257,7 +370,7 @@ export class BreakpointManager { //the calling function will merge this sourcemap into the other existing sourcemap, so just use the same name because it doesn't matter ? breakpoints[0].rootDirFilePath //the calling function doesn't have a sourcemap for this file, so we need to point it to the sourceDirs found location (probably rootDir...) - : breakpoints[0].sourceFilePath; + : breakpoints[0].srcPath; let sourceAndMap = this.getSourceAndMapWithBreakpoints(fileContents, originalFilePath, breakpoints); @@ -379,9 +492,36 @@ export class BreakpointManager { /** * Get the list of breakpoints for the specified file path, or an empty array */ - public getBreakpointsForFile(filePath: string): AugmentedSourceBreakpoint[] { + private getBreakpointsForFile(filePath: string, registerIfMissing = false): AugmentedSourceBreakpoint[] { let key = this.sanitizeSourceFilePath(filePath); - return this.breakpointsByFilePath[key] ?? []; + const result = this.breakpointsByFilePath.get(key) ?? []; + if (registerIfMissing === true) { + this.breakpointsByFilePath.set(key, result); + } + return result; + } + + /** + * Get the permanent breakpoint with the specified hash + * @returns the breakpoint with the matching hash, or undefined + */ + public getPermanentBreakpoint(hash: string) { + for (const [, breakpoints] of this.permanentBreakpointsBySrcPath) { + for (const breakpoint of breakpoints) { + if (breakpoint.hash === hash) { + return breakpoint; + } + } + } + } + + /** + * Get the list of breakpoints that were written to the source file + */ + public getPermanentBreakpointsForFile(srcPath: string) { + return this.permanentBreakpointsBySrcPath.get( + this.sanitizeSourceFilePath(srcPath) + ) ?? []; } /** @@ -391,51 +531,192 @@ export class BreakpointManager { public sanitizeSourceFilePath(filePath: string) { filePath = fileUtils.standardizePath(filePath); - for (let key in this.breakpointsByFilePath) { + for (let [key] of this.breakpointsByFilePath) { if (filePath.toLowerCase() === key.toLowerCase()) { return key; } } return filePath; } + + /** + * Determine if there's a breakpoint set at the given staging folder and line. + * This is not trivial, so only run when absolutely necessary + * @param projects the list of projects to scan + * @param pkgPath the path to the file in the staging directory + * @param line the 0-based line for the breakpoint + */ + public async lineHasBreakpoint(projects: Project[], pkgPath: string, line: number) { + const workByProject = (await Promise.all( + projects.map(project => this.getBreakpointWork(project)) + )); + for (const projectWork of workByProject) { + for (let key in projectWork) { + const work = projectWork[key]; + for (const item of work) { + if (item.pkgPath === pkgPath && item.line - 1 === line) { + return true; + } + } + } + } + } + + + /** + * Get a diff of all breakpoints that have changed since the last time the diff was retrieved. + * Sets the new baseline to the current state, so the next diff will be based on this new baseline. + * + * All projects should be passed in every time. + */ + public async getDiff(projects: Project[]): Promise { + //if the diff is currently running, return an empty "nothing has changed" diff + if (this.isGetDiffRunning) { + return { + added: [], + removed: [], + unchanged: [...this.lastState.values()] + }; + } + try { + this.isGetDiffRunning = true; + + const currentState = new Map(); + await Promise.all( + projects.map(async (project) => { + //get breakpoint data for every project + const work = await this.getBreakpointWork(project); + for (const filePath in work) { + const fileWork = work[filePath]; + for (const bp of fileWork) { + const key = [ + bp.stagingFilePath, + bp.line, + bp.column, + bp.condition, + bp.hitCondition, + bp.logMessage + ].join('--'); + //clone the breakpoint and then add it to the current state + currentState.set(key, { ...bp }); + } + } + }) + ); + + const added = new Map(); + const removed = new Map(); + const unchanged = new Map(); + for (const key of [...currentState.keys(), ...this.lastState.keys()]) { + const inCurrent = currentState.has(key); + const inLast = this.lastState.has(key); + //no change + if (inLast && inCurrent) { + unchanged.set(key, currentState.get(key)); + + //added since last time + } else if (!inLast && inCurrent) { + added.set(key, currentState.get(key)); + + //removed since last time + } else { + removed.set(key, this.lastState.get(key)); + } + } + this.lastState = currentState; + return { + added: [...added.values()], + removed: [...removed.values()], + unchanged: [...unchanged.values()] + }; + } finally { + this.isGetDiffRunning = false; + } + } + /** + * Flag indicating whether a `getDiff` function is currently running + */ + private isGetDiffRunning = false; + private lastState = new Map(); } -interface AugmentedSourceBreakpoint extends DebugProtocol.SourceBreakpoint { +export interface Diff { + added: BreakpointWorkItem[]; + removed: BreakpointWorkItem[]; + unchanged: BreakpointWorkItem[]; +} + +export interface AugmentedSourceBreakpoint extends DebugProtocol.SourceBreakpoint { /** - * An ID for this breakpoint, which is used to set/unset breakpoints in the client + * The path to the source file where this breakpoint was originally set */ - id: number; + srcPath: string; /** - * Was this breakpoint added before launch? That means this breakpoint was written into the source code as a `stop` statement, - * so if users toggle this breakpoint line on and off, it should get verified every time. + * A unique hash generated for the breakpoint at this exact file/line/column/feature. Every breakpoint with these same features should get the same hash */ - wasAddedBeforeLaunch: boolean; + hash: string; /** - * This breakpoint has been verified (i.e. we were able to set it at the given location) + * The device-provided breakpoint id. A missing ID means this breakpoint has not yet been verified by the device. */ - verified: boolean; + deviceId?: number; /** - * Since breakpoints are written into the source code, we can't delete the `wasAddedBeforeLaunch` breakpoints, - * otherwise the non-sourcemap debugging process's line offsets could get messed up. So, for the `wasAddedBeforeLaunch` - * breakpoints, we need to mark them as hidden when the user unsets them. + * A unique ID the debug adapter generates to help send updates to the client about this breakpoint */ - isHidden: boolean; + id: number; + /** + * This breakpoint has been verified (i.e. we were able to set it at the given location) + */ + verified: boolean; } -interface BreakpointWorkItem { - sourceFilePath: string; +export interface BreakpointWorkItem { + /** + * The path to the source file where this breakpoint was originally set + */ + srcPath: string; + /** + * The absolute path to the file in the staging folder + */ stagingFilePath: string; + /** + * The device path (i.e. `pkg:/source/main.brs`) + */ + pkgPath: string; + /** + * The path to the rootDir for this breakpoint + */ rootDirFilePath: string; /** * The 1-based line number */ line: number; + /** + * The device-provided breakpoint id. A missing ID means this breakpoint has not yet been verified by the device. + */ + deviceId?: number; + /** + * An id generated by the debug adapter used to identify this breakpoint in the client + */ + id: number; + /** + * A unique hash generated for the breakpoint at this exact file/line/column/feature. Every breakpoint with these same features should get the same hash + */ + hash: string; /** * The 0-based column index */ column: number; + /** + * If set, this breakpoint will only activate when this condition evaluates to true + */ condition?: string; + /** + * If set, this breakpoint will only activate once the breakpoint has been hit this many times. + */ hitCondition?: string; + /** + * If set, this breakpoint will emit a log message at runtime and will not actually stop at the breakpoint + */ logMessage?: string; /** * `sourceMap` means derived from a source map. diff --git a/src/managers/LocationManager.ts b/src/managers/LocationManager.ts index 70993706..91a712ec 100644 --- a/src/managers/LocationManager.ts +++ b/src/managers/LocationManager.ts @@ -154,7 +154,7 @@ export class LocationManager { //look through the files array to see if there are any mappings that reference this file. //both `src` and `dest` are assumed to already be standardized - for (let fileMapping of fileMappings) { + for (let fileMapping of fileMappings ?? []) { if (fileMapping.src === sourceFilePath) { return { type: 'fileMap', diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 20f7753e..37044a04 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -54,11 +54,11 @@ describe('ProjectManager', () => { describe('getLineNumberOffsetByBreakpoints', () => { let filePath = 'does not matter'; it('accounts for the entry breakpoint', () => { - sinon.stub(manager.breakpointManager, 'getBreakpointsForFile').returns([{ + manager.breakpointManager['permanentBreakpointsBySrcPath'].set(filePath, [{ line: 3 }, { line: 3 - }]); + }] as any); //no offset because line is before any breakpoints expect(manager.getLineNumberOffsetByBreakpoints(filePath, 1)).to.equal(1); //after the breakpoints, should be offset by -1 @@ -66,7 +66,6 @@ describe('ProjectManager', () => { }); it('works with zero breakpoints', () => { - sinon.stub(manager.breakpointManager, 'getBreakpointsForFile').returns([]); //no offset because line is before any breakpoints expect(manager.getLineNumberOffsetByBreakpoints(filePath, 1)).to.equal(1); //after the breakpoints, should be offset by -1 @@ -116,7 +115,7 @@ describe('ProjectManager', () => { line = 12 end function */ - sinon.stub(manager.breakpointManager, 'getBreakpointsForFile').returns([ + manager.breakpointManager['permanentBreakpointsBySrcPath'].set(filePath, [ { line: 3 }, { line: 4 }, { line: 5 }, @@ -124,7 +123,7 @@ describe('ProjectManager', () => { { line: 8 }, { line: 10 }, { line: 12 } - ]); + ] as any); //no offset because line is before any breakpoints //no breakpoint expect(manager.getLineNumberOffsetByBreakpoints(filePath, 1)).to.equal(1); diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 873e0465..f73a2324 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -35,6 +35,7 @@ export class ProjectManager { public launchConfiguration: { enableSourceMaps?: boolean; + enableDebugProtocol?: boolean; }; public mainProject: Project; @@ -44,6 +45,13 @@ export class ProjectManager { this.componentLibraryProjects.push(project); } + public getAllProjects() { + return [ + ...(this.mainProject ? [this.mainProject] : []), + ...(this.componentLibraryProjects ?? []) + ]; + } + /** * Get the list of staging folder paths from all projects */ @@ -61,7 +69,7 @@ export class ProjectManager { * @param debuggerLineNumber - the line number from the debugger */ public getLineNumberOffsetByBreakpoints(filePath: string, debuggerLineNumber: number) { - let breakpoints = this.breakpointManager.getBreakpointsForFile(filePath); + let breakpoints = this.breakpointManager.getPermanentBreakpointsForFile(filePath); //throw out duplicate breakpoints (account for entry breakpoint) and sort them ascending breakpoints = this.breakpointManager.sortAndRemoveDuplicateBreakpoints(breakpoints); @@ -117,8 +125,8 @@ export class ProjectManager { enableSourceMaps: this.launchConfiguration?.enableSourceMaps ?? true }); - //if sourcemaps are disabled, account for the breakpoint offsets - if (sourceLocation && this.launchConfiguration?.enableSourceMaps === false) { + //if sourcemaps are disabled, and this is a telnet debug dession, account for breakpoint offsets + if (sourceLocation && this.launchConfiguration?.enableSourceMaps === false && !this.launchConfiguration.enableDebugProtocol) { sourceLocation.lineNumber = this.getLineNumberOffsetByBreakpoints(sourceLocation.filePath, sourceLocation.lineNumber); } @@ -146,7 +154,7 @@ export class ProjectManager { let sourceLocation = await this.getSourceLocation(entryPoint.relativePath, entryPoint.lineNumber); //register the entry breakpoint - this.breakpointManager.registerBreakpoint(sourceLocation.filePath, { + this.breakpointManager.setBreakpoint(sourceLocation.filePath, { //+1 to select the first line of the function line: sourceLocation.lineNumber + 1 }); diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index 1b08e40e..69689a3e 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -1,3 +1,4 @@ +import { expect } from 'chai'; import dedent = require('dedent'); /** @@ -21,3 +22,43 @@ export function clean(strings: TemplateStringsArray, ...expressions: any) { result = dedent(result); return result; } + +/** + * Take only the properties from `subject` that are present on `pattern` + */ +export function pick(subject: Record, pattern: Record) { + if (!subject) { + return subject; + } + let keys = Object.keys(pattern ?? {}); + //if there were no keys provided, use some sane defaults + keys = keys.length > 0 ? keys : ['message', 'code', 'range', 'severity']; + + //copy only compare the specified keys from actualDiagnostic + const clone = {}; + for (const key of keys) { + clone[key] = subject[key]; + } + return clone; +} + +/** + * For every item in `patterns`, pick those properties from the item at the corresponding index in `subjects` + */ +export function pickArray(subjects: any[], patterns: any[]) { + subjects = [...subjects]; + for (let i = 0; i < patterns.length; i++) { + if (subjects[i]) { + subjects[i] = pick(subjects[i], patterns[i]); + } + } + return subjects; +} + +export function expectPickEquals(subjects: any[], patterns: any[]) { + expect( + pickArray(subjects, patterns) + ).to.eql( + patterns + ); +} diff --git a/tsconfig.json b/tsconfig.json index e497a465..4e135446 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "target": "es2018", "noImplicitAny": false, "removeComments": false, - "noUnusedLocals": true, + "noUnusedLocals": false, "noImplicitThis": true, "inlineSourceMap": false, "declaration": true, From 131a24c8233048f1f3dd228cebf2b9133ef87ac6 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 7 Jun 2022 15:21:20 -0400 Subject: [PATCH 017/197] Fix crash when RAF files show up in stacktrace (#88) --- .../BrightScriptDebugSession.spec.ts | 99 +++++++++++++------ src/debugSession/BrightScriptDebugSession.ts | 13 ++- src/managers/FileManager.ts | 6 +- src/managers/ProjectManager.ts | 2 +- 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index f79219ee..e7fd0ae6 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -7,7 +7,7 @@ import type { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; import { Breakpoint, DebugSession } from 'vscode-debugadapter'; import { BrightScriptDebugSession } from './BrightScriptDebugSession'; import { fileUtils } from '../FileUtils'; -import type { EvaluateContainer, TelnetAdapter } from '../adapters/TelnetAdapter'; +import type { EvaluateContainer, StackFrame, TelnetAdapter } from '../adapters/TelnetAdapter'; import { PrimativeType } from '../adapters/TelnetAdapter'; import { defer } from '../util'; import { HighLevelType } from '../interfaces'; @@ -16,12 +16,15 @@ import type { SinonStub } from 'sinon'; import { standardizePath as s } from 'brighterscript'; import { DefaultFiles } from 'roku-deploy'; import type { DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter'; +import type { AddProjectParams, ComponentLibraryConstructorParams } from '../managers/ProjectManager'; +import { ComponentLibraryProject, Project } from '../managers/ProjectManager'; -let sinon = sinonActual.createSandbox(); -const tempDir = s`${process.cwd()}/.tmp`; -const rootDir = s`${tempDir}/rootDir`; -const outDir = s`${tempDir}/out`; -const stagingFolderPath = s`${tempDir}/stagingDir`; +const sinon = sinonActual.createSandbox(); +const tempDir = s`${__dirname}/../../.tmp`; +const rootDir = s`${tempDir}/rootDir}`; +const outDir = s`${tempDir}/outDir`; +const stagingDir = s`${outDir}/stagingDir`; +const complib1Dir = s`${tempDir}/complib1`; describe('BrightScriptDebugSession', () => { let responseDeferreds = []; @@ -37,7 +40,8 @@ describe('BrightScriptDebugSession', () => { let launchConfiguration: LaunchConfiguration; let initRequestArgs: DebugProtocol.InitializeRequestArguments; - let rokuAdapter: ReturnType; + let rokuAdapter: TelnetAdapter; + let errorSpy: sinon.SinonSpy; beforeEach(() => { sinon.restore(); @@ -47,6 +51,7 @@ describe('BrightScriptDebugSession', () => { } catch (e) { console.log(e); } + errorSpy = sinon.spy(session.logger, 'error'); //override the error response function and throw an exception so we can fail any tests (session as any).sendErrorResponse = (...args: string[]) => { throw new Error(args[2]); @@ -54,7 +59,7 @@ describe('BrightScriptDebugSession', () => { launchConfiguration = { rootDir: rootDir, outDir: outDir, - stagingFolderPath: stagingFolderPath, + stagingFolderPath: stagingDir, files: DefaultFiles } as any; session['launchConfiguration'] = launchConfiguration; @@ -88,10 +93,22 @@ describe('BrightScriptDebugSession', () => { getFilePaths: () => { } }; - rokuAdapter = createRokuAdapter(); - (session as any).rokuAdapter = rokuAdapter; + rokuAdapter = { + on: () => { + return () => { + }; + }, + activate: () => Promise.resolve(), + registerSourceLocator: (a, b) => { }, + setConsoleOutput: (a) => { }, + evaluate: () => { }, + syncBreakpoints: () => { }, + getVariable: () => { }, + getStackTrace: () => { } + } as any; + session['rokuAdapter'] = rokuAdapter; //mock the roku adapter - (session as any).connectRokuAdapter = () => { + session['connectRokuAdapter'] = () => { return Promise.resolve(rokuAdapter); }; @@ -116,22 +133,6 @@ describe('BrightScriptDebugSession', () => { }); }); - function createRokuAdapter() { - return { - on: () => { - return () => { - }; - }, - activate: () => Promise.resolve(), - registerSourceLocator: (a, b) => { }, - setConsoleOutput: (a) => { }, - evaluate: (() => { }) as (...args) => any, - syncBreakpoints: () => { }, - getVariable: (() => Promise.resolve()) as () => any, - isAtDebuggerPrompt: false - }; - } - describe('initializeRequest', () => { it('does not throw', () => { assert.doesNotThrow(() => { @@ -425,12 +426,12 @@ describe('BrightScriptDebugSession', () => { it('registers the entry breakpoint when stopOnEntry is enabled', async () => { (session as any).launchConfiguration = { stopOnEntry: true }; session.projectManager.mainProject = { - stagingFolderPath: stagingFolderPath + stagingFolderPath: stagingDir }; let stub = sinon.stub(session.projectManager, 'registerEntryBreakpoint').returns(Promise.resolve()); await session.handleEntryBreakpoint(); expect(stub.called).to.be.true; - expect(stub.args[0][0]).to.equal(stagingFolderPath); + expect(stub.args[0][0]).to.equal(stagingDir); }); it('does NOT register the entry breakpoint when stopOnEntry is enabled', async () => { (session as any).launchConfiguration = { stopOnEntry: false }; @@ -478,10 +479,10 @@ describe('BrightScriptDebugSession', () => { rokuAdapter.isAtDebuggerPrompt = true; evalStub = sinon.stub(rokuAdapter, 'evaluate').callsFake((args) => { console.log('called with', args); - return { + return Promise.resolve({ message: undefined, type: 'message' - }; + }); }); getVarStub = sinon.stub(rokuAdapter, 'getVariable').callsFake(() => { return Promise.resolve(getVarValue); @@ -557,6 +558,42 @@ describe('BrightScriptDebugSession', () => { expect(session['variables']).to.be.empty; }); + describe('stackTraceRequest', () => { + it('gracefully handles missing files', async () => { + session.projectManager.mainProject = new Project({ + rootDir: rootDir, + outDir: stagingDir + } as Partial as any); + session.projectManager['mainProject'].fileMappings = []; + + session.projectManager.componentLibraryProjects.push( + new ComponentLibraryProject({ + rootDir: complib1Dir, + stagingFolderPath: stagingDir, + outDir: outDir, + libraryIndex: 1 + } as Partial as any) + ); + session.projectManager['componentLibraryProjects'][0].fileMappings = []; + + sinon.stub(rokuAdapter, 'getStackTrace').returns(Promise.resolve([{ + filePath: 'customComplib:/source/lib/AdManager__lib1.brs', + lineNumber: 500, + functionIdentifier: 'doSomething' + }, { + filePath: 'roku_ads_lib:/libsource/Roku_Ads.brs', + lineNumber: 400, + functionIdentifier: 'roku_ads__showads' + }, { + filePath: 'pkg:/source/main.brs', + lineNumber: 10, + functionIdentifier: 'main' + }] as StackFrame[])); + await session['stackTraceRequest']({} as any, { threadId: 1 }); + expect(errorSpy.getCalls()[0]?.args ?? []).to.eql([]); + }); + }); + describe('repl', () => { it('calls eval for print statement', async () => { await expectResponse({ diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 85845b3c..f7f591f6 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -634,7 +634,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { //the stacktrace returns function identifiers in all lower case. Try to get the actual case //load the contents of the file and get the correct casing for the function identifier try { - let functionName = this.fileManager.getCorrectFunctionNameCase(sourceLocation.filePath, debugFrame.functionIdentifier); + let functionName = this.fileManager.getCorrectFunctionNameCase(sourceLocation?.filePath, debugFrame.functionIdentifier); if (functionName) { //search for original function name if this is an anonymous function. @@ -651,14 +651,18 @@ export class BrightScriptDebugSession extends BaseDebugSession { } catch (error) { this.logger.error('Error correcting function identifier case', { error, sourceLocation, debugFrame }); } + const filePath = sourceLocation?.filePath ?? debugFrame.filePath; - let frame = new StackFrame( + const frame: DebugProtocol.StackFrame = new StackFrame( debugFrame.frameId, `${debugFrame.functionIdentifier}`, - new Source(path.basename(sourceLocation.filePath), sourceLocation.filePath), - sourceLocation.lineNumber, + new Source(path.basename(filePath), filePath), + sourceLocation?.lineNumber ?? debugFrame.lineNumber, 1 ); + if (!sourceLocation) { + frame.presentationHint = 'subtle'; + } frames.push(frame); } } else { @@ -1046,6 +1050,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { //make the connection await this.rokuAdapter.connect(); this.rokuAdapterDeferred.resolve(this.rokuAdapter); + return this.rokuAdapter; } private getVariableFromResult(result: EvaluateContainer, frameId: number) { diff --git a/src/managers/FileManager.ts b/src/managers/FileManager.ts index 02198a3d..7767c9f1 100644 --- a/src/managers/FileManager.ts +++ b/src/managers/FileManager.ts @@ -16,8 +16,8 @@ export class FileManager { private cache = {} as Record; public getCodeFile(filePath: string) { - let lowerFilePath = filePath.toLowerCase(); - if (!this.cache[lowerFilePath]) { + let lowerFilePath = filePath?.toLowerCase(); + if (lowerFilePath && !this.cache[lowerFilePath]) { let fileInfo = { lines: [], functionNameMap: {} @@ -126,7 +126,7 @@ export class FileManager { */ public getCorrectFunctionNameCase(sourceFilePath: string, functionName: string) { let fileInfo = this.getCodeFile(sourceFilePath); - return fileInfo.functionNameMap[functionName.toLowerCase()] ?? functionName; + return fileInfo?.functionNameMap[functionName?.toLowerCase()] ?? functionName; } /** diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index f73a2324..b1a89513 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -208,7 +208,7 @@ export class ProjectManager { } } -interface AddProjectParams { +export interface AddProjectParams { rootDir: string; outDir: string; sourceDirs?: string[]; From 098c1a5f593a6f39417e0671a195833e2d6f3d33 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 8 Jun 2022 13:14:20 -0400 Subject: [PATCH 018/197] update changelog for v0.13.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b8bac3..ab379a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.13.0](https://github.com/rokucommunity/roku-debug/compare/v0.12.2...v0.13.0) - 2022-06-08 +### Added + - Support for dynamic breakpoints when using Debug Protocol ([#84](https://github.com/rokucommunity/roku-debug/pull/84)) +### Fixed + - crash when RAF files show up in stacktrace ([#88](https://github.com/rokucommunity/roku-debug/pull/88)) + + + ## [0.12.2](https://github.com/rokucommunity/roku-debug/compare/v0.12.1...v0.12.2) - 2022-05-31 ### Changed - upgrade to [brighterscript@0.51.3](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0513---2022-05-31) From 9fb8d007a37c417ea38f9f026d138318e731a33f Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 8 Jun 2022 13:14:51 -0400 Subject: [PATCH 019/197] 0.13.0 --- CHANGELOG.md | 3 +++ package-lock.json | 57 +++++++++++++++++++++++++---------------------- package.json | 6 ++--- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab379a1a..d5596b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.13.0](https://github.com/rokucommunity/roku-debug/compare/v0.12.2...v0.13.0) - 2022-06-08 ### Added - Support for dynamic breakpoints when using Debug Protocol ([#84](https://github.com/rokucommunity/roku-debug/pull/84)) +### Changed + - upgrade to [brighterscript@0.52.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0520---2022-06-08) + - upgrade to [roku-deploy@3.7.1](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#371---2022-06-08) ### Fixed - crash when RAF files show up in stacktrace ([#88](https://github.com/rokucommunity/roku-debug/pull/88)) diff --git a/package-lock.json b/package-lock.json index 8989333e..60e3eee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "roku-debug", - "version": "0.12.2", + "version": "0.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.12.2", + "version": "0.13.0", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.51.3", + "brighterscript": "^0.52.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -19,7 +19,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.7.0", + "roku-deploy": "^3.7.1", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.51.3", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.51.3.tgz", - "integrity": "sha512-K0LqG9ADnsrX2T6WMyHZFK9r+9f4bjZCWBNlDwYGI4S++SvF9l0JSlSCaVU+iNQE8u5VPu7gRBoA+Qdib3ovng==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.52.0.tgz", + "integrity": "sha512-pEopdyaB73knCtmTo2YK+Ai6uEYsRShMn1Paa8pTJKG08GIfzutL3PyImVh+KK5+JHLugjzUDnKAe3pXfHKblg==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -1186,9 +1186,9 @@ "cross-platform-clear-console": "^2.3.0", "debounce-promise": "^3.1.0", "eventemitter3": "^4.0.0", + "fast-glob": "^3.2.11", "file-url": "^3.0.0", - "fs-extra": "^7.0.1", - "glob": "^7.1.6", + "fs-extra": "^8.0.0", "jsonc-parser": "^2.3.0", "long": "^3.2.0", "luxon": "^1.8.3", @@ -1196,7 +1196,7 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", - "roku-deploy": "^3.7.0", + "roku-deploy": "^3.7.1", "serialize-error": "^7.0.1", "source-map": "^0.7.3", "vscode-languageserver": "7.0.0", @@ -1253,10 +1253,11 @@ "license": "MIT" }, "node_modules/brighterscript/node_modules/fs-extra": { - "version": "7.0.1", - "license": "MIT", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dependencies": { - "graceful-fs": "^4.1.2", + "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" }, @@ -3966,9 +3967,9 @@ } }, "node_modules/roku-deploy": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.0.tgz", - "integrity": "sha512-+MBppc3q7ZEhzXu86O43mITxa+ro4hkCCXt4UhsWDqvCNx7w4b6CHzjrRAXYtdGQoDe6xqZjJhqCqC/YytJqWA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.1.tgz", + "integrity": "sha512-xXTYNr4Ug+Kr+bnhDqlJDcbuu6rg8x0MFIpA+36jbpJcqsI6ekbWzRh2QhUG6aZ4F8+zKt8jZFIkZDeyooJJfQ==", "dependencies": { "chalk": "^2.4.2", "dateformat": "^3.0.3", @@ -5552,9 +5553,9 @@ } }, "brighterscript": { - "version": "0.51.3", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.51.3.tgz", - "integrity": "sha512-K0LqG9ADnsrX2T6WMyHZFK9r+9f4bjZCWBNlDwYGI4S++SvF9l0JSlSCaVU+iNQE8u5VPu7gRBoA+Qdib3ovng==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.52.0.tgz", + "integrity": "sha512-pEopdyaB73knCtmTo2YK+Ai6uEYsRShMn1Paa8pTJKG08GIfzutL3PyImVh+KK5+JHLugjzUDnKAe3pXfHKblg==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5566,9 +5567,9 @@ "cross-platform-clear-console": "^2.3.0", "debounce-promise": "^3.1.0", "eventemitter3": "^4.0.0", + "fast-glob": "^3.2.11", "file-url": "^3.0.0", - "fs-extra": "^7.0.1", - "glob": "^7.1.6", + "fs-extra": "^8.0.0", "jsonc-parser": "^2.3.0", "long": "^3.2.0", "luxon": "^1.8.3", @@ -5576,7 +5577,7 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", - "roku-deploy": "^3.7.0", + "roku-deploy": "^3.7.1", "serialize-error": "^7.0.1", "source-map": "^0.7.3", "vscode-languageserver": "7.0.0", @@ -5619,9 +5620,11 @@ "version": "1.1.3" }, "fs-extra": { - "version": "7.0.1", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "requires": { - "graceful-fs": "^4.1.2", + "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } @@ -7311,9 +7314,9 @@ } }, "roku-deploy": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.0.tgz", - "integrity": "sha512-+MBppc3q7ZEhzXu86O43mITxa+ro4hkCCXt4UhsWDqvCNx7w4b6CHzjrRAXYtdGQoDe6xqZjJhqCqC/YytJqWA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.1.tgz", + "integrity": "sha512-xXTYNr4Ug+Kr+bnhDqlJDcbuu6rg8x0MFIpA+36jbpJcqsI6ekbWzRh2QhUG6aZ4F8+zKt8jZFIkZDeyooJJfQ==", "requires": { "chalk": "^2.4.2", "dateformat": "^3.0.3", diff --git a/package.json b/package.json index ea1540c1..d22e61c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.12.2", + "version": "0.13.0", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.51.3", + "brighterscript": "^0.52.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -94,7 +94,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.7.0", + "roku-deploy": "^3.7.1", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", From f39f721fc56f8c644f3ac6e3d0dd9286768dd5ee Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 9 Jun 2022 09:56:25 -0400 Subject: [PATCH 020/197] launch and lint tweaks --- .eslintrc.js | 1 + .vscode/launch.json | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index dd567d06..13e1a773 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { ], rules: { '@typescript-eslint/array-type': 'off', + '@typescript-eslint/class-literal-property-style': 'off', '@typescript-eslint/consistent-type-assertions': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-member-accessibility': 'off', diff --git a/.vscode/launch.json b/.vscode/launch.json index 11b3c433..981537e0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "type": "node", + "type": "pwa-node", "request": "launch", "name": "Run Roku Sample Project", "skipFiles": [ @@ -10,7 +10,6 @@ ], "program": "${workspaceFolder}/dist/index.js", "preLaunchTask": "tsc: build - tsconfig.json", - "protocol": "inspector", "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceFolder}/dist/**/*.js" @@ -18,7 +17,7 @@ }, { "name": "Debug Tests", - "type": "node", + "type": "pwa-node", "request": "launch", "smartStep": false, "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", @@ -32,7 +31,6 @@ "500000" ], "cwd": "${workspaceRoot}", - "protocol": "inspector", "internalConsoleOptions": "openOnSessionStart" } ] From c862cb1dfee54cdef29141f6dacec7b204a52c93 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 9 Jun 2022 10:28:45 -0400 Subject: [PATCH 021/197] Fix complib breakpoints (#89) * launch and lint tweaks * Fix complib postfix bug in breakpoints --- src/FileUtils.spec.ts | 24 ++++++++++++++ src/FileUtils.ts | 27 ++++++++++++++++ src/managers/BreakpointManager.spec.ts | 43 +++++++++++++++++++++++--- src/managers/BreakpointManager.ts | 2 ++ src/managers/ProjectManager.spec.ts | 29 ----------------- src/managers/ProjectManager.ts | 42 +++++++------------------ 6 files changed, 104 insertions(+), 63 deletions(-) diff --git a/src/FileUtils.spec.ts b/src/FileUtils.spec.ts index 22667067..0c6134c8 100644 --- a/src/FileUtils.spec.ts +++ b/src/FileUtils.spec.ts @@ -261,4 +261,28 @@ describe('FileUtils', () => { expect(fileUtils.removeTrailingSlash('a')).to.equal('a'); }); }); + + describe('unPostfixFilePath', () => { + it('removes postfix from paths that contain it', () => { + expect(fileUtils.unPostfixFilePath(`source/main__lib1.brs`, '__lib1')).to.equal('source/main.brs'); + expect(fileUtils.unPostfixFilePath(`components/component1__lib1.brs`, '__lib1')).to.equal('components/component1.brs'); + }); + + it('removes postfix case insensitive', () => { + expect(fileUtils.unPostfixFilePath(`source/main__LIB1.brs`, '__lib1')).to.equal('source/main.brs'); + expect(fileUtils.unPostfixFilePath(`source/MAIN__lib1.brs`, '__lib1')).to.equal('source/MAIN.brs'); + }); + + it('does nothing to files without the postfix', () => { + expect(fileUtils.unPostfixFilePath(`source/main.brs`, '__lib1')).to.equal('source/main.brs'); + }); + + it('does nothing to files with a different postfix', () => { + expect(fileUtils.unPostfixFilePath(`source/main__lib1.brs`, '__lib0')).to.equal('source/main__lib1.brs'); + }); + + it('only removes the postfix from the end of the file', () => { + expect(fileUtils.unPostfixFilePath(`source/__lib1.brs/main.brs`, '__lib1')).to.equal('source/__lib1.brs/main.brs'); + }); + }); }); diff --git a/src/FileUtils.ts b/src/FileUtils.ts index a9b3e40b..90790939 100644 --- a/src/FileUtils.ts +++ b/src/FileUtils.ts @@ -172,6 +172,33 @@ export class FileUtils { return result; } + /** + * Append a postfix to the filename BEFORE its file extension + */ + public postfixFilePath(filePath: string, postfix: string, fileExtensions: string[]) { + let parsedPath = path.parse(filePath); + + if (fileExtensions.includes(parsedPath.ext)) { + const regexp = new RegExp(parsedPath.ext + '$', 'i'); + return filePath.replace(regexp, postfix + parsedPath.ext); + } else { + return filePath; + } + } + + /** + * Given a file path, return a new path with the component library postfix removed + */ + public unPostfixFilePath(filePath: string, postfix: string) { + let parts = path.parse(filePath); + const search = `${postfix}${parts.ext}`; + if (filePath.toLowerCase().endsWith(search.toLowerCase())) { + return fileUtils.replaceCaseInsensitive(filePath, search, parts.ext); + } else { + return filePath; + } + } + /** * Replace all directory separators with current OS separators, * force all drive letters to lower case (because that's what VSCode does sometimes so this makes it consistent) diff --git a/src/managers/BreakpointManager.spec.ts b/src/managers/BreakpointManager.spec.ts index 616e6be5..527b7cb7 100644 --- a/src/managers/BreakpointManager.spec.ts +++ b/src/managers/BreakpointManager.spec.ts @@ -23,6 +23,8 @@ describe('BreakpointManager', () => { const srcPath = s`${rootDir}/source/main.brs`; const complib1RootDir = s`${tmpDir}/complib1/rootDir`; const complib1OutDir = s`${tmpDir}/complib1/outDir`; + const complib2RootDir = s`${tmpDir}/complib2/rootDir`; + const complib2OutDir = s`${tmpDir}/complib2/outDir`; let bpManager: BreakpointManager; let locationManager: LocationManager; @@ -55,6 +57,15 @@ describe('BreakpointManager', () => { outFile: s`${complib1OutDir}/complib1.zip` }) ); + projectManager.addComponentLibraryProject( + new ComponentLibraryProject({ + rootDir: complib2RootDir, + files: [], + libraryIndex: 1, + outDir: complib2OutDir, + outFile: s`${complib2OutDir}/complib2.zip` + }) + ); }); afterEach(() => { @@ -944,7 +955,6 @@ describe('BreakpointManager', () => { }); it('detects column number change (roku does not support this yet, but we might as well...)', async () => { - //add breakpoint with hit condition bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ line: 2, column: 4 @@ -957,7 +967,6 @@ describe('BreakpointManager', () => { }] }); - //change the breakpoint hit condition bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ line: 2, column: 8 @@ -976,7 +985,6 @@ describe('BreakpointManager', () => { }); it('maintains breakpoint IDs', async () => { - //add breakpoint with hit condition bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ line: 2, column: 4 @@ -989,7 +997,6 @@ describe('BreakpointManager', () => { }] }); - //change the breakpoint hit condition bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ line: 2, column: 8 @@ -1006,5 +1013,33 @@ describe('BreakpointManager', () => { }] }); }); + + it('accounts for complib filename postfixes', async () => { + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 1 + }]); + + bpManager.replaceBreakpoints(s`${complib1RootDir}/source/main.brs`, [{ + line: 2 + }]); + + bpManager.replaceBreakpoints(s`${complib2RootDir}/source/main.brs`, [{ + line: 3 + }]); + + await testDiffEquals({ + added: [{ + line: 1, + pkgPath: 'pkg:/source/main.brs' + }, { + line: 2, + pkgPath: 'pkg:/source/main__lib0.brs' + }, { + line: 3, + pkgPath: 'pkg:/source/main__lib1.brs' + }], removed: [], unchanged: [] + }); + }); + }); }); diff --git a/src/managers/BreakpointManager.ts b/src/managers/BreakpointManager.ts index 0c1c2ac7..2f2db87b 100644 --- a/src/managers/BreakpointManager.ts +++ b/src/managers/BreakpointManager.ts @@ -589,6 +589,8 @@ export class BreakpointManager { for (const filePath in work) { const fileWork = work[filePath]; for (const bp of fileWork) { + bp.stagingFilePath = fileUtils.postfixFilePath(bp.stagingFilePath, project.postfix, ['.brs']); + bp.pkgPath = fileUtils.postfixFilePath(bp.pkgPath, project.postfix, ['.brs']); const key = [ bp.stagingFilePath, bp.line, diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 37044a04..912c2367 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -719,33 +719,4 @@ describe('ComponentLibraryProject', () => { }); }); }); - - describe('removeFileNamePostfix', () => { - let project: ComponentLibraryProject; - beforeEach(() => { - project = new ComponentLibraryProject(params); - }); - - it('removes postfix from paths that contain it', () => { - expect(project.removeFileNamePostfix(`source/main__lib0.brs`)).to.equal('source/main.brs'); - expect(project.removeFileNamePostfix(`components/component1__lib0.brs`)).to.equal('components/component1.brs'); - }); - - it('removes postfix case insensitive', () => { - expect(project.removeFileNamePostfix(`source/main__LIB0.brs`)).to.equal('source/main.brs'); - expect(project.removeFileNamePostfix(`source/MAIN__lib0.brs`)).to.equal('source/MAIN.brs'); - }); - - it('does nothing to files without the postfix', () => { - expect(project.removeFileNamePostfix(`source/main.brs`)).to.equal('source/main.brs'); - }); - - it('does nothing to files with a different postfix', () => { - expect(project.removeFileNamePostfix(`source/main__lib1.brs`)).to.equal('source/main__lib1.brs'); - }); - - it('only removes the postfix from the end of the file', () => { - expect(project.removeFileNamePostfix(`source/__lib1.brs/main.brs`)).to.equal('source/__lib1.brs/main.brs'); - }); - }); }); diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index b1a89513..782bef32 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -110,8 +110,8 @@ export class ProjectManager { //remove the component library postfix if present if (project instanceof ComponentLibraryProject) { - stagingFileInfo.absolutePath = project.removeFileNamePostfix(stagingFileInfo.absolutePath); - stagingFileInfo.relativePath = project.removeFileNamePostfix(stagingFileInfo.relativePath); + stagingFileInfo.absolutePath = fileUtils.unPostfixFilePath(stagingFileInfo.absolutePath, project.postfix); + stagingFileInfo.relativePath = fileUtils.unPostfixFilePath(stagingFileInfo.relativePath, project.postfix); } let sourceLocation = await this.locationManager.getSourceLocation({ @@ -252,6 +252,11 @@ export class Project { public injectRdbOnDeviceComponent: boolean; public rdbFilesBasePath: string; + //the default project doesn't have a postfix, but component libraries will have a postfix, so just use empty string to standardize the postfix logic + public get postfix() { + return ''; + } + private logger = logger.createLogger(`[${ProjectManager.name}]`); public async stage() { @@ -577,22 +582,12 @@ export class ComponentLibraryProject extends Project { let relativePath = fileUtils.removeLeadingSlash( fileUtils.getRelativePath(this.stagingFolderPath, fileMapping.dest) ); - let parsedPath = path.parse(relativePath); - - if (parsedPath.ext) { - let originalRelativePath = relativePath; - - if (parsedPath.ext === '.brs') { - // Create the new file name to be used - let newFileName = `${parsedPath.name}${this.postfix}${parsedPath.ext}`; - relativePath = path.join(parsedPath.dir, newFileName); - - // Rename the brs files to include the postfix namespacing tag - await fsExtra.move(fileMapping.dest, path.join(this.stagingFolderPath, relativePath)); - } - + let postfixedPath = fileUtils.postfixFilePath(relativePath, this.postfix, ['.brs']); + if (postfixedPath !== relativePath) { + // Rename the brs files to include the postfix namespacing tag + await fsExtra.move(fileMapping.dest, path.join(this.stagingFolderPath, postfixedPath)); // Add to the map of original paths and the new paths - pathDetails[relativePath] = originalRelativePath; + pathDetails[postfixedPath] = relativePath; } })); @@ -608,17 +603,4 @@ export class ComponentLibraryProject extends Project { } }); } - - /** - * Given a file path, return a new path with the component library postfix removed - */ - public removeFileNamePostfix(filePath: string) { - let parts = path.parse(filePath); - let postfix = `${this.postfix}${parts.ext}`; - if (filePath.toLowerCase().endsWith(postfix.toLowerCase())) { - return fileUtils.replaceCaseInsensitive(filePath, postfix, parts.ext); - } else { - return filePath; - } - } } From 3a89a2ece3385ada69c6bbea9b0fd370f9eae626 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 9 Jun 2022 10:39:13 -0400 Subject: [PATCH 022/197] update changelog for v0.13.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5596b43..c64343be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.13.1](https://github.com/rokucommunity/roku-debug/compare/v0.13.0...v0.13.1) - 2022-06-09 +### Fixed + - dynamic breakpoints bug where component library breakpoints weren't being hit ([#89](https://github.com/rokucommunity/roku-debug/pull/89)) + + + ## [0.13.0](https://github.com/rokucommunity/roku-debug/compare/v0.12.2...v0.13.0) - 2022-06-08 ### Added - Support for dynamic breakpoints when using Debug Protocol ([#84](https://github.com/rokucommunity/roku-debug/pull/84)) From d51d5ea6cf13fcecbea12820a9e63b7400fd85ec Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 9 Jun 2022 10:39:54 -0400 Subject: [PATCH 023/197] 0.13.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60e3eee8..818b9c5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.13.0", + "version": "0.13.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.13.0", + "version": "0.13.1", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index d22e61c6..1a0f4ae2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.13.0", + "version": "0.13.1", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From ef79d0885d3c0628c64b85dbd48cd042dc404b22 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 10 Jun 2022 06:26:07 -0400 Subject: [PATCH 024/197] Show error when cannot resolve hostname (#90) * Show error when cannot resolve hostname * throw original error --- src/debugSession/BrightScriptDebugSession.ts | 15 +++++++++++++-- src/debugSession/Events.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index f7f591f6..f73565f8 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -38,7 +38,8 @@ import { StoppedEventReason, ChanperfEvent, DebugServerLogOutputEvent, - ChannelPublishedEvent + ChannelPublishedEvent, + PopupMessageEvent } from './Events'; import type { LaunchConfiguration, ComponentLibraryConfiguration } from '../LaunchConfiguration'; import { FileManager } from '../managers/FileManager'; @@ -180,6 +181,10 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.logger.log('initializeRequest finished'); } + private showPopupMessage(message: string, severity: 'error' | 'warn' | 'info') { + this.sendEvent(new PopupMessageEvent(message, severity)); + } + public async launchRequest(response: DebugProtocol.LaunchResponse, config: LaunchConfiguration) { this.logger.log('[launchRequest] begin'); this.launchConfiguration = config; @@ -190,7 +195,13 @@ export class BrightScriptDebugSession extends BaseDebugSession { } //do a DNS lookup for the host to fix issues with roku rejecting ECP - this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host); + try { + this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host); + } catch (e) { + const errorMessage = `Could not resolve ip address for "${this.launchConfiguration.host}"`; + this.showPopupMessage(errorMessage, 'error'); + throw e; + } this.projectManager.launchConfiguration = this.launchConfiguration; this.breakpointManager.launchConfiguration = this.launchConfiguration; diff --git a/src/debugSession/Events.ts b/src/debugSession/Events.ts index 16f91d26..9938bb67 100644 --- a/src/debugSession/Events.ts +++ b/src/debugSession/Events.ts @@ -70,6 +70,18 @@ export class LaunchStartEvent implements DebugProtocol.Event { public type: string; } +export class PopupMessageEvent implements DebugProtocol.Event { + constructor(message: string, severity: 'error' | 'info' | 'warn') { + this.body = { message, severity }; + this.event = 'BSPopupMessageEvent'; + } + + public body: any; + public event: string; + public seq: number; + public type: string; +} + export class ChannelPublishedEvent implements DebugProtocol.Event { constructor( body: { From 6592b76dcbeded74532eadb3c51b9c1e5762c0f4 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 14 Jul 2022 15:45:57 -0400 Subject: [PATCH 025/197] Case-sensitivity in getVariables protocol request (#91) --- src/adapters/DebugProtocolAdapter.ts | 3 +- src/debugProtocol/Constants.ts | 31 ++++++++++++- src/debugProtocol/Debugger.spec.ts | 69 +++++++++++++++++++++++++++- src/debugProtocol/Debugger.ts | 42 +++++++++++++++-- src/util.spec.ts | 14 +++--- src/util.ts | 3 +- 6 files changed, 146 insertions(+), 16 deletions(-) diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 2b8ad6a5..14a16495 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -211,7 +211,7 @@ export class DebugProtocolAdapter { // TODO: Update once we know the exact version of the debug protocol this issue was fixed in. // Due to casing issues with variables on protocol version and under we first need to try the request in the supplied case. // If that fails we retry in lower case. - this.enableVariablesLowerCaseRetry = semver.satisfies(this.activeProtocolVersion, '*'); + this.enableVariablesLowerCaseRetry = semver.satisfies(this.activeProtocolVersion, '<3.1.0'); // While execute was added as a command in 2.1.0. It has shortcoming that prevented us for leveraging the command. // This was mostly addressed in the 3.0.0 release to the point where we were comfortable adding support for the command. this.supportsExecuteCommand = semver.satisfies(this.activeProtocolVersion, '>=3.0.0'); @@ -478,6 +478,7 @@ export class DebugProtocolAdapter { response = await this.socketDebugger.getVariables(variablePath, withChildren, frame.frameIndex, frame.threadIndex); } + if (response.errorCode === ERROR_CODES.OK) { let mainContainer: EvaluateContainer; let children: EvaluateContainer[] = []; diff --git a/src/debugProtocol/Constants.ts b/src/debugProtocol/Constants.ts index e016e7c4..fe1b6866 100644 --- a/src/debugProtocol/Constants.ts +++ b/src/debugProtocol/Constants.ts @@ -51,13 +51,42 @@ export enum UPDATE_TYPES { THREAD_ATTACHED = 3 } +export enum VARIABLE_REQUEST_FLAGS { + GET_CHILD_KEYS = 0x01, + CASE_SENSITIVITY_OPTIONS = 0x02 +} + export enum VARIABLE_FLAGS { + /** + * value is a child of the requested variable + * e.g., an element of an array or field of an AA + */ isChildKey = 0x01, + /** + * value is constant + */ isConst = 0x02, + /** + * The referenced value is a container (e.g., a list or array) + */ isContainer = 0x04, + /** + * The name is included in this VariableInfo + */ isNameHere = 0x08, + /** + * value is reference-counted. + */ isRefCounted = 0x10, - isValueHere = 0x20 + /** + * value is included in this VariableInfo + */ + isValueHere = 0x20, + /** + * Value is container, key lookup is case sensitive + * @since protocol 3.1.0 + */ + isKeysCaseSensitive = 0x40 } export enum VARIABLE_TYPES { diff --git a/src/debugProtocol/Debugger.spec.ts b/src/debugProtocol/Debugger.spec.ts index 6f2fc61e..7869de75 100644 --- a/src/debugProtocol/Debugger.spec.ts +++ b/src/debugProtocol/Debugger.spec.ts @@ -5,7 +5,7 @@ import { MockDebugProtocolServer } from './MockDebugProtocolServer.spec'; import { createSandbox } from 'sinon'; import { createHandShakeResponse, createHandShakeResponseV3, createProtocolEventV3 } from './responses/responseCreationHelpers.spec'; import { HandshakeResponseV3, ProtocolEventV3 } from './responses'; -import { ERROR_CODES, UPDATE_TYPES } from './Constants'; +import { ERROR_CODES, UPDATE_TYPES, VARIABLE_REQUEST_FLAGS } from './Constants'; const sinon = createSandbox(); describe('debugProtocol Debugger', () => { @@ -147,4 +147,71 @@ describe('debugProtocol Debugger', () => { expect(calls[1].args[0]).instanceOf(ProtocolEventV3); }); }); + + describe('getVariables', () => { + function getVariablesRequestBufferToJson(buffer: SmartBuffer) { + const result = { + flags: buffer.readUInt8(), + threadIndex: buffer.readUInt32LE(), + stackFrameIndex: buffer.readUInt32LE(), + variablePathEntries: [], + pathForceCaseInsensitive: [] + }; + + const pathLength = buffer.readUInt32LE(); + if (pathLength > 0) { + result.variablePathEntries = []; + for (let i = 0; i < pathLength; i++) { + result.variablePathEntries.push( + buffer.readBufferNT().toString() + ); + } + } + // eslint-disable-next-line no-bitwise + if (result.flags & VARIABLE_REQUEST_FLAGS.CASE_SENSITIVITY_OPTIONS) { + result.pathForceCaseInsensitive = []; + for (let i = 0; i < pathLength; i++) { + result.pathForceCaseInsensitive.push( + buffer.readUInt8() === 0 ? false : true + ); + } + } + return result; + } + + it('skips case sensitivity info on lower protocol versions', async () => { + bsDebugger.protocolVersion = '2.0.0'; + bsDebugger['stopped'] = true; + const stub = sinon.stub(bsDebugger as any, 'makeRequest').callsFake(() => { }); + await bsDebugger.getVariables(['m', 'top'], false, 1, 2); + expect( + getVariablesRequestBufferToJson(stub.getCalls()[0].args[0]) + ).to.eql({ + flags: 0, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: ['m', 'top'], + //should be empty + pathForceCaseInsensitive: [] + }); + }); + + it('marks strings as case-sensitive', async () => { + bsDebugger.protocolVersion = '3.1.0'; + bsDebugger['stopped'] = true; + const stub = sinon.stub(bsDebugger as any, 'makeRequest').callsFake(() => { }); + await bsDebugger.getVariables(['m', 'top', '"someKey"', '""someKeyWithInternalQuotes""'], true, 1, 2); + expect( + getVariablesRequestBufferToJson(stub.getCalls()[0].args[0]) + ).to.eql({ + // eslint-disable-next-line no-bitwise + flags: VARIABLE_REQUEST_FLAGS.GET_CHILD_KEYS | VARIABLE_REQUEST_FLAGS.CASE_SENSITIVITY_OPTIONS, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: ['m', 'top', 'someKey', '"someKeyWithInternalQuotes"'], + //should be empty + pathForceCaseInsensitive: [true, true, false, false] + }); + }); + }); }); diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 1f7181bb..f073c841 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -18,7 +18,7 @@ import { UpdateThreadsResponse, VariableResponse } from './responses'; -import { PROTOCOL_ERROR_CODES, COMMANDS, STEP_TYPE, STOP_REASONS } from './Constants'; +import { PROTOCOL_ERROR_CODES, COMMANDS, STEP_TYPE, STOP_REASONS, VARIABLE_REQUEST_FLAGS } from './Constants'; import { SmartBuffer } from 'smart-buffer'; import { logger } from '../logging'; import { ERROR_CODES, UPDATE_TYPES } from '..'; @@ -238,16 +238,50 @@ export class Debugger { } } + /** + * @param variablePathEntries One or more path entries to the variable to be inspected. E.g., m.top.myObj["someKey"] can be accessed with ["m","top","myobj","\"someKey\""]. + * + * If no path is specified, the variables accessible from the specified stack frame are returned. + * + * Starting in protocol v3.1.0, The keys for indexed gets (i.e. obj["key"]) should be wrapped in quotes so they can be handled in a case-sensitive fashion (if applicable on device). + * All non-quoted keys (i.e. strings without leading and trailing quotes inside them) will be treated as case-insensitive). + * @param getChildKeys If set, VARIABLES response include the child keys for container types like lists and associative arrays + * @param stackFrameIndex 0 = first function called, nframes-1 = last function. This indexing does not match the order of the frames returned from the STACKTRACE command + * @param threadIndex the index (or perhaps ID?) of the thread to get variables for + */ public async getVariables(variablePathEntries: Array = [], getChildKeys = true, stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) { if (this.stopped && threadIndex > -1) { + //starting in protocol v3.1.0, it supports marking certain path items as case-insensitive (i.e. parts of DottedGet expressions) + const sendCaseInsensitiveData = semver.satisfies(this.protocolVersion, '>=3.1.0') && variablePathEntries.length > 0; let buffer = new SmartBuffer({ size: 17 }); - buffer.writeUInt8(getChildKeys ? 1 : 0); // variable_request_flags + let flags = 0; + if (getChildKeys) { + // eslint-disable-next-line no-bitwise + flags |= VARIABLE_REQUEST_FLAGS.GET_CHILD_KEYS; + } + if (sendCaseInsensitiveData) { + // eslint-disable-next-line no-bitwise + flags |= VARIABLE_REQUEST_FLAGS.CASE_SENSITIVITY_OPTIONS; + } + buffer.writeUInt8(flags); // variable_request_flags buffer.writeUInt32LE(threadIndex); // thread_index buffer.writeUInt32LE(stackFrameIndex); // stack_frame_index buffer.writeUInt32LE(variablePathEntries.length); // variable_path_len - variablePathEntries.forEach(variablePathEntry => { - buffer.writeStringNT(variablePathEntry); // variable_path_entries - optional + variablePathEntries.forEach(entry => { + if (entry.startsWith('"') && entry.endsWith('"')) { + //remove leading and trailing quotes + entry = entry.substring(1, entry.length - 1); + } + buffer.writeStringNT(entry); // variable_path_entries - optional }); + if (sendCaseInsensitiveData) { + variablePathEntries.forEach(entry => { + buffer.writeUInt8( + //0 means case SENSITIVE lookup, 1 means case INsensitive lookup + entry.startsWith('"') ? 0 : 1 + ); + }); + } return this.makeRequest(buffer, COMMANDS.VARIABLES, variablePathEntries); } } diff --git a/src/util.spec.ts b/src/util.spec.ts index 474c73d3..e4a1a380 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -268,18 +268,18 @@ describe('Util', () => { expect(util.getVariablePath('a[0]')).to.eql(['a', '0']); expect(util.getVariablePath('a[0].b')).to.eql(['a', '0', 'b']); expect(util.getVariablePath('a[0].b[0]')).to.eql(['a', '0', 'b', '0']); - expect(util.getVariablePath('a["b"]')).to.eql(['a', 'b']); - expect(util.getVariablePath('a["b"]["c"]')).to.eql(['a', 'b', 'c']); - expect(util.getVariablePath('a["b"][0]')).to.eql(['a', 'b', '0']); - expect(util.getVariablePath('a["b"].c[0]')).to.eql(['a', 'b', 'c', '0']); - expect(util.getVariablePath(`m_that["this -that.thing"] .other[9]`)).to.eql(['m_that', 'this -that.thing', 'other', '9']); + expect(util.getVariablePath('a["b"]')).to.eql(['a', '"b"']); + expect(util.getVariablePath('a["b"]["c"]')).to.eql(['a', '"b"', '"c"']); + expect(util.getVariablePath('a["b"][0]')).to.eql(['a', '"b"', '0']); + expect(util.getVariablePath('a["b"].c[0]')).to.eql(['a', '"b"', 'c', '0']); + expect(util.getVariablePath(`m_that["this -that.thing"] .other[9]`)).to.eql(['m_that', '"this -that.thing"', 'other', '9']); expect(util.getVariablePath(`a`)).to.eql(['a']); expect(util.getVariablePath(`boy5`)).to.eql(['boy5']); expect(util.getVariablePath(`super_man$`)).to.eql(['super_man$']); expect(util.getVariablePath(`_super_man$`)).to.eql(['_super_man$']); - expect(util.getVariablePath(`a["something with a quote"].c`)).to.eql(['a', 'something with a quote', 'c']); + expect(util.getVariablePath(`a["something with a quote"].c`)).to.eql(['a', '"something with a quote"', 'c']); expect(util.getVariablePath(`m.global.initialInputEvent`)).to.eql(['m', 'global', 'initialInputEvent']); - expect(util.getVariablePath(`m.["that"]`)).to.eql(['m', 'that']); + expect(util.getVariablePath(`m.["that"]`)).to.eql(['m', '"that"']); }); it('rejects invalid patterns', () => { diff --git a/src/util.ts b/src/util.ts index e3f9d512..56eb203e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -308,8 +308,7 @@ class Util { } else if (isIndexedGetExpression(value)) { if (isLiteralExpression(value.index)) { parts.unshift( - //remove leading and trailing quotes (won't hurt for numeric literals) - value.index.token.text?.replace(/^"/, '').replace(/"$/, '') + value.index.token.text ); } else { //if we found a non-literal value, this entire variable path is NOT a true variable path From b1ed3e2f129a1523c1bc101901a8d5bb0d3ef4a2 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 14 Jul 2022 16:06:22 -0400 Subject: [PATCH 026/197] Update changelog for v0.14.0 --- CHANGELOG.md | 9 +++++++++ package-lock.json | 14 +++++++------- package.json | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c64343be..ae664497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.14.0](https://github.com/rokucommunity/roku-debug/compare/v0.13.1...0.14.0) - 2022-07-14 +### Added + - debug protocol: support for case-sensitivity in getVariables protocol request ([#91](https://github.com/rokucommunity/roku-debug/pull/91)) + - Show error when cannot resolve hostname ([#90](https://github.com/rokucommunity/roku-debug/pull/90)) +### Changed + - upgrade to [brighterscript@0.53.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0530---undefined) + + + ## [0.13.1](https://github.com/rokucommunity/roku-debug/compare/v0.13.0...v0.13.1) - 2022-06-09 ### Fixed - dynamic breakpoints bug where component library breakpoints weren't being hit ([#89](https://github.com/rokucommunity/roku-debug/pull/89)) diff --git a/package-lock.json b/package-lock.json index 818b9c5c..4e0af4ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.52.0", + "brighterscript": "^0.53.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.52.0.tgz", - "integrity": "sha512-pEopdyaB73knCtmTo2YK+Ai6uEYsRShMn1Paa8pTJKG08GIfzutL3PyImVh+KK5+JHLugjzUDnKAe3pXfHKblg==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.0.tgz", + "integrity": "sha512-YNSlD5T93GVdZ3bL/r076bo0PRCPzOGcGY+np2v/Uo17C+j1glMmXh8dDrsNajNRGDZdwMhUmmK0YRVP1ctnnQ==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5553,9 +5553,9 @@ } }, "brighterscript": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.52.0.tgz", - "integrity": "sha512-pEopdyaB73knCtmTo2YK+Ai6uEYsRShMn1Paa8pTJKG08GIfzutL3PyImVh+KK5+JHLugjzUDnKAe3pXfHKblg==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.0.tgz", + "integrity": "sha512-YNSlD5T93GVdZ3bL/r076bo0PRCPzOGcGY+np2v/Uo17C+j1glMmXh8dDrsNajNRGDZdwMhUmmK0YRVP1ctnnQ==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", diff --git a/package.json b/package.json index 1a0f4ae2..91fd5f09 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.52.0", + "brighterscript": "^0.53.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From 2949da872a2b832d4fa75dc87a49872fa0a5db36 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 14 Jul 2022 16:06:53 -0400 Subject: [PATCH 027/197] 0.14.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e0af4ac..b231e92f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.13.1", + "version": "0.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.13.1", + "version": "0.14.0", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index 91fd5f09..8b25f698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.13.1", + "version": "0.14.0", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From 9342d6e4c5b5412f82ac8c03218d58205462887c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Jul 2022 23:16:31 -0400 Subject: [PATCH 028/197] Bump moment from 2.29.2 to 2.29.4 (#92) Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4) --- updated-dependencies: - dependency-name: moment dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b231e92f..e7ec54a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3317,9 +3317,9 @@ } }, "node_modules/moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "engines": { "node": "*" } @@ -6905,9 +6905,9 @@ } }, "moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "ms": { "version": "2.1.2", From f926472002718d8091cce66ef806ae75014b720b Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 16 Jul 2022 07:09:26 -0400 Subject: [PATCH 029/197] Update changelog for v0.14.1 --- CHANGELOG.md | 10 +++++++++- package-lock.json | 14 +++++++------- package.json | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae664497..b2b92fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.14.1](https://github.com/rokucommunity/roku-debug/compare/v0.14.0...0.14.1) - 2022-07-16 +### Changed + - Bump moment from 2.29.2 to 2.29.4 ([#92](https://github.com/rokucommunity/roku-debug/pull/92)) + - upgrade to [brighterscript@0.53.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0531---2022-07-15). Notable changes since 0.53.0: + - Bump moment from 2.29.2 to 2.29.4 ([brighterscript#640](https://github.com/rokucommunity/brighterscript/pull/640)) + + + ## [0.14.0](https://github.com/rokucommunity/roku-debug/compare/v0.13.1...0.14.0) - 2022-07-14 ### Added - debug protocol: support for case-sensitivity in getVariables protocol request ([#91](https://github.com/rokucommunity/roku-debug/pull/91)) - Show error when cannot resolve hostname ([#90](https://github.com/rokucommunity/roku-debug/pull/90)) ### Changed - - upgrade to [brighterscript@0.53.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0530---undefined) + - upgrade to [brighterscript@0.53.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0530---2022-07-14) diff --git a/package-lock.json b/package-lock.json index e7ec54a1..12469155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.53.0", + "brighterscript": "^0.53.1", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.0.tgz", - "integrity": "sha512-YNSlD5T93GVdZ3bL/r076bo0PRCPzOGcGY+np2v/Uo17C+j1glMmXh8dDrsNajNRGDZdwMhUmmK0YRVP1ctnnQ==", + "version": "0.53.1", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.1.tgz", + "integrity": "sha512-uq6iPHV2W/8yXg2CFHp8DVT4824nrqVGdM0DyDtRUxt1GWyRr5PeIAMIse1vPNDzw33F/P2ZrHI9ceab2MIL1A==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5553,9 +5553,9 @@ } }, "brighterscript": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.0.tgz", - "integrity": "sha512-YNSlD5T93GVdZ3bL/r076bo0PRCPzOGcGY+np2v/Uo17C+j1glMmXh8dDrsNajNRGDZdwMhUmmK0YRVP1ctnnQ==", + "version": "0.53.1", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.1.tgz", + "integrity": "sha512-uq6iPHV2W/8yXg2CFHp8DVT4824nrqVGdM0DyDtRUxt1GWyRr5PeIAMIse1vPNDzw33F/P2ZrHI9ceab2MIL1A==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", diff --git a/package.json b/package.json index 8b25f698..fa758349 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.53.0", + "brighterscript": "^0.53.1", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From 110331326c44131256deda1fe0ffbd26bdc5f2b3 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 16 Jul 2022 07:10:04 -0400 Subject: [PATCH 030/197] 0.14.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12469155..bc3fa21d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.14.0", + "version": "0.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.14.0", + "version": "0.14.1", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index fa758349..965bb239 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.14.0", + "version": "0.14.1", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From a7db9a3a67780db59a1ae289991c236508336fad Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 10 Aug 2022 13:33:48 -0400 Subject: [PATCH 031/197] Upload zip and connect to protocol socket in parallel (#94) * Connect to protocol socket at same time as publish * remove unused promise * remove .only test * Don't kill the debugger on tcp error * Fix controller port retry logic. Rename uti.retry options * better crash handling * remove debugging code --- src/adapters/DebugProtocolAdapter.ts | 7 +- src/adapters/TelnetAdapter.ts | 3 + src/debugProtocol/Debugger.ts | 79 +++++++++++++------- src/debugSession/BrightScriptDebugSession.ts | 44 +++++++++-- src/util.spec.ts | 56 ++++++++++++++ src/util.ts | 43 +++++++++++ 6 files changed, 196 insertions(+), 36 deletions(-) diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 14a16495..0da06161 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -1,4 +1,4 @@ -import type { ProtocolVersionDetails } from '../debugProtocol/Debugger'; +import type { ConstructorOptions, ProtocolVersionDetails } from '../debugProtocol/Debugger'; import { Debugger } from '../debugProtocol/Debugger'; import * as EventEmitter from 'events'; import { Socket } from 'net'; @@ -22,7 +22,7 @@ import { ActionQueue } from '../managers/ActionQueue'; */ export class DebugProtocolAdapter { constructor( - private options: AdapterOptions, + private options: AdapterOptions & ConstructorOptions, private projectManager: ProjectManager, private breakpointManager: BreakpointManager ) { @@ -46,6 +46,9 @@ export class DebugProtocolAdapter { private logger = logger.createLogger(`[${DebugProtocolAdapter.name}]`); + /** + * Indicates whether the adapter has successfully established a connection with the device + */ public connected: boolean; /** diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 67a5608b..73e44165 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -48,6 +48,9 @@ export class TelnetAdapter { } public logger = logger.createLogger(`[${TelnetAdapter.name}]`); + /** + * Indicates whether the adapter has successfully established a connection with the device + */ public connected: boolean; private compileErrorProcessor: CompileErrorProcessor; diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index f073c841..36daac14 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -26,6 +26,7 @@ import { ExecuteResponseV3 } from './responses/ExecuteResponseV3'; import { ListBreakpointsResponse } from './responses/ListBreakpointsResponse'; import { AddBreakpointsResponse } from './responses/AddBreakpointsResponse'; import { RemoveBreakpointsResponse } from './responses/RemoveBreakpointsResponse'; +import { util } from '../util'; export class Debugger { @@ -70,6 +71,13 @@ export class Debugger { /** * Get a promise that resolves after an event occurs exactly once */ + public once(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start'): Promise; + public once(eventName: 'data'): Promise; + public once(eventName: 'runtime-error' | 'suspend'): Promise; + public once(eventName: 'connected'): Promise; + public once(eventName: 'io-output'): Promise; + public once(eventName: 'protocol-version'): Promise; + public once(eventName: 'handshake-verified'): Promise; public once(eventName: string) { return new Promise((resolve) => { const disconnect = this.on(eventName as Parameters[0], (...args) => { @@ -113,21 +121,36 @@ export class Debugger { const debugSetupEnd = 'total socket debugger setup time'; console.time(debugSetupEnd); - // Create a new TCP client.` - this.controllerClient = new Net.Socket(); - // Send a connection request to the server. + await util.retry(() => { + this.controllerClient = new Net.Socket(); + return new Promise((resolve) => { + this.controllerClient.once('error', (error) => { + console.error('Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error); + this.controllerClient?.destroy(); + }); + this.controllerClient.connect({ port: this.options.controllerPort, host: this.options.host }, () => { + resolve(this.controllerClient); + }); + }); + }, { + onCancel: async () => { + this.controllerClient?.destroy(); + this.controllerClient = undefined; + //small timeout to let the connection destroy settle + await util.sleep(5); + }, + tryTime: this.options.controllerConnectInterval ?? 250, + totalTime: this.options.controllerConnectMaxTime ?? 5 * 60 * 1000 //5 minutes + }); - this.controllerClient.connect({ port: this.options.controllerPort, host: this.options.host }, () => { - // If there is no error, the server has accepted the request and created a new - // socket dedicated to us. - this.logger.log('TCP connection established with the server.'); + // If there is no error, the server has accepted the request and created a new dedicated socket + this.logger.log('TCP connection established with the server.'); - // The client can also receive data from the server by reading from its socket. - // The client can now send data to the server by writing to its socket. - let buffer = new SmartBuffer({ size: Buffer.byteLength(Debugger.DEBUGGER_MAGIC) + 1 }).writeStringNT(Debugger.DEBUGGER_MAGIC).toBuffer(); - this.logger.log('Sending magic to server'); - this.controllerClient.write(buffer); - }); + // The client can also receive data from the server by reading from its socket. + // The client can now send data to the server by writing to its socket. + let buffer = new SmartBuffer({ size: Buffer.byteLength(Debugger.DEBUGGER_MAGIC) + 1 }).writeStringNT(Debugger.DEBUGGER_MAGIC).toBuffer(); + this.logger.log('Sending magic to server'); + this.controllerClient.write(buffer); this.controllerClient.on('data', (buffer) => { if (this.unhandledData) { @@ -150,19 +173,9 @@ export class Debugger { this.shutdown('close'); }); - let connectPromise: Promise = new Promise((resolve, reject) => { - let disconnect = this.on('connected', (connected) => { - disconnect(); - console.timeEnd(debugSetupEnd); - if (connected) { - resolve(connected); - } else { - reject(connected); - } - }); - }); - - return connectPromise; + const isConnected = await this.once('connected'); + console.timeEnd(debugSetupEnd); + return isConnected; } public async continue() { @@ -618,7 +631,6 @@ export interface BreakpointSpec { hitCount?: number; } - export interface ConstructorOptions { /** * The host/ip address of the Roku @@ -629,4 +641,17 @@ export interface ConstructorOptions { * but is configurable here to support unit testing or alternate runtimes (i.e. https://www.npmjs.com/package/brs) */ controllerPort?: number; + /** + * The interval (in milliseconds) for how frequently the `connect` + * call should retry connecting to the controller port. At the start of a debug session, + * the protocol debugger will start trying to connect the moment the channel is sideloaded, + * and keep trying until a successful connection is established or the debug session is terminated + * @default 250 + */ + controllerConnectInterval?: number; + /** + * The maximum time (in milliseconds) the debugger will keep retrying connections. + * This is here to prevent infinitely pinging the Roku device. + */ + controllerConnectMaxTime?: number; } diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index f73565f8..9c12a3da 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -49,6 +49,7 @@ import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager'; import { BreakpointManager } from '../managers/BreakpointManager'; import type { LogMessage } from '../logging'; import { logger, debugServerLogOutputEventTransport } from '../logging'; +import { waitForDebugger } from 'inspector'; export class BrightScriptDebugSession extends BaseDebugSession { public constructor() { @@ -327,18 +328,12 @@ export class BrightScriptDebugSession extends BaseDebugSession { // Set the remote debug flag on the args to be passed to roku deploy so the socket debugger can be started if needed. (this.launchConfiguration as any).remoteDebug = this.enableDebugProtocol; - //publish the package to the target Roku - await this.rokuDeploy.publish(this.launchConfiguration as any as RokuDeployOptions); + await this.connectAndPublish(); this.sendEvent(new ChannelPublishedEvent({ launchConfiguration: this.launchConfiguration })); - if (this.enableDebugProtocol) { - //connect to the roku debug via sockets - await this.connectRokuAdapter(); - } - //tell the adapter adapter that the channel has been launched. await this.rokuAdapter.activate(); @@ -393,6 +388,40 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } + private async connectAndPublish() { + let connectPromise: Promise; + //connect to the roku debug via sockets + if (this.enableDebugProtocol) { + connectPromise = this.connectRokuAdapter().catch(e => this.logger.error(e)); + } + + let packageIsPublished = false; + //publish the package to the target Roku + const publishPromise = this.rokuDeploy.publish(this.launchConfiguration as any as RokuDeployOptions).then(() => { + packageIsPublished = true; + }); + + await publishPromise; + + //the channel has been deployed. Wait for the adapter to finish connecting. + //if it hasn't connected after 5 seconds, it probably will never connect. + await Promise.race([ + connectPromise, + util.sleep(5000) + ]); + this.logger.log('Finished racing promises'); + //if the adapter is still not connected, then it will probably never connect. Abort. + if (packageIsPublished && !this.rokuAdapter.connected) { + //kill the session cuz it won't ever come back + await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + const message = 'Debug session cancelled: failed to connect to debug protocol control port.'; + this.showPopupMessage(message, 'error'); + this.logger.error(message); + this.shutdown(); + this.sendEvent(new TerminatedEvent()); + } + } + /** * Send log output to the "client" (i.e. vscode) * @param logOutput @@ -1058,6 +1087,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.rokuAdapter.on('cannot-continue', () => { this.sendEvent(new TerminatedEvent()); }); + //make the connection await this.rokuAdapter.connect(); this.rokuAdapterDeferred.resolve(this.rokuAdapter); diff --git a/src/util.spec.ts b/src/util.spec.ts index e4a1a380..cb0d793b 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -430,4 +430,60 @@ describe('Util', () => { ).to.be.true; }); }); + + describe('retry', () => { + it('kills on first error encountered', async () => { + let tryCount = 0; + let cancelCount = 0; + await util.retry(() => { + tryCount++; + throw new Error('Crash'); + }, { + onCancel: () => { + cancelCount++; + }, + tryTime: 4, + totalTime: 10 + }).catch(() => { /*ignore the error */ }); + expect(tryCount).to.equal(1); + expect(cancelCount).to.equal(0); + }); + + it('kills long-running tries', async () => { + let tryCount = 0; + let cancelCount = 0; + const result = await util.retry(() => { + tryCount++; + if (tryCount >= 3) { + return true; + } else { + return util.sleep(50); + } + }, { + onCancel: () => { + cancelCount++; + }, + tryTime: 4, + totalTime: 100 + }); + expect(result).to.be.true; + expect(tryCount).to.equal(3); + expect(cancelCount).to.equal(tryCount - 1); + }); + + it('kills tries that exceed the total runtime', async () => { + try { + await util.retry(() => { + return util.sleep(50); + }, { + onCancel: () => { + }, + tryTime: 4000, + totalTime: 10 + }); + } catch (e) { + expect(e.message).to.equal('Total allotted time exceeded'); + } + }); + }); }); diff --git a/src/util.ts b/src/util.ts index 56eb203e..e1d4bb1e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -385,6 +385,49 @@ class Util { options.brightScriptConsolePort ??= 8085; options.remotePort ??= 8060; } + + public async retry(action: () => T, options: { + /** + * The max number of milliseconds an individual try can run + */ + tryTime: number; + /** + * The max number of milliseconds this entire operation can run. + */ + totalTime: number; + /** + * A callback that is run every time a try is canceled + */ + onCancel?: (error: Error) => void; + }): Promise { + options = { + tryTime: 100, + totalTime: 1000, + onCancel: () => { }, + ...options ?? {} + }; + const startTime = Date.now(); + while (true) { + if (Date.now() - startTime >= options.totalTime) { + throw new Error('Total allotted time exceeded'); + } + let timedOut = false; + const actionResult = Promise.resolve(action()); + const timeoutResult = this.sleep(options.tryTime).then(() => { + timedOut = true; + }); + const result = await Promise.race([actionResult, timeoutResult]); + if (timedOut) { + await Promise.resolve( + options?.onCancel?.( + new Error('Try timed out') + ) + ); + } else { + return result as T; + } + } + } } export function defer() { From 7fcd9ed9ca14a860b371b35eb56e5315ed39ce87 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 10 Aug 2022 15:51:30 -0400 Subject: [PATCH 032/197] Disable thread hopping workaround >= protocol v3.1.0 (#95) --- src/debugProtocol/Debugger.ts | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 36daac14..92b6cabe 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -68,6 +68,14 @@ export class Debugger { private activeRequests = {}; private options: ConstructorOptions; + /** + * Prior to protocol v3.1.0, the Roku device would regularly set the wrong thread as "active", + * so this flag lets us know if we should use our better-than-nothing workaround + */ + private get enableThreadHoppingWorkaround() { + return semver.satisfies(this.protocolVersion, '<3.1.0'); + } + /** * Get a promise that resolves after an event occurs exactly once */ @@ -229,16 +237,23 @@ export class Debugger { public async threads() { if (this.stopped) { let result = await this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.THREADS); - //TODO uncomment this once the device starts correctly reporting `isPrimary`. Right now our logic is better at tracking the primary thread. - // if (result.errorCode === ERROR_CODES.OK) { - // for (let i = 0; i < result.threadsCount; i++) { - // let thread = result.threads[i]; - // if (thread.isPrimary) { - // this.primaryThread = i; - // break; - // } - // } - // } + + if (result.errorCode === ERROR_CODES.OK) { + //older versions of the debug protocol had issues with maintaining the active thread, so our workaround is to keep track of it elsewhere + if (this.enableThreadHoppingWorkaround) { + //ignore the `isPrimary` flag on threads + this.logger.debug(`Ignoring the 'isPrimary' flag from threads because protocol version ${this.protocolVersion} and lower has a bug`); + } else { + //trust the debug protocol's `isPrimary` flag on threads + for (let i = 0; i < result.threadsCount; i++) { + let thread = result.threads[i]; + if (thread.isPrimary) { + this.primaryThread = i; + break; + } + } + } + } return result; } } From 564f48fdeb22a2b50d6cde9f5e631bbc624b0a4b Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 12 Aug 2022 11:13:43 -0400 Subject: [PATCH 033/197] Support complib breakpoints on 11.5.0 (#96) * Support complib breakpoints on 11.5.0 * fix failing test --- src/adapters/DebugProtocolAdapter.ts | 3 ++- src/debugProtocol/Debugger.ts | 21 ++++++++++++++++++-- src/debugSession/BrightScriptDebugSession.ts | 3 ++- src/managers/BreakpointManager.ts | 9 +++++++-- src/managers/ProjectManager.spec.ts | 2 ++ src/managers/ProjectManager.ts | 20 ++++++++++--------- src/util.ts | 2 ++ 7 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 0da06161..e5353652 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -689,7 +689,8 @@ export class DebugProtocolAdapter { filePath: breakpoint.pkgPath, lineNumber: breakpoint.line, hitCount: !isNaN(hitCount) ? hitCount : undefined, - key: breakpoint.hash + key: breakpoint.hash, + componentLibraryName: breakpoint.componentLibraryName }; }); diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 92b6cabe..4b7bb3d0 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -76,6 +76,14 @@ export class Debugger { return semver.satisfies(this.protocolVersion, '<3.1.0'); } + /** + * Starting in protocol v3.1.0, component libary breakpoints must be added in the format `lib://`, but prior they didn't require this. + * So this flag tells us which format to support + */ + private get enableComponentLibrarySpecificBreakpoints() { + return semver.satisfies(this.protocolVersion, '>=3.1.0'); + } + /** * Get a promise that resolves after an event occurs exactly once */ @@ -325,12 +333,21 @@ export class Debugger { } } - public async addBreakpoints(breakpoints: BreakpointSpec[]): Promise { + public async addBreakpoints(breakpoints: Array): Promise { + const { enableComponentLibrarySpecificBreakpoints } = this; if (breakpoints?.length > 0) { let buffer = new SmartBuffer(); buffer.writeUInt32LE(breakpoints.length); // num_breakpoints - The number of breakpoints in the breakpoints array. breakpoints.forEach((breakpoint) => { - buffer.writeStringNT(breakpoint.filePath); // file_path - The path of the source file where the breakpoint is to be inserted. + let { filePath } = breakpoint; + //protocol >= v3.1.0 requires complib breakpoints have a special prefix + if (enableComponentLibrarySpecificBreakpoints) { + if (breakpoint.componentLibraryName) { + filePath = filePath.replace(/^pkg:\//i, `lib:/${breakpoint.componentLibraryName}/`); + } + } + + buffer.writeStringNT(filePath); // file_path - The path of the source file where the breakpoint is to be inserted. buffer.writeUInt32LE(breakpoint.lineNumber); // line_number - The line number in the channel application code where the breakpoint is to be executed. buffer.writeUInt32LE(breakpoint.hitCount ?? 0); // ignore_count - The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. }); diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 9c12a3da..9492f514 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -1066,7 +1066,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.clearState(); const event: StoppedEvent = new StoppedEvent( StoppedEventReason.breakpoint, - activeThread.threadId, + //Not sure why, but sometimes there is no active thread. Just pick thread 0 to prevent the app from totally crashing + activeThread?.threadId ?? 0, '' //exception text ); // Socket debugger will always stop all threads and supports multi thread inspection. diff --git a/src/managers/BreakpointManager.ts b/src/managers/BreakpointManager.ts index 2f2db87b..ad594e09 100644 --- a/src/managers/BreakpointManager.ts +++ b/src/managers/BreakpointManager.ts @@ -4,7 +4,7 @@ import type { CodeWithSourceMap } from 'source-map'; import { SourceNode } from 'source-map'; import type { DebugProtocol } from 'vscode-debugprotocol'; import { fileUtils, standardizePath } from '../FileUtils'; -import type { Project } from './ProjectManager'; +import type { ComponentLibraryProject, Project } from './ProjectManager'; import { standardizePath as s } from 'roku-deploy'; import type { SourceMapManager } from './SourceMapManager'; import type { LocationManager } from './LocationManager'; @@ -280,7 +280,8 @@ export class BreakpointManager { column: stagingLocation.columnIndex, stagingFilePath: stagingLocation.filePath, type: stagingLocationsResult.type, - pkgPath: pkgPath + pkgPath: pkgPath, + componentLibraryName: (project as ComponentLibraryProject).name }; if (!result[stagingLocation.filePath]) { result[stagingLocation.filePath] = []; @@ -720,6 +721,10 @@ export interface BreakpointWorkItem { * If set, this breakpoint will emit a log message at runtime and will not actually stop at the breakpoint */ logMessage?: string; + /** + * The name of the component library this belongs to. Will be null for the main project + */ + componentLibraryName?: string; /** * `sourceMap` means derived from a source map. * `fileMap` means derived from the {src;dest} entry used by roku-deploy diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 912c2367..4a24e407 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; import * as path from 'path'; +import { util } from '../util'; import { rokuDeploy } from 'roku-deploy'; import * as sinonActual from 'sinon'; import { fileUtils, standardizePath as s } from '../FileUtils'; @@ -676,6 +677,7 @@ describe('ComponentLibraryProject', () => { { src: s`${rootDir}/source/main.brs`, dest: s`source/main.brs` } ])); sinon.stub(Project.prototype, 'stage').returns(Promise.resolve()); + sinon.stub(util, 'convertManifestToObject').returns(Promise.resolve({})); await project.stage(); expect(project.fileMappings[0]).to.eql({ diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 782bef32..03973562 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -501,6 +501,10 @@ export class ComponentLibraryProject extends Project { } public outFile: string; public libraryIndex: number; + /** + * The name of the component library that this project represents. This is loaded during `this.computeOutFileName` + */ + public name: string; /** * Takes a component Library and checks the outFile for replaceable values pulled from the libraries manifest @@ -509,18 +513,16 @@ export class ComponentLibraryProject extends Project { private async computeOutFileName(manifestPath: string) { let regexp = /\$\{([\w\d_]*)\}/; let renamingMatch: RegExpExecArray; - let manifestValues: Record; + let manifestValues = await util.convertManifestToObject(manifestPath); + if (!manifestValues) { + throw new Error(`Cannot find manifest file at "${manifestPath}"\n\nCould not complete automatic component library naming.`); + } + + //load the component libary name from the manifest + this.name = manifestValues.sg_component_libs_provided; // search the outFile for replaceable values such as ${title} while ((renamingMatch = regexp.exec(this.outFile))) { - if (!manifestValues) { - // The first time a value is found we need to get the manifest values - manifestValues = await util.convertManifestToObject(manifestPath); - - if (!manifestValues) { - throw new Error(`Cannot find manifest file at "${manifestPath}"\n\nCould not complete automatic component library naming.`); - } - } // replace the replaceable key with the manifest value let manifestVariableName = renamingMatch[1]; diff --git a/src/util.ts b/src/util.ts index e1d4bb1e..f9d0ca86 100644 --- a/src/util.ts +++ b/src/util.ts @@ -92,6 +92,8 @@ class Util { let match = /(\w+)=(.+)/.exec(line); if (match) { manifestValues[match[1]] = match[2]; + //add match in all lower case too (for consistency) + manifestValues[match[1]?.toLowerCase()] = match[2]; } } From 027c82479033d9fe36c762d7132d46c23f86368e Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 12 Aug 2022 11:29:34 -0400 Subject: [PATCH 034/197] Update changelog for v0.14.2 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 26 +++++++++++++++++++------- package.json | 2 +- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b92fe0..50997f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.14.2](https://github.com/rokucommunity/roku-debug/compare/v0.14.1...0.14.2) - 2022-08-12 +### Changed + - Support complib breakpoints on 11.5.0 ([#96](https://github.com/rokucommunity/roku-debug/pull/96)) + - Disable thread hopping workaround >= protocol v3.1.0 ([#95](https://github.com/rokucommunity/roku-debug/pull/95)) + - Upload zip and connect to protocol socket in parallel ([#94](https://github.com/rokucommunity/roku-debug/pull/94)) + - upgrade to [brighterscript@0.55.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0551---2022-08-07). Notable changes since 0.53.1: + - Fix typescript error for ast parent setting ([brighterscript#659](https://github.com/rokucommunity/brighterscript/pull/659)) + - Performance boost: better function sorting during validation ([brighterscript#651](https://github.com/rokucommunity/brighterscript/pull/651)) + - Export some vscode interfaces ([brighterscript#644](https://github.com/rokucommunity/brighterscript/pull/644)) + + + ## [0.14.1](https://github.com/rokucommunity/roku-debug/compare/v0.14.0...0.14.1) - 2022-07-16 ### Changed - Bump moment from 2.29.2 to 2.29.4 ([#92](https://github.com/rokucommunity/roku-debug/pull/92)) diff --git a/package-lock.json b/package-lock.json index bc3fa21d..f982d243 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.53.1", + "brighterscript": "^0.55.1", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.53.1", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.1.tgz", - "integrity": "sha512-uq6iPHV2W/8yXg2CFHp8DVT4824nrqVGdM0DyDtRUxt1GWyRr5PeIAMIse1vPNDzw33F/P2ZrHI9ceab2MIL1A==", + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.55.1.tgz", + "integrity": "sha512-J3OOA3sQRsGTVcMh9P0TBuhgd26L2xauHWucv8GKMm7aVubrfqVY4XUBiD/B7Gyit3BqPHEkFMMhflP+Nnn6bw==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -1196,6 +1196,7 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", + "require-relative": "^0.8.7", "roku-deploy": "^3.7.1", "serialize-error": "^7.0.1", "source-map": "^0.7.3", @@ -3913,6 +3914,11 @@ "dev": true, "license": "ISC" }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==" + }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -5553,9 +5559,9 @@ } }, "brighterscript": { - "version": "0.53.1", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.53.1.tgz", - "integrity": "sha512-uq6iPHV2W/8yXg2CFHp8DVT4824nrqVGdM0DyDtRUxt1GWyRr5PeIAMIse1vPNDzw33F/P2ZrHI9ceab2MIL1A==", + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.55.1.tgz", + "integrity": "sha512-J3OOA3sQRsGTVcMh9P0TBuhgd26L2xauHWucv8GKMm7aVubrfqVY4XUBiD/B7Gyit3BqPHEkFMMhflP+Nnn6bw==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5577,6 +5583,7 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", + "require-relative": "^0.8.7", "roku-deploy": "^3.7.1", "serialize-error": "^7.0.1", "source-map": "^0.7.3", @@ -7279,6 +7286,11 @@ "version": "2.0.0", "dev": true }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==" + }, "resolve-from": { "version": "4.0.0", "dev": true diff --git a/package.json b/package.json index 965bb239..b957a1bb 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.53.1", + "brighterscript": "^0.55.1", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From b50c15b2b10d671f63cdcea8dad0ea34268421e6 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 12 Aug 2022 11:30:10 -0400 Subject: [PATCH 035/197] 0.14.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f982d243..1379cdf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.14.1", + "version": "0.14.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.14.1", + "version": "0.14.2", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index b957a1bb..52743fd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.14.1", + "version": "0.14.2", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From b4b5d88e6a491a296a417d71d8ccb690203a62a7 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 17 Aug 2022 10:30:45 -0400 Subject: [PATCH 036/197] Add support for conditional breakpoints (#97) * Add support for conditional breakpoints * Update src/debugProtocol/Debugger.ts Co-authored-by: christopher Dwyer-Perkins * Fix barrel import * Add official breakpoint error update response structure * Increase connect wait time * remove unused `util.retry` method * fix broken specs * Update src/debugSession/BrightScriptDebugSession.ts Co-authored-by: christopher Dwyer-Perkins --- src/adapters/DebugProtocolAdapter.ts | 1 + src/debugProtocol/Constants.ts | 15 +++- src/debugProtocol/Debugger.ts | 81 +++++++++++++----- .../responses/BreakpointErrorResponse.spec.ts | 61 ++++++++++++++ .../BreakpointErrorUpdateResponse.ts | 82 +++++++++++++++++++ .../responses/responseCreationHelpers.spec.ts | 43 +++++++++- src/util.spec.ts | 56 ------------- src/util.ts | 55 ++++--------- 8 files changed, 274 insertions(+), 120 deletions(-) create mode 100644 src/debugProtocol/responses/BreakpointErrorResponse.spec.ts create mode 100644 src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index e5353652..f523ae12 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -689,6 +689,7 @@ export class DebugProtocolAdapter { filePath: breakpoint.pkgPath, lineNumber: breakpoint.line, hitCount: !isNaN(hitCount) ? hitCount : undefined, + conditionalExpression: breakpoint.condition, key: breakpoint.hash, componentLibraryName: breakpoint.componentLibraryName }; diff --git a/src/debugProtocol/Constants.ts b/src/debugProtocol/Constants.ts index fe1b6866..aa04e9fd 100644 --- a/src/debugProtocol/Constants.ts +++ b/src/debugProtocol/Constants.ts @@ -15,6 +15,7 @@ export enum COMMANDS { LIST_BREAKPOINTS = 8, // since protocol 1.2 REMOVE_BREAKPOINTS = 9, // since protocol 1.2 EXECUTE = 10, // since protocol 2.1 + ADD_CONDITIONAL_BREAKPOINTS = 11, // since protocol 3.1 EXIT_CHANNEL = 122 } @@ -46,9 +47,19 @@ export enum STOP_REASONS { export enum UPDATE_TYPES { UNDEF = 0, - IO_PORT_OPENED = 1, + IO_PORT_OPENED = 1, // client needs to connect to port to retrieve channel output ALL_THREADS_STOPPED = 2, - THREAD_ATTACHED = 3 + THREAD_ATTACHED = 3, + /** + * A compilation or runtime error occurred when evaluating the cond_expr of a conditional breakpoint + * @since protocol 3.1 + */ + BREAKPOINT_ERROR = 4, + /** + * A compilation error occurred + * @since protocol 3.1 + */ + COMPILE_ERROR = 5 } export enum VARIABLE_REQUEST_FLAGS { diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 4b7bb3d0..7dea00f9 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -18,15 +18,15 @@ import { UpdateThreadsResponse, VariableResponse } from './responses'; -import { PROTOCOL_ERROR_CODES, COMMANDS, STEP_TYPE, STOP_REASONS, VARIABLE_REQUEST_FLAGS } from './Constants'; +import { PROTOCOL_ERROR_CODES, COMMANDS, STEP_TYPE, STOP_REASONS, VARIABLE_REQUEST_FLAGS, ERROR_CODES, UPDATE_TYPES } from './Constants'; import { SmartBuffer } from 'smart-buffer'; import { logger } from '../logging'; -import { ERROR_CODES, UPDATE_TYPES } from '..'; import { ExecuteResponseV3 } from './responses/ExecuteResponseV3'; import { ListBreakpointsResponse } from './responses/ListBreakpointsResponse'; import { AddBreakpointsResponse } from './responses/AddBreakpointsResponse'; import { RemoveBreakpointsResponse } from './responses/RemoveBreakpointsResponse'; import { util } from '../util'; +import { BreakpointErrorUpdateResponse } from './responses/BreakpointErrorUpdateResponse'; export class Debugger { @@ -84,6 +84,13 @@ export class Debugger { return semver.satisfies(this.protocolVersion, '>=3.1.0'); } + /** + * Starting in protocol v3.1.0, breakpoints can support conditional expressions. This flag indicates whether the current sessuion supports that functionality. + */ + private get supportsConditionalBreakpoints() { + return semver.satisfies(this.protocolVersion, '>=3.1.0'); + } + /** * Get a promise that resolves after an event occurs exactly once */ @@ -137,26 +144,29 @@ export class Debugger { const debugSetupEnd = 'total socket debugger setup time'; console.time(debugSetupEnd); - await util.retry(() => { - this.controllerClient = new Net.Socket(); - return new Promise((resolve) => { - this.controllerClient.once('error', (error) => { + this.controllerClient = await new Promise((resolve) => { + const pendingSockets = new Set(); + util.setInterval((cancelInterval) => { + const socket = new Net.Socket(); + pendingSockets.add(socket); + socket.once('error', (error) => { console.error('Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error); - this.controllerClient?.destroy(); + socket?.destroy(); + pendingSockets.delete(socket); }); - this.controllerClient.connect({ port: this.options.controllerPort, host: this.options.host }, () => { - resolve(this.controllerClient); + socket.connect({ port: this.options.controllerPort, host: this.options.host }, () => { + this.logger.debug(`Connected to debug protocol controller port. Socket ${[...pendingSockets].indexOf(socket)} of ${pendingSockets.size} was the winner`); + //this socket successfully connected. + //remove this socket from the list of pending sockets + pendingSockets.delete(socket); + //clean up all remaining pending sockets + for (const pendingSocket of pendingSockets) { + pendingSocket?.destroy(); + } + resolve(socket); + cancelInterval(); }); - }); - }, { - onCancel: async () => { - this.controllerClient?.destroy(); - this.controllerClient = undefined; - //small timeout to let the connection destroy settle - await util.sleep(5); - }, - tryTime: this.options.controllerConnectInterval ?? 250, - totalTime: this.options.controllerConnectMaxTime ?? 5 * 60 * 1000 //5 minutes + }, this.options.controllerConnectInterval ?? 250); }); // If there is no error, the server has accepted the request and created a new dedicated socket @@ -337,6 +347,10 @@ export class Debugger { const { enableComponentLibrarySpecificBreakpoints } = this; if (breakpoints?.length > 0) { let buffer = new SmartBuffer(); + //set the `FLAGS` value if supported + if (this.supportsConditionalBreakpoints) { + buffer.writeUInt32LE(0); // flags - Should always be passed as 0. Unused, reserved for future use. + } buffer.writeUInt32LE(breakpoints.length); // num_breakpoints - The number of breakpoints in the breakpoints array. breakpoints.forEach((breakpoint) => { let { filePath } = breakpoint; @@ -350,8 +364,16 @@ export class Debugger { buffer.writeStringNT(filePath); // file_path - The path of the source file where the breakpoint is to be inserted. buffer.writeUInt32LE(breakpoint.lineNumber); // line_number - The line number in the channel application code where the breakpoint is to be executed. buffer.writeUInt32LE(breakpoint.hitCount ?? 0); // ignore_count - The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. + //if the protocol supports conditional breakpoints, add any present condition + if (this.supportsConditionalBreakpoints) { + //There's a bug in 3.1 where empty conditional expressions would crash the breakpoints, so just default to `true` which always succeeds + buffer.writeStringNT(breakpoint.conditionalExpression ?? 'true'); // cond_expr - the condition that must evaluate to `true` in order to hit the breakpoint + } }); - return this.makeRequest(buffer, COMMANDS.ADD_BREAKPOINTS); + return this.makeRequest(buffer, + this.supportsConditionalBreakpoints ? COMMANDS.ADD_CONDITIONAL_BREAKPOINTS : COMMANDS.ADD_BREAKPOINTS + //COMMANDS.ADD_BREAKPOINTS + ); } return new AddBreakpointsResponse(null); } @@ -381,6 +403,7 @@ export class Debugger { this.activeRequests[requestId] = { commandType: command, + commandTypeText: COMMANDS[command], extraData: extraData }; @@ -393,6 +416,7 @@ export class Debugger { }); if (this.controllerClient) { + this.logger.debug('makeRequest', `requestId=${requestId}`, this.activeRequests[requestId]); this.controllerClient.write(buffer.toBuffer()); } else { throw new Error(`Controller connection was closed - Command: ${COMMANDS[command]}`); @@ -440,6 +464,12 @@ export class Debugger { return false; case UPDATE_TYPES.UNDEF: return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); + case UPDATE_TYPES.BREAKPOINT_ERROR: + const response = new BreakpointErrorUpdateResponse(slicedBuffer); + //we do nothing with breakpoint errors at this time. + return this.checkResponse(response, buffer, packetLength); + case UPDATE_TYPES.COMPILE_ERROR: + return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); default: return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); } @@ -455,6 +485,7 @@ export class Debugger { case COMMANDS.EXECUTE: return this.checkResponse(new ExecuteResponseV3(slicedBuffer), buffer, packetLength); case COMMANDS.ADD_BREAKPOINTS: + case COMMANDS.ADD_CONDITIONAL_BREAKPOINTS: return this.checkResponse(new AddBreakpointsResponse(slicedBuffer), buffer, packetLength); case COMMANDS.LIST_BREAKPOINTS: return this.checkResponse(new ListBreakpointsResponse(slicedBuffer), buffer, packetLength); @@ -503,6 +534,7 @@ export class Debugger { } private removedProcessedBytes(responseHandler: { requestId: number; readOffset: number }, unhandledData: Buffer, packetLength = 0) { + const activeRequest = this.activeRequests[responseHandler.requestId]; if (responseHandler.requestId > 0 && this.activeRequests[responseHandler.requestId]) { delete this.activeRequests[responseHandler.requestId]; } @@ -510,7 +542,7 @@ export class Debugger { this.emit('data', responseHandler); this.unhandledData = unhandledData.slice(packetLength ? packetLength : responseHandler.readOffset); - this.logger.debug('[raw]', (responseHandler as any)?.constructor?.name ?? '', responseHandler); + this.logger.debug('[raw]', `requestId=${responseHandler?.requestId}`, activeRequest, (responseHandler as any)?.constructor?.name ?? '', responseHandler); this.parseUnhandledData(this.unhandledData); } @@ -661,6 +693,13 @@ export interface BreakpointSpec { * The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. */ hitCount?: number; + /** + * BrightScript code that evaluates to a boolean value. The expression is compiled and executed in + * the context where the breakpoint is located. If specified, the hitCount is only be + * updated if this evaluates to true. + * @avaiable since protocol version 3.1.0 + */ + conditionalExpression?: string; } export interface ConstructorOptions { diff --git a/src/debugProtocol/responses/BreakpointErrorResponse.spec.ts b/src/debugProtocol/responses/BreakpointErrorResponse.spec.ts new file mode 100644 index 00000000..6bb07222 --- /dev/null +++ b/src/debugProtocol/responses/BreakpointErrorResponse.spec.ts @@ -0,0 +1,61 @@ +import { createBreakpointErrorUpdateResponse } from './responseCreationHelpers.spec'; +import { expect } from 'chai'; +import { BreakpointErrorUpdateResponse } from './BreakpointErrorUpdateResponse'; +import { ERROR_CODES, UPDATE_TYPES } from '../Constants'; + +describe('BreakpointErrorUpdateResponse', () => { + it('Handles zero errors', () => { + const smartBuffer = createBreakpointErrorUpdateResponse({ + flags: 0, + breakpoint_id: 23, + errorCode: ERROR_CODES.OK, + compile_errors: [], + runtime_errors: [], + other_errors: [], + includePacketLength: false + }); + const rawBuffer = smartBuffer.toBuffer(); + let update = new BreakpointErrorUpdateResponse( + rawBuffer + ); + expect(update.requestId).to.eql(0); + expect(update.errorCode).to.eql(0); + expect(update.updateType).to.eql(UPDATE_TYPES.BREAKPOINT_ERROR); + expect(update.breakpointId).to.eql(23); + expect(update.flags).to.eql(0); + expect(update.success).to.eql(true); + + expect(update.compileErrorCount).to.eql(0); + expect(update.compileErrors).to.eql([]); + + expect(update.runtimeErrorCount).to.eql(0); + expect(update.runtimeErrors).to.eql([]); + + expect(update.otherErrorCount).to.eql(0); + expect(update.otherErrors).to.eql([]); + }); + + it('Handles many errors', () => { + const smartBuffer = createBreakpointErrorUpdateResponse({ + flags: 0, + breakpoint_id: 23, + errorCode: ERROR_CODES.OK, + compile_errors: ['compile error 1'], + runtime_errors: ['runtime error 1', 'runtime error 2'], + other_errors: ['other error 1', 'other error 2', 'other error 3'], + includePacketLength: false + }); + const rawBuffer = smartBuffer.toBuffer(); + let update = new BreakpointErrorUpdateResponse( + rawBuffer + ); + expect(update.compileErrorCount).to.eql(1); + expect(update.compileErrors).to.eql(['compile error 1']); + + expect(update.runtimeErrorCount).to.eql(2); + expect(update.runtimeErrors).to.eql(['runtime error 1', 'runtime error 2']); + + expect(update.otherErrorCount).to.eql(3); + expect(update.otherErrors).to.eql(['other error 1', 'other error 2', 'other error 3']); + }); +}); diff --git a/src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts b/src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts new file mode 100644 index 00000000..f7e14fa3 --- /dev/null +++ b/src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts @@ -0,0 +1,82 @@ +import { SmartBuffer } from 'smart-buffer'; +import { util } from '../../util'; +import { UPDATE_TYPES } from '../Constants'; + +/** + * Data sent as the data segment of message type: BREAKPOINT_ERROR + ``` + struct BreakpointErrorUpdateData { + uint32 flags; // Always 0, reserved for future use + uint32 breakpoint_id; + uint32 num_compile_errors; + utf8z[num_compile_errors] compile_errors; + uint32 num_runtime_errors; + utf8z[num_runtime_errors] runtime_errors; + uint32 num_other_errors; // E.g., permissions errors + utf8z[num_other_errors] other_errors; + } + ``` +*/ +export class BreakpointErrorUpdateResponse { + + constructor(buffer: Buffer) { + // The minimum size of a undefined response + if (buffer.byteLength >= 12) { + let bufferReader = SmartBuffer.fromBuffer(buffer); + this.requestId = bufferReader.readUInt32LE(); + + // Updates will always have an id of zero because we didn't ask for this information + if (this.requestId === 0) { + this.errorCode = bufferReader.readUInt32LE(); + this.updateType = bufferReader.readUInt32LE(); + } + if (this.updateType === UPDATE_TYPES.BREAKPOINT_ERROR) { + try { + this.flags = bufferReader.readUInt32LE(); // flags - always 0, reserved for future use + this.breakpointId = bufferReader.readUInt32LE(); // breakpoint_id + + this.compileErrorCount = bufferReader.readUInt32LE(); // num_compile_errors + for (let i = 0; i < this.compileErrorCount; i++) { + this.compileErrors.push( + util.readStringNT(bufferReader) + ); + } + + this.runtimeErrorCount = bufferReader.readUInt32LE(); // num_runtime_errors + for (let i = 0; i < this.runtimeErrorCount; i++) { + this.runtimeErrors.push( + util.readStringNT(bufferReader) + ); + } + + this.otherErrorCount = bufferReader.readUInt32LE(); // num_other_errors + for (let i = 0; i < this.otherErrorCount; i++) { + this.otherErrors.push( + util.readStringNT(bufferReader) + ); + } + this.success = true; + } catch (error) { + // Could not process + } + } + } + } + public success = false; + public readOffset = 0; + public requestId = -1; + public errorCode = -1; + public updateType = -1; + + public flags: number; + public breakpointId: number; + + public compileErrorCount: number; + public compileErrors: string[] = []; + + public runtimeErrorCount: number; + public runtimeErrors: string[] = []; + + public otherErrorCount: number; + public otherErrors: string[] = []; +} diff --git a/src/debugProtocol/responses/responseCreationHelpers.spec.ts b/src/debugProtocol/responses/responseCreationHelpers.spec.ts index d60df11d..8f9c6a1c 100644 --- a/src/debugProtocol/responses/responseCreationHelpers.spec.ts +++ b/src/debugProtocol/responses/responseCreationHelpers.spec.ts @@ -1,5 +1,5 @@ import { SmartBuffer } from 'smart-buffer'; -import type { ERROR_CODES, UPDATE_TYPES } from '../Constants'; +import { ERROR_CODES, UPDATE_TYPES } from '../Constants'; import type { BreakpointInfo } from './ListBreakpointsResponse'; interface Handshake { @@ -93,6 +93,9 @@ export function createProtocolEventV3(protocolEvent: ProtocolEvent, extraBufferD return addPacketLength(buffer); } +/** + * Add packetLength to the beginning of the buffer + */ function addPacketLength(buffer: SmartBuffer): SmartBuffer { return buffer.insertUInt32LE(buffer.length + 4, 0); // packet_length - The size of the packet to be sent. } @@ -119,6 +122,44 @@ export function createListBreakpointsResponse(params: { requestId?: number; erro return addPacketLength(buffer); } +/** + * Contains a list of breakpoint errors + */ +export function createBreakpointErrorUpdateResponse(params: { errorCode?: number; flags?: number; breakpoint_id?: number; compile_errors?: string[]; runtime_errors?: string[]; other_errors?: string[]; extraBufferData?: Buffer; includePacketLength?: boolean }): SmartBuffer { + let buffer = new SmartBuffer(); + + writeIfSet(0, x => buffer.writeUInt32LE(x)); //request_id + writeIfSet(ERROR_CODES.OK, x => buffer.writeUInt32LE(x)); //error_code + writeIfSet(UPDATE_TYPES.BREAKPOINT_ERROR, x => buffer.writeUInt32LE(x)); //update_type + + writeIfSet(params.flags, x => buffer.writeUInt32LE(x)); //flags + + writeIfSet(params.breakpoint_id, x => buffer.writeUInt32LE(x)); //breakpoint_id + + writeIfSet(params.compile_errors?.length, x => buffer.writeUInt32LE(x)); + for (const error of params.compile_errors ?? []) { + buffer.writeStringNT(error); + } + + writeIfSet(params.runtime_errors?.length, x => buffer.writeUInt32LE(x)); + for (const error of params.runtime_errors ?? []) { + buffer.writeStringNT(error); + } + + writeIfSet(params.other_errors?.length, x => buffer.writeUInt32LE(x)); + for (const error of params.other_errors ?? []) { + buffer.writeStringNT(error); + } + + // write any extra data for testing + writeIfSet(params.extraBufferData, x => buffer.writeBuffer(x)); + + if (params.includePacketLength) { + buffer = addPacketLength(buffer); + } + return buffer; +} + /** * If the value is undefined or null, skip the callback. * All other values will cause the callback to be called diff --git a/src/util.spec.ts b/src/util.spec.ts index cb0d793b..e4a1a380 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -430,60 +430,4 @@ describe('Util', () => { ).to.be.true; }); }); - - describe('retry', () => { - it('kills on first error encountered', async () => { - let tryCount = 0; - let cancelCount = 0; - await util.retry(() => { - tryCount++; - throw new Error('Crash'); - }, { - onCancel: () => { - cancelCount++; - }, - tryTime: 4, - totalTime: 10 - }).catch(() => { /*ignore the error */ }); - expect(tryCount).to.equal(1); - expect(cancelCount).to.equal(0); - }); - - it('kills long-running tries', async () => { - let tryCount = 0; - let cancelCount = 0; - const result = await util.retry(() => { - tryCount++; - if (tryCount >= 3) { - return true; - } else { - return util.sleep(50); - } - }, { - onCancel: () => { - cancelCount++; - }, - tryTime: 4, - totalTime: 100 - }); - expect(result).to.be.true; - expect(tryCount).to.equal(3); - expect(cancelCount).to.equal(tryCount - 1); - }); - - it('kills tries that exceed the total runtime', async () => { - try { - await util.retry(() => { - return util.sleep(50); - }, { - onCancel: () => { - }, - tryTime: 4000, - totalTime: 10 - }); - } catch (e) { - expect(e.message).to.equal('Total allotted time exceeded'); - } - }); - }); }); diff --git a/src/util.ts b/src/util.ts index f9d0ca86..2ec0d91a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -388,47 +388,22 @@ class Util { options.remotePort ??= 8060; } - public async retry(action: () => T, options: { - /** - * The max number of milliseconds an individual try can run - */ - tryTime: number; - /** - * The max number of milliseconds this entire operation can run. - */ - totalTime: number; - /** - * A callback that is run every time a try is canceled - */ - onCancel?: (error: Error) => void; - }): Promise { - options = { - tryTime: 100, - totalTime: 1000, - onCancel: () => { }, - ...options ?? {} + /** + * Set an interval that can be cleared by calling the callback + * @param intervalMs the number of milliseconds to wait for the next interval + */ + public setInterval(callback: (cancel: () => void) => any, intervalMs: number) { + const cancel = () => { + clearInterval(handle); }; - const startTime = Date.now(); - while (true) { - if (Date.now() - startTime >= options.totalTime) { - throw new Error('Total allotted time exceeded'); - } - let timedOut = false; - const actionResult = Promise.resolve(action()); - const timeoutResult = this.sleep(options.tryTime).then(() => { - timedOut = true; - }); - const result = await Promise.race([actionResult, timeoutResult]); - if (timedOut) { - await Promise.resolve( - options?.onCancel?.( - new Error('Try timed out') - ) - ); - } else { - return result as T; - } - } + const handle = setInterval(() => { + callback(cancel); + }, intervalMs); + + //call immediately + callback(cancel); + + return cancel; } } From 8113fa4cbd74e846081a37030db3523256895897 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 18 Aug 2022 09:54:39 -0400 Subject: [PATCH 037/197] Add `invalid` data type support (#99) --- .../responses/VariableResponse.spec.ts | 58 ++++++++++ .../responses/VariableResponse.ts | 5 + .../responses/responseCreationHelpers.spec.ts | 105 +++++++++++++++++- 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/debugProtocol/responses/VariableResponse.spec.ts diff --git a/src/debugProtocol/responses/VariableResponse.spec.ts b/src/debugProtocol/responses/VariableResponse.spec.ts new file mode 100644 index 00000000..b950d9bd --- /dev/null +++ b/src/debugProtocol/responses/VariableResponse.spec.ts @@ -0,0 +1,58 @@ +/* eslint-disable no-bitwise */ +import { VariableResponse } from './VariableResponse'; +import { createVariableResponse } from './responseCreationHelpers.spec'; +import { expect } from 'chai'; +import { ERROR_CODES, VARIABLE_FLAGS, VARIABLE_TYPES } from '../Constants'; + +describe('VariableResponse', () => { + + it('Properly parses invalid variable', () => { + let buffer = createVariableResponse({ + requestId: 2, + errorCode: ERROR_CODES.OK, + variables: [{ + name: 'person', + refCount: 2, + isConst: false, + variableType: VARIABLE_TYPES.AA, + keyType: VARIABLE_TYPES.String, + value: undefined, + children: [{ + name: 'firstName', + refCount: 1, + value: 'Bob', + variableType: VARIABLE_TYPES.String, + isConst: false + }, { + name: 'lastName', + refCount: 1, + value: undefined, + variableType: VARIABLE_TYPES.Invalid, + isConst: false + }] + }], + includePacketLength: false + }); + + let response = new VariableResponse(buffer.toBuffer()); + expect( + response.variables?.map(x => ({ + name: x.name, + value: x.value, + isContainer: x.isContainer + })) + ).to.eql([{ + name: 'person', + value: null, + isContainer: true + }, { + name: 'firstName', + value: 'Bob', + isContainer: false + }, { + name: 'lastName', + value: 'Invalid', + isContainer: false + }]); + }); +}); diff --git a/src/debugProtocol/responses/VariableResponse.ts b/src/debugProtocol/responses/VariableResponse.ts index 80f77ae2..fd268e03 100644 --- a/src/debugProtocol/responses/VariableResponse.ts +++ b/src/debugProtocol/responses/VariableResponse.ts @@ -127,8 +127,13 @@ export class VariableInfo { this.value = 'Unknown'; this.success = true; break; + case 'Invalid': + this.value = 'Invalid'; + this.success = true; + break; case 'AA': case 'Array': + case 'List': this.value = null; this.success = true; break; diff --git a/src/debugProtocol/responses/responseCreationHelpers.spec.ts b/src/debugProtocol/responses/responseCreationHelpers.spec.ts index 8f9c6a1c..452bf756 100644 --- a/src/debugProtocol/responses/responseCreationHelpers.spec.ts +++ b/src/debugProtocol/responses/responseCreationHelpers.spec.ts @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/no-loop-func */ +/* eslint-disable no-bitwise */ import { SmartBuffer } from 'smart-buffer'; -import { ERROR_CODES, UPDATE_TYPES } from '../Constants'; +import { ERROR_CODES, UPDATE_TYPES, VARIABLE_FLAGS, VARIABLE_TYPES } from '../Constants'; import type { BreakpointInfo } from './ListBreakpointsResponse'; interface Handshake { @@ -122,6 +124,107 @@ export function createListBreakpointsResponse(params: { requestId?: number; erro return addPacketLength(buffer); } +interface Variable { + variableType: VARIABLE_TYPES; + name: string; + flags?: number; + refCount: number; + isConst: boolean; + children?: Variable[]; + value: any; + keyType?: VARIABLE_TYPES; +} + +export function createVariableResponse(params: { + requestId?: number; variables?: Variable[]; errorCode?: number; extraBufferData?: Buffer; includePacketLength?: boolean; +}): SmartBuffer { + let buffer = new SmartBuffer(); + + writeIfSet(params.requestId, x => buffer.writeUInt32LE(x)); + writeIfSet(params.errorCode, x => buffer.writeUInt32LE(x)); + + const variables = [...params.variables]; + for (let i = 0; i < variables.length; i++) { + const variable = variables[i]; + if (variable.children) { + variables.splice(i + 1, 0, ...variable.children); + } + } + + writeIfSet(variables?.length, x => buffer.writeUInt32LE(x)); + + while (variables.length > 0) { + const variable = variables.shift(); + let flags = 0; + if (variable.isConst) { + flags |= VARIABLE_FLAGS.isConst; + } + if (variable.children) { + flags |= VARIABLE_FLAGS.isContainer; + } + if (variable.name !== undefined) { + flags |= VARIABLE_FLAGS.isNameHere; + } + if (variable.refCount !== undefined) { + flags |= VARIABLE_FLAGS.isRefCounted; + } + if (variable.value !== undefined) { + flags |= VARIABLE_FLAGS.isValueHere; + } + buffer.writeUInt8(flags); //flags + writeIfSet(variable.variableType, x => buffer.writeUInt8(x)); //variable_type + writeIfSet(variable.name, x => buffer.writeStringNT(variable.name)); + if (variable.refCount !== undefined) { + writeIfSet(variable.refCount, x => buffer.writeUInt32LE(variable.refCount)); + } + if (variable.children) { + for (const child of variable.children) { + child.flags = (child.flags ?? 0) | VARIABLE_FLAGS.isChildKey; + } + writeIfSet(variable.keyType, x => buffer.writeUInt8(variable.keyType)); + //element_count + writeIfSet(variable.children.length, x => buffer.writeUInt32LE(variable.keyType)); + } + + switch (variable.variableType) { + case VARIABLE_TYPES.Interface: + case VARIABLE_TYPES.Object: + case VARIABLE_TYPES.String: + case VARIABLE_TYPES.Subroutine: + case VARIABLE_TYPES.Function: + buffer.writeStringNT(variable.value); + break; + case VARIABLE_TYPES.Subtyped_Object: + buffer.writeStringNT(variable.value[0]); + buffer.writeStringNT(variable.value[1]); + break; + case VARIABLE_TYPES.Boolean: + buffer.writeUInt8(variable.value ? 1 : 0); + break; + case VARIABLE_TYPES.Double: + buffer.writeDoubleLE(variable.value); + break; + case VARIABLE_TYPES.Float: + buffer.writeFloatLE(variable.value); + break; + case VARIABLE_TYPES.Integer: + buffer.writeInt32LE(variable.value); + break; + case VARIABLE_TYPES.Long_Integer: + buffer.writeBigInt64LE(variable.value); + break; + default: + //nothing to write + break; + } + } + + if (params.includePacketLength) { + buffer = addPacketLength(buffer); + } + return buffer; +} + /** * Contains a list of breakpoint errors */ From 9ed27b56bf42d366581a38b4609fdd9013838fa1 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 19 Aug 2022 14:15:28 -0400 Subject: [PATCH 038/197] Fix stopOnEntry bug with deep links. (#100) Fix dns resolving for deep links --- src/debugSession/BrightScriptDebugSession.ts | 6 ++++-- src/util.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 9492f514..2b7b5961 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -379,9 +379,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { await this.rokuAdapter.continue(); //kill the app on the roku await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + //convert a hostname to an ip address + const deepLinkUrl = await util.resolveUrl(this.launchConfiguration.deepLinkUrl); //send the deep link http request await new Promise((resolve, reject) => { - request.post(this.launchConfiguration.deepLinkUrl, (err, response) => { + request.post(deepLinkUrl, (err, response) => { return err ? reject(err) : resolve(response); }); }); @@ -1184,7 +1186,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { public async handleEntryBreakpoint() { if (!this.enableDebugProtocol) { this.entryBreakpointWasHandled = true; - if (this.launchConfiguration.stopOnEntry) { + if (this.launchConfiguration.stopOnEntry || this.launchConfiguration.deepLinkUrl) { await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingFolderPath); } } diff --git a/src/util.ts b/src/util.ts index 2ec0d91a..7d7ff278 100644 --- a/src/util.ts +++ b/src/util.ts @@ -324,6 +324,19 @@ class Util { } } + /** + * Given a full URL, convert any dns name into its IP address and then return the full URL with the name replaced + */ + public async resolveUrl(url: string, skipCache = false) { + //https://regex101.com/r/cSkoTx/1 + const [, protocol, host] = /^((?:http[s]?|ftp):\/\/)?([^:\/\s]+)(:\d+)?([^?#]+)?(\?[^#]+)?(#.*)?$/.exec(url) ?? []; + if (host) { + const ipAddress = await this.dnsLookup(host); + url = protocol + ipAddress + url.substring(protocol.length + host.length); + } + return url; + } + /* * Look up the ip address for a hostname. This is cached for the lifetime of the app, or bypassed with the `skipCache` parameter * @param host From 46acac23fad5de1f0d5dcc899bdca729f8d084bd Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 23 Aug 2022 07:29:59 -0400 Subject: [PATCH 039/197] Update changelog for v0.15.0 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 14 +++++++------- package.json | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50997f9e..d92497a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.15.0](https://github.com/rokucommunity/roku-debug/compare/v0.14.2...0.15.0) - 2022-08-23 +### Added + - support for conditional breakpoints over the debug protocol([#97](https://github.com/rokucommunity/roku-debug/pull/97)) +### Changed +- upgrade to [brighterscript@0.56.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0560---2022-08-23). Notable changes since 0.55.1: + - Fix compile crash for scope-less files ([brighterscript#674](https://github.com/rokucommunity/brighterscript/pull/674)) + - Allow const as variable name ([brighterscript#670](https://github.com/rokucommunity/brighterscript/pull/670)) +### Fixed + - `stopOnEntry` bug with `deepLinkUrl`. ([#100](https://github.com/rokucommunity/roku-debug/pull/100)) + - bug that was omitting `invalid` data types over the debug protocol ([#99](https://github.com/rokucommunity/roku-debug/pull/99)) + + + ## [0.14.2](https://github.com/rokucommunity/roku-debug/compare/v0.14.1...0.14.2) - 2022-08-12 ### Changed - Support complib breakpoints on 11.5.0 ([#96](https://github.com/rokucommunity/roku-debug/pull/96)) diff --git a/package-lock.json b/package-lock.json index 1379cdf5..b204a348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.55.1", + "brighterscript": "^0.56.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.55.1.tgz", - "integrity": "sha512-J3OOA3sQRsGTVcMh9P0TBuhgd26L2xauHWucv8GKMm7aVubrfqVY4XUBiD/B7Gyit3BqPHEkFMMhflP+Nnn6bw==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.56.0.tgz", + "integrity": "sha512-SG8Oj3pDjtsbq1eZRnAI96T3PVSIDORPh8QfBtXVMPRG2ntJd2OfySpHsaItt5K9oveZi1OjPPguRGL7xaU0MQ==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5559,9 +5559,9 @@ } }, "brighterscript": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.55.1.tgz", - "integrity": "sha512-J3OOA3sQRsGTVcMh9P0TBuhgd26L2xauHWucv8GKMm7aVubrfqVY4XUBiD/B7Gyit3BqPHEkFMMhflP+Nnn6bw==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.56.0.tgz", + "integrity": "sha512-SG8Oj3pDjtsbq1eZRnAI96T3PVSIDORPh8QfBtXVMPRG2ntJd2OfySpHsaItt5K9oveZi1OjPPguRGL7xaU0MQ==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", diff --git a/package.json b/package.json index 52743fd2..0bddcd77 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.55.1", + "brighterscript": "^0.56.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From fd6fd66ed774e41a76eea9812c0d1ef97ff10f6d Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 23 Aug 2022 07:30:30 -0400 Subject: [PATCH 040/197] 0.15.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b204a348..951388d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.14.2", + "version": "0.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.14.2", + "version": "0.15.0", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index 0bddcd77..149abe09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.14.2", + "version": "0.15.0", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From 29821612f03b42cbb715b37637ca27dcbea77433 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 29 Aug 2022 08:22:03 -0400 Subject: [PATCH 041/197] Better debug protocol launch handling (#102) * Better debug protocol launch handling * change some logs * Apply suggestions from code review Co-authored-by: christopher Dwyer-Perkins Co-authored-by: christopher Dwyer-Perkins --- src/debugProtocol/Debugger.ts | 78 +++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index 7dea00f9..cee3f9df 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -139,44 +139,41 @@ export class Debugger { }, 0); } - public async connect(): Promise { - this.logger.log('connect', this.options); - const debugSetupEnd = 'total socket debugger setup time'; - console.time(debugSetupEnd); - - this.controllerClient = await new Promise((resolve) => { - const pendingSockets = new Set(); + private async establishControllerConnection() { + const pendingSockets = new Set(); + const connection = await new Promise((resolve) => { util.setInterval((cancelInterval) => { const socket = new Net.Socket(); pendingSockets.add(socket); - socket.once('error', (error) => { - console.error('Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error); - socket?.destroy(); - pendingSockets.delete(socket); + socket.on('error', (error) => { + console.debug(Date.now(), 'Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error); }); socket.connect({ port: this.options.controllerPort, host: this.options.host }, () => { + cancelInterval(); + this.logger.debug(`Connected to debug protocol controller port. Socket ${[...pendingSockets].indexOf(socket)} of ${pendingSockets.size} was the winner`); - //this socket successfully connected. - //remove this socket from the list of pending sockets - pendingSockets.delete(socket); //clean up all remaining pending sockets for (const pendingSocket of pendingSockets) { - pendingSocket?.destroy(); + pendingSocket.removeAllListeners(); + //cleanup and destroy all other sockets + if (pendingSocket !== socket) { + pendingSocket.end(); + pendingSocket?.destroy(); + } } + pendingSockets.clear(); resolve(socket); - cancelInterval(); }); }, this.options.controllerConnectInterval ?? 250); }); + return connection; + } - // If there is no error, the server has accepted the request and created a new dedicated socket - this.logger.log('TCP connection established with the server.'); + public async connect(): Promise { + this.logger.log('connect', this.options); - // The client can also receive data from the server by reading from its socket. - // The client can now send data to the server by writing to its socket. - let buffer = new SmartBuffer({ size: Buffer.byteLength(Debugger.DEBUGGER_MAGIC) + 1 }).writeStringNT(Debugger.DEBUGGER_MAGIC).toBuffer(); - this.logger.log('Sending magic to server'); - this.controllerClient.write(buffer); + // If there is no error, the server has accepted the request and created a new dedicated control socket + this.controllerClient = await this.establishControllerConnection(); this.controllerClient.on('data', (buffer) => { if (this.unhandledData) { @@ -185,7 +182,13 @@ export class Debugger { this.unhandledData = buffer; } + this.logger.debug(`on('data'): incoming bytes`, buffer.length); + const startBufferSize = this.unhandledData.length; + this.parseUnhandledData(this.unhandledData); + + const endBufferSize = this.unhandledData?.length ?? 0; + this.logger.debug(`buffer size before:`, startBufferSize, ', buffer size after:', endBufferSize, ', bytes consumed:', startBufferSize - endBufferSize); }); this.controllerClient.on('end', () => { @@ -195,15 +198,25 @@ export class Debugger { // Don't forget to catch error, for your own sake. this.controllerClient.once('error', (error) => { - console.error(`TCP connection error`, error); + //the Roku closed the connection for some unknown reason... + console.error(`TCP connection error on control port`, error); this.shutdown('close'); }); + //send the magic, which triggers the debug session + this.sendMagic(); + + //wait for the handshake response from the device const isConnected = await this.once('connected'); - console.timeEnd(debugSetupEnd); return isConnected; } + private sendMagic() { + let buffer = new SmartBuffer({ size: Buffer.byteLength(Debugger.DEBUGGER_MAGIC) + 1 }).writeStringNT(Debugger.DEBUGGER_MAGIC).toBuffer(); + this.logger.log('Sending magic to server'); + this.controllerClient.write(buffer); + } + public async continue() { if (this.stopped) { this.stopped = false; @@ -415,8 +428,8 @@ export class Debugger { } }); + this.logger.debug('makeRequest', `requestId=${requestId}`, this.activeRequests[requestId]); if (this.controllerClient) { - this.logger.debug('makeRequest', `requestId=${requestId}`, this.activeRequests[requestId]); this.controllerClient.write(buffer.toBuffer()); } else { throw new Error(`Controller connection was closed - Command: ${COMMANDS[command]}`); @@ -435,7 +448,7 @@ export class Debugger { let packetLength = debuggerRequestResponse.packetLength; let slicedBuffer = packetLength ? buffer.slice(4) : buffer; - this.logger.log('incoming data - ', `bytes: ${buffer.length}`, debuggerRequestResponse); + this.logger.log(`incoming bytes: ${buffer.length}`, debuggerRequestResponse); if (debuggerRequestResponse.success) { if (debuggerRequestResponse.requestId > this.totalRequests) { this.removedProcessedBytes(debuggerRequestResponse, slicedBuffer, packetLength); @@ -508,6 +521,8 @@ export class Debugger { } else { let debuggerHandshake: HandshakeResponse | HandshakeResponseV3; debuggerHandshake = new HandshakeResponseV3(buffer); + this.logger.log(`incoming bytes: ${buffer.length}`, debuggerHandshake); + if (!debuggerHandshake.success) { debuggerHandshake = new HandshakeResponse(buffer); } @@ -516,6 +531,8 @@ export class Debugger { this.handshakeComplete = true; this.verifyHandshake(debuggerHandshake); this.removedProcessedBytes(debuggerHandshake, buffer); + //once the handshake is complete, we have successfully "connected" + this.emit('connected', true); return true; } } @@ -564,7 +581,7 @@ export class Debugger { errorCode: PROTOCOL_ERROR_CODES.SUPPORTED }); } else if (semver.gtr(this.protocolVersion, this.supportedVersionRange)) { - this.logger.log('not tested'); + this.logger.log('roku-debug has not been tested against protocol version', this.protocolVersion); this.emit('protocol-version', { message: `Protocol Version ${this.protocolVersion} has not been tested and my not work as intended.\nPlease open any issues you have with this version to https://github.com/rokucommunity/roku-debug/issues`, errorCode: PROTOCOL_ERROR_CODES.NOT_TESTED @@ -590,6 +607,7 @@ export class Debugger { } private connectToIoPort(connectIoPortResponse: ConnectIOPortResponse, unhandledData: Buffer, packetLength = 0) { + this.logger.log('Connecting to IO port. response status success =', connectIoPortResponse.success); if (connectIoPortResponse.success) { // Create a new TCP client. this.ioClient = new Net.Socket(); @@ -625,10 +643,8 @@ export class Debugger { // Don't forget to catch error, for your own sake. this.ioClient.once('error', (err) => { this.ioClient.end(); - this.logger.log(`Error: ${err}`); + this.logger.error(err); }); - - this.emit('connected', true); }); this.removedProcessedBytes(connectIoPortResponse, unhandledData, packetLength); From 10b3fcf3dee0e25b3a34e7ee5a1fb4e1b4e4e85c Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 30 Aug 2022 14:11:11 -0400 Subject: [PATCH 042/197] Standardize custom events, add is* helpers (#103) * Standardize custom events, add is* helpers * Better compile error handling * roku-deploy@3.8.0 --- package-lock.json | 14 +- package.json | 2 +- src/debugSession/BrightScriptDebugSession.ts | 23 +-- src/debugSession/Events.spec.ts | 26 +++ src/debugSession/Events.ts | 181 +++++++++++++------ src/index.ts | 1 + 6 files changed, 169 insertions(+), 78 deletions(-) create mode 100644 src/debugSession/Events.spec.ts diff --git a/package-lock.json b/package-lock.json index 951388d5..29388b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.7.1", + "roku-deploy": "^3.8.0", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", @@ -3973,9 +3973,9 @@ } }, "node_modules/roku-deploy": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.1.tgz", - "integrity": "sha512-xXTYNr4Ug+Kr+bnhDqlJDcbuu6rg8x0MFIpA+36jbpJcqsI6ekbWzRh2QhUG6aZ4F8+zKt8jZFIkZDeyooJJfQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.8.0.tgz", + "integrity": "sha512-pcrNUBklhN5t1JNdYPWCpOTcbU1H869DAj2N3SlM+udL7g+cQF73ZHSd6kNOEBg7xF6nvOmrSSgaoVbguxtuUg==", "dependencies": { "chalk": "^2.4.2", "dateformat": "^3.0.3", @@ -7326,9 +7326,9 @@ } }, "roku-deploy": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.7.1.tgz", - "integrity": "sha512-xXTYNr4Ug+Kr+bnhDqlJDcbuu6rg8x0MFIpA+36jbpJcqsI6ekbWzRh2QhUG6aZ4F8+zKt8jZFIkZDeyooJJfQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.8.0.tgz", + "integrity": "sha512-pcrNUBklhN5t1JNdYPWCpOTcbU1H869DAj2N3SlM+udL7g+cQF73ZHSd6kNOEBg7xF6nvOmrSSgaoVbguxtuUg==", "requires": { "chalk": "^2.4.2", "dateformat": "^3.0.3", diff --git a/package.json b/package.json index 149abe09..88de68f6 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.7.1", + "roku-deploy": "^3.8.0", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 2b7b5961..a65e021f 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -2,7 +2,7 @@ import * as fsExtra from 'fs-extra'; import { orderBy } from 'natural-orderby'; import * as path from 'path'; import * as request from 'request'; -import { rokuDeploy } from 'roku-deploy'; +import { rokuDeploy, CompileError } from 'roku-deploy'; import type { RokuDeploy, RokuDeployOptions } from 'roku-deploy'; import { BreakpointEvent, @@ -330,9 +330,9 @@ export class BrightScriptDebugSession extends BaseDebugSession { await this.connectAndPublish(); - this.sendEvent(new ChannelPublishedEvent({ - launchConfiguration: this.launchConfiguration - })); + this.sendEvent(new ChannelPublishedEvent( + this.launchConfiguration + )); //tell the adapter adapter that the channel has been launched. await this.rokuAdapter.activate(); @@ -356,16 +356,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } catch (e) { //if the message is anything other than compile errors, we want to display the error - //TODO: look into the reason why we are getting the 'Invalid response code: 400' on compile errors - if (e.message !== 'compileErrors' && e.message !== 'Invalid response code: 400') { - //TODO make the debugger stop! + if (!(e instanceof CompileError)) { util.log('Encountered an issue during the publish process'); util.log((e as Error).message); this.sendErrorResponse(response, -1, (e as Error).message); - } else { - //request adapter to send errors (even empty) before ending the session - await this.rokuAdapter.sendErrors(); } + + //send any compile errors to the client + await this.rokuAdapter.sendErrors(); this.logger.error('Error. Shutting down.', e); this.shutdown(); return; @@ -399,7 +397,10 @@ export class BrightScriptDebugSession extends BaseDebugSession { let packageIsPublished = false; //publish the package to the target Roku - const publishPromise = this.rokuDeploy.publish(this.launchConfiguration as any as RokuDeployOptions).then(() => { + const publishPromise = this.rokuDeploy.publish({ + ...this.launchConfiguration, + failOnCompileError: true + } as any as RokuDeployOptions).then(() => { packageIsPublished = true; }); diff --git a/src/debugSession/Events.spec.ts b/src/debugSession/Events.spec.ts new file mode 100644 index 00000000..2aea1610 --- /dev/null +++ b/src/debugSession/Events.spec.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { isCompileFailureEvent, CompileFailureEvent, isLogOutputEvent, LogOutputEvent, isDebugServerLogOutputEvent, DebugServerLogOutputEvent, isRendezvousEvent, RendezvousEvent, isChanperfEvent, ChanperfEvent, isLaunchStartEvent, LaunchStartEvent, isPopupMessageEvent, PopupMessageEvent, isChannelPublishedEvent, ChannelPublishedEvent } from './Events'; + +describe('Events', () => { + it('is* methods work properly', () => { + //match + expect(isCompileFailureEvent(new CompileFailureEvent(null))).to.be.true; + expect(isLogOutputEvent(new LogOutputEvent(null))).to.be.true; + expect(isDebugServerLogOutputEvent(new DebugServerLogOutputEvent(null))).to.be.true; + expect(isRendezvousEvent(new RendezvousEvent(null))).to.be.true; + expect(isChanperfEvent(new ChanperfEvent(null))).to.be.true; + expect(isLaunchStartEvent(new LaunchStartEvent(null))).to.be.true; + expect(isPopupMessageEvent(new PopupMessageEvent(null, 'error'))).to.be.true; + expect(isChannelPublishedEvent(new ChannelPublishedEvent(null))).to.be.true; + + //not match + expect(isCompileFailureEvent(null)).to.be.false; + expect(isLogOutputEvent(null)).to.be.false; + expect(isDebugServerLogOutputEvent(null)).to.be.false; + expect(isRendezvousEvent(null)).to.be.false; + expect(isChanperfEvent(null)).to.be.false; + expect(isLaunchStartEvent(null)).to.be.false; + expect(isPopupMessageEvent(null)).to.be.false; + expect(isChannelPublishedEvent(null)).to.be.false; + }); +}); diff --git a/src/debugSession/Events.ts b/src/debugSession/Events.ts index 9938bb67..d8e711fd 100644 --- a/src/debugSession/Events.ts +++ b/src/debugSession/Events.ts @@ -1,108 +1,171 @@ +/* eslint-disable @typescript-eslint/no-useless-constructor */ import type { DebugProtocol } from 'vscode-debugprotocol'; import type { BrightScriptDebugCompileError } from '../CompileErrorProcessor'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { ChanperfData } from '../ChanperfTracker'; import type { RendezvousHistory } from '../RendezvousTracker'; -export class CompileFailureEvent implements DebugProtocol.Event { - constructor(compileError: BrightScriptDebugCompileError[]) { - this.body = compileError; +export class CustomEvent implements DebugProtocol.Event { + public constructor(body: T) { + this.body = body; + this.event = this.constructor.name; } - - public body: any; + /** + * The body (payload) of the event. + */ + public body: T; + /** + * The name of the event. This name is how the client identifies the type of event and how to handle it + */ public event: string; + /** + * The type of ProtocolMessage. Hardcoded to 'event' for all custom events + */ + public type = 'event'; public seq: number; - public type: string; } -export class LogOutputEvent implements DebugProtocol.Event { - constructor(lines: string) { - this.body = lines; - this.event = 'BSLogOutputEvent'; +/** + * Emitted when compile errors were encountered during the current debug session, + * usually during the initial sideload process as the Roku is compiling the app. + */ +export class CompileFailureEvent extends CustomEvent<{ compileErrors: BrightScriptDebugCompileError[] }> { + constructor(compileErrors: BrightScriptDebugCompileError[]) { + super({ compileErrors }); } +} - public body: any; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `CompileFailureEvent` + */ +export function isCompileFailureEvent(event: any): event is CompileFailureEvent { + return !!event && event.event === CompileFailureEvent.name; +} + +/** + * A line of log ouptut from the Roku device + */ +export class LogOutputEvent extends CustomEvent<{ line: string }> { + constructor(line: string) { + super({ line }); + } } -export class DebugServerLogOutputEvent extends LogOutputEvent { - constructor(lines: string) { - super(lines); - this.event = 'BSDebugServerLogOutputEvent'; +/** + * Is the object a `LogOutputEvent` + */ +export function isLogOutputEvent(event: any): event is LogOutputEvent { + return !!event && event.event === LogOutputEvent.name; +} + +/** + * Log output from the debug server. These are logs emitted from NodeJS from the various RokuCommunity tools + */ +export class DebugServerLogOutputEvent extends CustomEvent<{ line: string }> { + constructor(line: string) { + super({ line }); } } -export class RendezvousEvent implements DebugProtocol.Event { +/** + * Is the object a `DebugServerLogOutputEvent` + */ +export function isDebugServerLogOutputEvent(event: any): event is DebugServerLogOutputEvent { + return !!event && event.event === DebugServerLogOutputEvent.name; +} + +/** + * Emitted when a rendezvous has occurred. Contains the full history of rendezvous since the start of the current debug session + */ +export class RendezvousEvent extends CustomEvent { constructor(output: RendezvousHistory) { - this.body = output; - this.event = 'BSRendezvousEvent'; + super(output); } +} - public body: RendezvousHistory; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `RendezvousEvent` + */ +export function isRendezvousEvent(event: any): event is RendezvousEvent { + return !!event && event.event === RendezvousEvent.name; } -export class ChanperfEvent implements DebugProtocol.Event { +/** + * Emitted anytime the debug session receives chanperf data. + */ +export class ChanperfEvent extends CustomEvent { constructor(output: ChanperfData) { - this.body = output; - this.event = 'BSChanperfEvent'; + super(output); } +} - public body: ChanperfData; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `ChanperfEvent` + */ +export function isChanperfEvent(event: any): event is ChanperfEvent { + return !!event && event.event === ChanperfEvent.name; } -export class LaunchStartEvent implements DebugProtocol.Event { - constructor(args: LaunchConfiguration) { - this.body = args; - this.event = 'BSLaunchStartEvent'; + +/** + * Emitted when the launch sequence first starts. This is right after the debug session receives the `launch` request, + * which happens before any zipping, sideloading, etc. + */ +export class LaunchStartEvent extends CustomEvent { + constructor(launchConfiguration: LaunchConfiguration) { + super(launchConfiguration); } +} - public body: any; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `LaunchStartEvent` + */ +export function isLaunchStartEvent(event: any): event is LaunchStartEvent { + return !!event && event.event === LaunchStartEvent.name; } -export class PopupMessageEvent implements DebugProtocol.Event { +/** + * This event indicates that the client should show a popup message with the supplied information + */ +export class PopupMessageEvent extends CustomEvent<{ message: string; severity: 'error' | 'info' | 'warn' }> { constructor(message: string, severity: 'error' | 'info' | 'warn') { - this.body = { message, severity }; - this.event = 'BSPopupMessageEvent'; + super({ message, severity }); } +} - public body: any; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `PopupMessageEvent` + */ +export function isPopupMessageEvent(event: any): event is PopupMessageEvent { + return !!event && event.event === PopupMessageEvent.name; } -export class ChannelPublishedEvent implements DebugProtocol.Event { +/** + * Emitted once the channel has been sideloaded to the channel and the session is ready to start actually debugging. + */ +export class ChannelPublishedEvent extends CustomEvent<{ launchConfiguration: LaunchConfiguration }> { constructor( - body: { - launchConfiguration: LaunchConfiguration; - } + launchConfiguration: LaunchConfiguration ) { - this.body = body ?? {}; - this.event = 'BSChannelPublishedEvent'; + super({ launchConfiguration }); } - - public body: any; - public event: string; - public seq: number; - public type: string; } +/** + * Is the object a `ChannelPublishedEvent` + */ +export function isChannelPublishedEvent(event: any): event is ChannelPublishedEvent { + return !!event && event.event === ChannelPublishedEvent.name; +} export enum StoppedEventReason { step = 'step', breakpoint = 'breakpoint', exception = 'exception', pause = 'pause', - entry = 'entry' + entry = 'entry', + goto = 'goto', + functionBreakpoint = 'function breakpoint', + dataBreakpoint = 'data breakpoint', + instructionBreakpoint = 'instruction breakpoint' } diff --git a/src/index.ts b/src/index.ts index 8e266d1b..553afdd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export * from './managers/BreakpointManager'; export * from './LaunchConfiguration'; export * from './debugProtocol/Debugger'; export * from './debugSession/BrightScriptDebugSession'; +export * from './debugSession/Events'; export * from './ComponentLibraryServer'; export * from './CompileErrorProcessor'; export * from './debugProtocol/Constants'; From 97b870d1f33c8d3eafbf60f6f36ab18cccfc6a44 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 2 Sep 2022 12:39:06 -0400 Subject: [PATCH 043/197] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 377bcad7..d2539eab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: COVERALLS_REPO_TOKEN: "NMGk1IhVG2Ds5VQKiEuXpZE8xftkORa7W" strategy: matrix: - os: [ubuntu-18.04, macos-10.15, windows-2019] + os: [ubuntu-latest, ubuntu-latest, ubuntu-latest] steps: - uses: actions/checkout@master - uses: actions/setup-node@master @@ -24,7 +24,7 @@ jobs: #only run this task if a tag starting with 'v' was used to trigger this (i.e. a tagged release) if: startsWith(github.ref, 'refs/tags/v') needs: ci - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: From 37b322a46d38b40cd1e63ba0b0ccb987f14e9687 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 2 Sep 2022 12:39:27 -0400 Subject: [PATCH 044/197] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2539eab..412c9553 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: COVERALLS_REPO_TOKEN: "NMGk1IhVG2Ds5VQKiEuXpZE8xftkORa7W" strategy: matrix: - os: [ubuntu-latest, ubuntu-latest, ubuntu-latest] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@master - uses: actions/setup-node@master From 01733231bc4196d692b0c43949c9057a35790e47 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 7 Sep 2022 10:13:46 -0400 Subject: [PATCH 045/197] Device diagnostics instead of compile errors (#104) * Standardize custom events, add is* helpers * Better compile error handling * Emit all diagnostics instead of just compile errors * Apply suggestions from code review Co-authored-by: christopher Dwyer-Perkins * remove GENERAL_XML_ERROR * Add test for source location test Co-authored-by: christopher Dwyer-Perkins --- src/CompileErrorProcessor.spec.ts | 554 ++++++++++++------ src/CompileErrorProcessor.ts | 403 ++++++------- src/adapters/DebugProtocolAdapter.ts | 10 +- src/adapters/TelnetAdapter.ts | 13 +- .../BrightScriptDebugSession.spec.ts | 37 +- src/debugSession/BrightScriptDebugSession.ts | 51 +- src/debugSession/Events.spec.ts | 6 +- src/debugSession/Events.ts | 14 +- src/index.ts | 2 - src/util.spec.ts | 52 +- src/util.ts | 19 - 11 files changed, 664 insertions(+), 497 deletions(-) diff --git a/src/CompileErrorProcessor.spec.ts b/src/CompileErrorProcessor.spec.ts index ba6904ff..1caabf12 100644 --- a/src/CompileErrorProcessor.spec.ts +++ b/src/CompileErrorProcessor.spec.ts @@ -1,10 +1,12 @@ -import type { BrightScriptDebugCompileError } from './CompileErrorProcessor'; +import type { BSDebugDiagnostic } from './CompileErrorProcessor'; import { CompileErrorProcessor, CompileStatus } from './CompileErrorProcessor'; -import { expect, assert } from 'chai'; +import { expect } from 'chai'; +import type { SinonFakeTimers } from 'sinon'; import { createSandbox } from 'sinon'; +import { util as bscUtil } from 'brighterscript'; const sinon = createSandbox(); -describe('BrightScriptDebugger', () => { +describe('CompileErrorProcessor', () => { let compiler: CompileErrorProcessor; beforeEach(() => { @@ -15,100 +17,292 @@ describe('BrightScriptDebugger', () => { }); afterEach(() => { - compiler = undefined; sinon.restore(); + compiler.destroy(); + compiler = undefined; }); - describe('getSingleFileXmlError ', () => { - it('tests no input', () => { - let input = ['']; - let errors = compiler.getSingleFileXmlError(input); - assert.isEmpty(errors); + describe('events', () => { + let clock: SinonFakeTimers; + beforeEach(() => { + clock = sinon.useFakeTimers(); }); - it('tests no match', () => { - let input = ['some other output']; - let errors = compiler.getSingleFileXmlError(input); - assert.isEmpty(errors); + afterEach(() => { + clock.restore(); }); - it('tests no match multiline', () => { - let input = [`multiline text`, `with no match`]; - let errors = compiler.getSingleFileXmlError(input); - assert.isEmpty(errors); + it('it allows unsubscribing', () => { + let count = 0; + const unobserve = compiler.on('diagnostics', () => { + count++; + unobserve(); + }); + compiler['emit']('diagnostics'); + compiler['emit']('diagnostics'); + + clock.tick(200); + expect(count).to.eql(1); + }); + + it('does not throw when emitter is destroyed', () => { + const unobserve = compiler.on('diagnostics', () => { }); + delete compiler['emitter']; + unobserve(); + compiler['emit']('diagnostics'); + clock.tick(200); + //test passes because no exception was thrown + }); + + it('skips emitting the event when there are zero errros', () => { + let callCount = 0; + const unobserve = compiler.on('diagnostics', () => { + callCount++; + }); + compiler['reportErrors'](); + clock.tick(200); + expect(callCount).to.equal(0); }); - it('match', () => { - let input = [`-------> Error parsing XML component SimpleEntitlements.xml`]; - let errors = compiler.getSingleFileXmlError(input); - assert.lengthOf(errors, 1); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('excludes diagnostics that are missing a path', () => { + sinon.stub(compiler as any, 'processMultiLineErrors').returns({}); + expect( + compiler.getErrors(['']) + ).to.eql([]); + }); + + describe('sendErrors', () => { + it('emits the errors', async () => { + compiler.processUnhandledLines(`-------> Error parsing XML component SimpleButton.xml`); + let callCount = 0; + compiler.on('diagnostics', () => { + callCount++; + }); + let promise = compiler.sendErrors(); + clock.tick(1000); + await promise; + expect(callCount).to.eql(1); + + }); }); }); - describe('getMultipleFileXmlError ', () => { - it('tests no input', () => { - let input = ['']; - let errors = compiler.getMultipleFileXmlError(input); - assert.isEmpty(errors); + describe('parseGenericXmlError ', () => { + it('handles empty line', () => { + expect( + compiler.getErrors([``]) + ).to.eql([]); }); - it('tests no match', () => { - let input = ['some other output']; - let errors = compiler.getMultipleFileXmlError(input); - assert.isEmpty(errors); + it('handles non match', () => { + expect( + compiler.getErrors(['some other output']) + ).to.eql([]); }); - it('tests no match multiline', () => { - let input = [`multiline text`, `with no match`]; - let errors = compiler.getMultipleFileXmlError(input); - assert.isEmpty(errors); + it('handles multi-line non match no match multiline', () => { + expect( + compiler.getErrors([`multiline text`, `with no match`]) + ).to.eql([]); }); - it('match 1 file', () => { - let input = [`-------> Error parsing multiple XML components (SimpleEntitlements.xml)`]; - let errors = compiler.getMultipleFileXmlError(input); - assert.lengthOf(errors, 1); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('matches relative xml path', () => { + expect( + compiler.getErrors([`-------> Error parsing XML component SimpleButton.xml`]) + ).to.eql([{ + path: 'SimpleButton.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); }); - it('match 2 files', () => { - let input = [`-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`]; - let errors = compiler.getMultipleFileXmlError(input); - assert.lengthOf(errors, 2); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('matches absolute xml path', () => { + expect( + compiler.getErrors([`-------> Error parsing XML component pkg:/components/SimpleButton.xml`]) + ).to.eql([{ + path: 'pkg:/components/SimpleButton.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); + }); + }); + + it('handles when the next line is missing', () => { + expect( + compiler.getErrors([ + `Error in XML component RedButton defined in file pkg:/components/RedButton.xml` + //normally there's another line here, containing something like `-- Extends type does not exist: "ColoredButton"`. + //This test omits it on purpose to make sure we can still detect an error + ]) + ).to.eql([{ + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error in XML component RedButton', + path: 'pkg:/components/RedButton.xml', + code: undefined + }]); + }); - let error2 = errors[1]; - assert.equal(error2.path, 'Otherfile.xml'); + describe('parseSyntaxAndCompileErrors', () => { + it('works with standard message', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: '&h92' + }]); }); - it('match 2 files amongst other stuff', () => { - let input = [ - `some other output`, - `some other output2`, - `-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`, - `some other output3` - ]; - let errors = compiler.getMultipleFileXmlError(input); - assert.lengthOf(errors, 2); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('works with zero leading junk', () => { + expect( + compiler.getErrors([`Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: '&h92' + }]); + }); + + it('works when missing trailing context', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19)`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined)`, + code: '&h92' + }]); + }); + + it('works when missing line number', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs() 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: '&h92' + }]); + }); + + it('works when missing error code', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error ) in Parsers.brs(19) 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: undefined + }]); + }); + }); + + describe('getMultipleFileXmlError ', () => { + it('matches 1 relative file', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (SimpleEntitlements.xml)`]) + ).to.eql([{ + path: 'SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); + }); - let error2 = errors[1]; - assert.equal(error2.path, 'Otherfile.xml'); + it('matches 2 relative files', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`]) + ).to.eql([{ + path: 'SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }, { + path: 'Otherfile.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); + }); + + it('matches 1 absolute file', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (pkg:/components/SimpleEntitlements.xml)`]) + ).to.eql([{ + path: 'pkg:/components/SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); + }); + + it('matches 2 absolute files', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (pkg:/components/SimpleEntitlements.xml, pkg:/components/Otherfile.xml)`]) + ).to.eql([{ + path: 'pkg:/components/SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }, { + path: 'pkg:/components/Otherfile.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); + }); + + it('match 2 files amongst other stuff', () => { + expect( + compiler.getErrors([ + `some other output`, + `some other output2`, + `-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`, + `some other output3` + ]) + ).to.eql([{ + path: 'SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }, { + path: 'Otherfile.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined + }]); }); }); + it('ignores livecompile errors', () => { + expect( + compiler.getErrors([ + `------ Compiling dev 'sampleApp' ------`, + `=================================================================`, + `Found 1 compile error in file tmp/plugin/IJBAAAfijvb8/pkg:/components/Scene/MainScene.GetConfigurationw.brs`, + `--- Error loading file. (compile error &hb9) in pkg:/components/Scene/MainScene.GetConfigurationw.brs`, + `A block (such as FOR/NEXT or IF/ENDIF) was not terminated correctly. (compile error &hb5) in $LIVECOMPILE(1190)`, + `BrightScript Debugger> while True` + ]) + ).to.eql([{ + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error loading file', + path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs', + code: '&hb9' + }]); + }); + describe('processUnhandledLines', () => { - async function runTest(lines: string[], expectedStatus: CompileStatus, expectedErrors?: BrightScriptDebugCompileError[]) { - let compileErrors: BrightScriptDebugCompileError[]; + async function runTest(lines: string[], expectedStatus: CompileStatus, expectedErrors?: BSDebugDiagnostic[]) { + let compileErrors: BSDebugDiagnostic[]; let promise: Promise; if (expectedErrors) { promise = new Promise((resolve) => { - compiler.on('compile-errors', (errors) => { + compiler.on('diagnostics', (errors) => { compileErrors = errors; resolve(); }); @@ -170,20 +364,61 @@ describe('BrightScriptDebugger', () => { `-------> Compilation Failed.` ]; - let expectedErrors = [{ - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'Found 1 compile error in file tmp/plugin/IJBAAAfijvb8/pkg:/components/Scene/MainScene.GetConfigurationw.brs\n--- Error loading file. (compile error &hb9) in pkg:/components/Scene/MainScene.GetConfigurationw.brs', - path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs' - }]; + await runTest(lines, CompileStatus.compileError, [{ + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error loading file', + path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs', + code: '&hb9' + }]); + }); - await runTest(lines, CompileStatus.compileError, expectedErrors); + it('detects multi-line syntax errors', async () => { + await runTest([ + `08-25 19:03:56.531 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0 ms)`, + `08-25 19:03:56.531 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms)`, + `08-25 19:03:56.531 [scrpt.cmpl] Compiling 'Hello World Console 2', id 'dev'`, + `08-25 19:03:56.532 [scrpt.load.mkup] Loading markup dev 'Hello World Console 2'`, + `08-25 19:03:56.532 [scrpt.unload.mkup] Unloading markup dev 'Hello World Console 2'`, + `=================================================================`, + `Found 3 parse errors in XML file Foo.xml`, + `--- Line 2: Unexpected data found inside a element (first 10 characters are "aaa")`, + `--- Line 3: Some unique error message`, + `--- Line 5: message with Line 4 inside it`, + `08-25 19:03:56.536 [scrpt.parse.mkup.time] Parsed markup dev 'Hello World Console 2' in 4 milliseconds`, + `------ Compiling dev 'Hello World Console 2' ------`, + `BRIGHTSCRIPT: WARNING: unused variable 'person' in function 'main' in #130`, + `BRIGHTSCRIPT: WARNING: unused variable 'arg1' in function 'noop' in #131`, + `Displayed 2 of 2 warnings`, + `08-25 19:03:56.566 [scrpt.ctx.cmpl.time] Compiled 'Hello World Console 2', id 'dev' in 29 milliseconds (BCVer:0)`, + `08-25 19:03:56.567 [scrpt.unload.mkup] Unloading markup dev 'Hello World Console 2'`, + `=================================================================`, + `An error occurred while attempting to compile the application's components:`, + `-------> Error parsing XML component Foo.xml` + ], CompileStatus.compileError, [{ + range: bscUtil.createRange(1, 0, 1, 999), + message: 'Unexpected data found inside a element (first 10 characters are "aaa")', + path: 'Foo.xml', + code: undefined + }, { + range: bscUtil.createRange(2, 0, 2, 999), + message: 'Some unique error message', + path: 'Foo.xml', + code: undefined + }, { + range: bscUtil.createRange(4, 0, 4, 999), + message: 'message with Line 4 inside it', + path: 'Foo.xml', + code: undefined + }, { + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'Foo.xml', + code: undefined + }]); }); it('detects XML syntax error', async () => { - let lines = [ + await runTest([ `03-26 22:21:46.570 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)`, `03-26 22:21:46.571 [beacon.signal] |AppCompileInitiate --------> TimeBase(1 ms)`, `03-26 22:21:46.571 [scrpt.cmpl] Compiling 'sampleApp', id 'dev'`, @@ -205,27 +440,19 @@ describe('BrightScriptDebugger', () => { `-------> Error parsing XML component SampleScreen.xml`, ``, `[RAF] Roku_Ads Framework version 2.1231` - ]; - - let expectedErrors = [ + ], CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 3, + range: bscUtil.createRange(2, 0, 2, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', - path: 'SampleScreen.xml' + path: 'SampleScreen.xml', + code: undefined }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'General XML compilation error', - path: 'SampleScreen.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'SampleScreen.xml', + code: undefined } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects BRS syntax error', async () => { @@ -247,46 +474,34 @@ describe('BrightScriptDebugger', () => { `--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(734)` ]; - let expectedErrors = [ + await runTest(lines, CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - lineNumber: 595, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(595)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(595)', + range: bscUtil.createRange(595 - 1, 0, 595 - 1, 999), + code: '&h02', + message: 'Syntax Error', path: 'pkg:/components/Services/Network/Parsers.brs' }, { - charEnd: 999, - charStart: 0, - lineNumber: 598, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(598)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(598)', + range: bscUtil.createRange(598 - 1, 0, 598 - 1, 999), + code: '&h02', + message: 'Syntax Error', path: 'pkg:/components/Services/Network/Parsers.brs' }, { - charEnd: 999, - charStart: 0, - lineNumber: 732, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(732)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(732)', + range: bscUtil.createRange(732 - 1, 0, 732 - 1, 999), + code: '&h02', + message: 'Syntax Error', path: 'pkg:/components/Services/Network/Parsers.brs' }, { - charEnd: 999, - charStart: 0, - lineNumber: 733, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(733)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(733)', + range: bscUtil.createRange(733 - 1, 0, 733 - 1, 999), + code: '&h02', + message: 'Syntax Error', path: 'pkg:/components/Services/Network/Parsers.brs' }, { - charEnd: 999, - charStart: 0, - lineNumber: 734, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(734)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(734)', + range: bscUtil.createRange(734 - 1, 0, 734 - 1, 999), + code: '&h02', + message: 'Syntax Error', path: 'pkg:/components/Services/Network/Parsers.brs' } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects Multiple XML syntax errors', async () => { @@ -302,8 +517,8 @@ describe('BrightScriptDebugger', () => { `--- Line 3: XML syntax error found ---> not well-formed (invalid token)`, ``, `=================================================================`, - `Error in XML component Oops defined in file pkg:/components/Oops.xml`, - `-- Extends type does not exist: "BaseOops"`, + `Error in XML component RedButton defined in file pkg:/components/RedButton.xml`, + `-- Extends type does not exist: "ColoredButton"`, ``, `=================================================================`, `Found 1 parse error in XML file ChannelItemComponent.xml`, @@ -316,49 +531,42 @@ describe('BrightScriptDebugger', () => { ``, `=================================================================`, `An error occurred while attempting to compile the application's components:`, - `-------> Error parsing multiple XML components (SampleScreen.xml, ChannelItemComponent.xml, Ooops)` + `-------> Error parsing multiple XML components (SampleScreen.xml, ChannelItemComponent.xml, RedButton.xml)` ]; - let expectedErrors = [ + await runTest(lines, CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 3, + range: bscUtil.createRange(2, 0, 2, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', - path: 'SampleScreen.xml' + path: 'SampleScreen.xml', + code: undefined + }, { + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Extends type does not exist: "ColoredButton"', + path: 'pkg:/components/RedButton.xml', + code: undefined }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 9, + range: bscUtil.createRange(8, 0, 8, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', - path: 'ChannelItemComponent.xml' + path: 'ChannelItemComponent.xml', + code: undefined }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'General XML compilation error', - path: 'SampleScreen.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'SampleScreen.xml', + code: undefined }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'General XML compilation error', - path: 'ChannelItemComponent.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'ChannelItemComponent.xml', + code: undefined }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'Error in XML component Oops defined in file pkg:/components/Oops.xml\n-- Extends type does not exist: "BaseOops"', - path: 'pkg:/components/Oops.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'RedButton.xml', + code: undefined } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects Invalid #If/#ElseIf expression', async () => { @@ -377,22 +585,18 @@ describe('BrightScriptDebugger', () => { `--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) 'BAD_BS_CONST'` ]; - let expectedErrors = [ + await runTest(lines, CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: '--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) \'BAD_BS_CONST\'', - lineNumber: 19, - message: 'compile error &h92) in Parsers.brs(19) \'BAD_BS_CONST\'', + code: '&h92', + range: bscUtil.createRange(19 - 1, 0, 19 - 1, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, path: 'Parsers.brs' } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects No manifest', async () => { - let lines = [ + await runTest([ `03-27 00:19:07.768 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)`, `03-27 00:19:07.768 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms)`, `03-27 00:19:07.768 [scrpt.cmpl] Compiling 'sampleApp', id 'dev'`, @@ -406,19 +610,13 @@ describe('BrightScriptDebugger', () => { `An error occurred while attempting to install the application:`, ``, `------->No manifest. Invalid package.` - ]; - - let expectedErrors = [ + ], CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'No manifest. Invalid package.', - path: 'manifest' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'No manifest. Invalid package', + path: 'pkg:/manifest' } - ]; - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); }); diff --git a/src/CompileErrorProcessor.ts b/src/CompileErrorProcessor.ts index dfc3ec94..0c4b23db 100644 --- a/src/CompileErrorProcessor.ts +++ b/src/CompileErrorProcessor.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; +import type { Diagnostic } from 'vscode-languageserver-protocol/node'; import { logger } from './logging'; - -export const GENERAL_XML_ERROR = 'General XML compilation error'; +import { util as bscUtil } from 'brighterscript'; export class CompileErrorProcessor { @@ -14,7 +14,7 @@ export class CompileErrorProcessor { private emitter = new EventEmitter(); public compileErrorTimer: NodeJS.Timeout; - public on(eventName: 'compile-errors', handler: (params: BrightScriptDebugCompileError[]) => void); + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: string, handler: (payload: any) => void) { this.emitter.on(eventName, handler); return () => { @@ -24,7 +24,7 @@ export class CompileErrorProcessor { }; } - private emit(eventName: 'compile-errors', data?) { + private emit(eventName: 'diagnostics', data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues setTimeout(() => { //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists @@ -87,220 +87,222 @@ export class CompileErrorProcessor { }); } - private getErrors() { - return [ - ...this.getSyntaxErrors(this.compilingLines), - ...this.getCompileErrors(this.compilingLines), - ...this.getMultipleFileXmlError(this.compilingLines), - ...this.getSingleFileXmlError(this.compilingLines), - ...this.getSingleFileXmlComponentError(this.compilingLines), - ...this.getMissingManifestError(this.compilingLines) - ]; + public getErrors(lines: string[]) { + const result: BSDebugDiagnostic[] = []; + //clone the lines so the parsers can manipulate them + lines = [...lines]; + while (lines.length > 0) { + const startLength = lines.length; + const line = lines[0]; + + if (line) { + result.push( + ...[ + this.processMultiLineErrors(lines), + this.parseComponentDefinedInFileError(lines), + this.parseGenericXmlError(line), + this.parseSyntaxAndCompileErrors(line), + this.parseMissingManifestError(line) + ].flat().filter(x => !!x) + ); + } + //if none of the parsers consumed a line, remove the first line + if (lines.length === startLength) { + lines.shift(); + } + } + return result.filter(x => { + //throw out $livecompile errors (those are generated by REPL/eval code) + return x.path && !x.path.toLowerCase().includes('$livecompile'); + }); } /** - * Runs a regex to get the content between telnet commands - * @param value + * Parse generic xml errors with no further context below */ - private getSyntaxErrorDetails(value: string) { - return /(syntax|compile) error.* in (.*)\((\d+)\)(.*)/gim.exec(value); + public parseGenericXmlError(line: string): BSDebugDiagnostic[] { + let [, message, files] = this.execAndTrim( + // https://regex101.com/r/LDUyww/3 + /^(?:-+\>)?\s*(Error parsing (?:multiple )?XML component[s]?)\s+\(?(.+\.xml)\)?.*$/igm, + line + ) ?? []; + if (message && typeof files === 'string') { + //use the singular xml parse message since the plural doesn't make much sense when attached to a single file + if (message.toLowerCase() === 'error parsing multiple xml components') { + message = 'Error parsing XML component'; + } + //there can be 1 or more file paths, so add a distinct error for each one + return files.split(',') + .map(filePath => ({ + path: this.sanitizeCompilePath(filePath), + range: bscUtil.createRange(0, 0, 0, 999), + message: this.buildMessage(message), + code: undefined + })) + .filter(x => !!x); + } } - public getSyntaxErrors(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let match: RegExpExecArray; - // let syntaxRegEx = /(syntax|compile) error.* in (.*)\((\d+)\)/gim; - for (const line of lines) { - match = this.getSyntaxErrorDetails(line); - if (match) { - let path = this.sanitizeCompilePath(match[2]); - let lineNumber = parseInt(match[3]); //1-based - - //FIXME - //if this match is a livecompile error, throw out all prior errors because that means we are re-running - if (!path.toLowerCase().includes('$livecompile')) { + /** + * Parse the standard syntax and compile error format + */ + private parseSyntaxAndCompileErrors(line: string): BSDebugDiagnostic[] { + let [, message, errorType, code, trailingInfo] = this.execAndTrim( + // https://regex101.com/r/HHZ6dE/3 + /(.*?)(?:\(((?:syntax|compile)\s+error)\s+(&h[\w\d]+)?\s*\))\s*in\b\s+(.+)/ig, + line + ) ?? []; + + if (message) { + //split the file path, line number, and trailing context if available. + let [, filePath, lineNumber, context] = this.execAndTrim( + /(.+)\((\d+)?\)(.*)/ig, + trailingInfo + //default the `filePath` var to the whole `trailingInfo` string + ) ?? [null, trailingInfo, null, null]; + + return [{ + path: this.sanitizeCompilePath(filePath), + message: this.buildMessage(message, context), + range: this.getRange(lineNumber), //lineNumber is 1-based + code: code + }]; + } + } + /** + * Handles when an error lists the filename on the first line, then subsequent lines each have 1 error. + * Stops on the first line that doesn't have an error line. Like this: + * ``` + * Found 3 parse errors in XML file Foo.xml + * --- Line 2: Unexpected data found inside a element (first 10 characters are "aaa") + * --- Line 3: Some unique error message + * --- Line 5: message with Line 4 inside it + */ + private processMultiLineErrors(lines: string[]): BSDebugDiagnostic[] { + const errors = []; + let [, count, filePath] = this.execAndTrim( + // https://regex101.com/r/wBMp8B/1 + /found (\d+).*error[s]? in.*?file(.*)/gmi, + lines[0] + ) ?? []; + filePath = this.sanitizeCompilePath(filePath); + if (filePath) { + let i = 0; + //parse each line that looks like it's an error. + for (i = 1; i < lines.length; i++) { + //example: `Line 1: Unexpected data found inside a element (first 10 characters are "aaa")`) + const [, lineNumber, message] = this.execAndTrim( + /^[\-\s]*line (\d*):(.*)$/gim, + lines[i] + ) ?? []; + if (lineNumber && message) { errors.push({ - path: path, - lineNumber: lineNumber, - errorText: line, - message: match[0].trim(), - charStart: 0, - charEnd: 999 //TODO + path: filePath, + range: this.getRange(lineNumber), //lineNumber is 1-based + message: this.buildMessage(message), + code: undefined }); + } else { + //assume there are no more errors for this file + break; } } + //remove the lines we consumed + lines.splice(0, i); } return errors; } - public getCompileErrors(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let responseText = lines.join('\n'); - const filesWithErrors = responseText.split('================================================================='); - if (filesWithErrors.length < 2) { - return []; - } - - let getFileInfoRegEx = /Found(?:.*)file (.*)$/im; - for (let index = 1; index < filesWithErrors.length - 1; index++) { - const fileErrorText = filesWithErrors[index]; - //TODO - for now just a simple parse - later on someone can improve with proper line checks + all parse/compile types - //don't have time to do this now; just doing what keeps me productive. - let match = getFileInfoRegEx.exec(fileErrorText); - if (!match) { - continue; - } - - let path = this.sanitizeCompilePath(match[1]); - let lineNumber = 1; //TODO this should iterate over all line numbers found in a file - let errorText = 'ERR_COMPILE:'; - let message = fileErrorText.trim(); - - let error = { - path: path, - lineNumber: lineNumber, - errorText: errorText, - message: message, - charStart: 0, - charEnd: 999 //TODO - }; - - //now iterate over the lines, to see if there's any errors we can extract - let lineErrors = this.getLineErrors(path, fileErrorText); - if (lineErrors.length > 0) { - errors.push(...lineErrors); - } else { - errors.push(error); + /** + * Parse errors that look like this: + * ``` + * Error in XML component RedButton defined in file pkg:/components/RedButton.xml + * -- Extends type does not exist: "ColoredButton" + */ + private parseComponentDefinedInFileError(lines: string[]): BSDebugDiagnostic[] { + let [, message, filePath] = this.execAndTrim( + /(Error in XML component [a-z0-9_-]+) defined in file (.*)/i, + lines[0] + ) ?? []; + if (filePath) { + lines.shift(); + //assume the next line includes the actual error message + if (lines[0]) { + message = lines.shift(); } + return [{ + message: this.buildMessage(message), + path: this.sanitizeCompilePath(filePath), + range: this.getRange(), + code: undefined + }]; } - return errors; } - public getLineErrors(path: string, fileErrorText: string): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /^--- Line (\d*): (.*)$/gim; - let match: RegExpExecArray; - while ((match = getFileInfoRegEx.exec(fileErrorText))) { - let lineNumber = parseInt(match[1]); // 1-based - let errorText = 'ERR_COMPILE:'; - let message = this.sanitizeCompilePath(match[2]); - - errors.push({ - path: path, - lineNumber: lineNumber, - errorText: errorText, - message: message, - charStart: 0, - charEnd: 999 //TODO - }); + /** + * Parse error messages that look like this: + * ``` + * ------->No manifest. Invalid package. + * ``` + */ + private parseMissingManifestError(line: string): BSDebugDiagnostic[] { + let [, message] = this.execAndTrim( + // https://regex101.com/r/ANr5xd/1 + /^(?:-+)>(No manifest\. Invalid package\.)/i + , + line + ) ?? []; + if (message) { + return [{ + path: 'pkg:/manifest', + range: bscUtil.createRange(0, 0, 0, 999), + message: this.buildMessage(message) + }]; } - - return errors; } - public getSingleFileXmlError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /^-------> Error parsing XML component (.*).*$/i; - for (let line of lines) { - let match = getFileInfoRegEx.exec(line); - if (match) { - let errorText = 'ERR_COMPILE:'; - let path = this.sanitizeCompilePath(match[1]); - - errors.push({ - path: path, - lineNumber: 1, - errorText: errorText, - message: GENERAL_XML_ERROR, - charStart: 0, - charEnd: 999 //TODO - }); - } - } - - return errors; + /** + * Exec the regexp, and if there's a match, trim every group + */ + private execAndTrim(pattern: RegExp, text: string) { + return pattern.exec(text)?.map(x => x?.trim()); } - public getSingleFileXmlComponentError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /Error in XML component [a-z0-9_-]+ defined in file (.*)/i; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - let match = getFileInfoRegEx.exec(line); - if (match) { - let errorText = 'ERR_COMPILE:'; - let path = match[1]; - errors.push({ - path: path, - lineNumber: 1, - errorText: errorText, - message: `${line}\n${lines[i + 1] ?? ''}`, - charStart: 0, - charEnd: 999 //TODO - }); - } - } - return errors; - } + private buildMessage(message: string, context?: string) { + //remove any leading dashes or whitespace + message = message.replace(/^[ \t\-]+/g, ''); - public getMultipleFileXmlError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /^-------> Error parsing multiple XML components \((.*)\)/i; - for (const line of lines) { - let match = getFileInfoRegEx.exec(line); - if (match) { - let errorText = 'ERR_COMPILE:'; - let filePaths = match[1].split(','); - for (const path of filePaths) { - errors.push({ - path: this.sanitizeCompilePath(path.trim()), - lineNumber: 1, - errorText: errorText, - message: GENERAL_XML_ERROR, - charStart: 0, - charEnd: 999 //TODO - }); - } - } + //append context to end of message (if available) + if (context?.length > 0) { + message += ' ' + context; } + //remove trailing period from message + message = message.replace(/[\s.]+$/, ''); - return errors; + return message; } - public getMissingManifestError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getMissingManifestErrorRegEx = /^(?:-+)>(No manifest\. Invalid package\.)/i; - for (const line of lines) { - let match = getMissingManifestErrorRegEx.exec(line); - if (match) { - errors.push({ - path: 'manifest', - lineNumber: 1, - errorText: 'ERR_COMPILE:', - message: match[1], - charStart: 0, - charEnd: 999 //TODO - }); - } - } - - return errors; + /** + * Given a text-based line number, convert it to a number and return a range. + * Defaults to line number 1 (1-based) if unable to parse. + * @returns a zero-based vscode `Range` object + */ + private getRange(lineNumberText?: string) { + //convert the line number to an integer (if applicable) + let lineNumber = parseInt(lineNumberText); //1-based + lineNumber = isNaN(lineNumber) ? 1 : lineNumber; + return bscUtil.createRange(lineNumber - 1, 0, lineNumber - 1, 999); } + /** + * Trim all leading junk up to the `pkg:/` in this string + */ public sanitizeCompilePath(debuggerPath: string): string { - let protocolIndex = debuggerPath.indexOf('pkg:/'); - - if (protocolIndex > 0) { - return debuggerPath.slice(protocolIndex); - } - - return debuggerPath; + return debuggerPath?.replace(/.*?(?=pkg:\/)/, '')?.trim(); } public resetCompileErrorTimer(isRunning): any { - // console.debug('resetCompileErrorTimer isRunning' + isRunning); - if (this.compileErrorTimer) { clearInterval(this.compileErrorTimer); this.compileErrorTimer = undefined; @@ -308,7 +310,6 @@ export class CompileErrorProcessor { if (isRunning) { if (this.status === CompileStatus.compileError) { - // console.debug('resetting resetCompileErrorTimer'); this.compileErrorTimer = setTimeout(() => { this.onCompileErrorTimer(); }, this.compileErrorTimeoutMs); @@ -317,8 +318,6 @@ export class CompileErrorProcessor { } public onCompileErrorTimer() { - console.debug('onCompileErrorTimer: timer complete. should\'ve caught all errors '); - this.status = CompileStatus.compileError; this.resetCompileErrorTimer(false); this.reportErrors(); @@ -354,26 +353,30 @@ export class CompileErrorProcessor { * @param responseText */ private reportErrors() { - console.debug('reportErrors'); - - const errors = this.getErrors().filter((e) => { - const path = e.path.toLowerCase(); - return path.endsWith('.brs') || path.endsWith('.xml') || path === 'manifest'; - }); - + const errors = this.getErrors(this.compilingLines); if (errors.length > 0) { - this.emit('compile-errors', errors); + this.emit('diagnostics', errors); + } + } + + public destroy() { + if (this.emitter) { + this.emitter.removeAllListeners(); } } } -export interface BrightScriptDebugCompileError { +export interface BSDebugDiagnostic extends Diagnostic { + /** + * Path to the file in question. When emitted from a Roku device, this will be a full pkgPath (i.e. `pkg:/source/main.brs`). + * As it flows through the program, this may be modified to represent a source location (i.e. `C:/projects/app/source/main.brs`) + */ path: string; - lineNumber: number; - message: string; - errorText: string; - charStart: number; - charEnd: number; + /** + * The name of the component library this diagnostic was emitted from. Should be undefined if diagnostic originated from the + * main app. + */ + componentLibraryName?: string; } export enum CompileStatus { diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index f523ae12..9fdc2963 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -2,6 +2,7 @@ import type { ConstructorOptions, ProtocolVersionDetails } from '../debugProtoco import { Debugger } from '../debugProtocol/Debugger'; import * as EventEmitter from 'events'; import { Socket } from 'net'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import { CompileErrorProcessor } from '../CompileErrorProcessor'; import type { RendezvousHistory } from '../RendezvousTracker'; import { RendezvousTracker } from '../RendezvousTracker'; @@ -86,7 +87,7 @@ export class DebugProtocolAdapter { public on(eventname: 'chanperf', handler: (output: ChanperfData) => void); public on(eventName: 'close', handler: () => void); public on(eventName: 'app-exit', handler: () => void); - public on(eventName: 'compile-errors', handler: (params: { path: string; lineNumber: number }[]) => void); + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: 'connected', handler: (params: boolean) => void); public on(eventname: 'console-output', handler: (output: string) => void); // TODO: might be able to remove this at some point. public on(eventname: 'protocol-version', handler: (output: ProtocolVersionDetails) => void); @@ -105,7 +106,8 @@ export class DebugProtocolAdapter { } private emit(eventName: 'suspend'); - private emit(eventName: 'app-exit' | 'cannot-continue' | 'chanperf' | 'close' | 'compile-errors' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output', data?); + private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]); + private emit(eventName: 'app-exit' | 'cannot-continue' | 'chanperf' | 'close' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output', data?); private emit(eventName: string, data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues setTimeout(() => { @@ -279,9 +281,9 @@ export class DebugProtocolAdapter { let deferred = defer(); try { this.compileClient = new Socket(); - this.compileErrorProcessor.on('compile-errors', (errors) => { + this.compileErrorProcessor.on('diagnostics', (errors) => { this.compileClient.end(); - this.emit('compile-errors', errors); + this.emit('diagnostics', errors); }); //if the connection fails, reject the connect promise diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 73e44165..67b4858d 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -3,6 +3,7 @@ import * as EventEmitter from 'eventemitter3'; import { Socket } from 'net'; import { rokuDeploy } from 'roku-deploy'; import { PrintedObjectParser } from '../PrintedObjectParser'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import { CompileErrorProcessor } from '../CompileErrorProcessor'; import type { RendezvousHistory } from '../RendezvousTracker'; import { RendezvousTracker } from '../RendezvousTracker'; @@ -81,7 +82,7 @@ export class TelnetAdapter { public on(eventname: 'chanperf', handler: (output: ChanperfData) => void); public on(eventName: 'close', handler: () => void); public on(eventName: 'app-exit', handler: () => void); - public on(eventName: 'compile-errors', handler: (params: { path: string; lineNumber: number }[]) => void); + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: 'connected', handler: (params: boolean) => void); public on(eventname: 'console-output', handler: (output: string) => void); public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); @@ -98,6 +99,7 @@ export class TelnetAdapter { }; } + private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]); private emit( /* eslint-disable @typescript-eslint/indent */ eventName: @@ -105,7 +107,6 @@ export class TelnetAdapter { 'cannot-continue' | 'chanperf' | 'close' | - 'compile-errors' | 'connected' | 'console-output' | 'rendezvous' | @@ -114,8 +115,8 @@ export class TelnetAdapter { 'suspend' | 'unhandled-console-output', /* eslint-enable @typescript-eslint/indent */ - data? - ) { + data?); + private emit(eventName: string, data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues setTimeout(() => { //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists @@ -264,8 +265,8 @@ export class TelnetAdapter { }); //listen for any compile errors - this.compileErrorProcessor.on('compile-errors', (errors) => { - this.emit('compile-errors', errors); + this.compileErrorProcessor.on('diagnostics', (errors) => { + this.emit('diagnostics', errors); }); //listen for any console output that was not handled by other methods in the adapter diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index e7fd0ae6..b2149060 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -4,7 +4,7 @@ import * as fsExtra from 'fs-extra'; import * as path from 'path'; import * as sinonActual from 'sinon'; import type { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { Breakpoint, DebugSession } from 'vscode-debugadapter'; +import { DebugSession } from 'vscode-debugadapter'; import { BrightScriptDebugSession } from './BrightScriptDebugSession'; import { fileUtils } from '../FileUtils'; import type { EvaluateContainer, StackFrame, TelnetAdapter } from '../adapters/TelnetAdapter'; @@ -13,15 +13,14 @@ import { defer } from '../util'; import { HighLevelType } from '../interfaces'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { SinonStub } from 'sinon'; -import { standardizePath as s } from 'brighterscript'; +import { util as bscUtil, standardizePath as s } from 'brighterscript'; import { DefaultFiles } from 'roku-deploy'; -import type { DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter'; import type { AddProjectParams, ComponentLibraryConstructorParams } from '../managers/ProjectManager'; import { ComponentLibraryProject, Project } from '../managers/ProjectManager'; const sinon = sinonActual.createSandbox(); const tempDir = s`${__dirname}/../../.tmp`; -const rootDir = s`${tempDir}/rootDir}`; +const rootDir = s`${tempDir}/rootDir`; const outDir = s`${tempDir}/outDir`; const stagingDir = s`${outDir}/stagingDir`; const complib1Dir = s`${tempDir}/complib1`; @@ -465,6 +464,34 @@ describe('BrightScriptDebugSession', () => { }); }); + describe('handleDiagnostics', () => { + it('finds source location for file-only path', async () => { + session['rokuAdapter'] = { destroy: () => { } } as any; + session.projectManager.mainProject = new Project({ + rootDir: rootDir, + outDir: stagingDir + } as Partial as any); + session.projectManager['mainProject'].fileMappings = []; + + fsExtra.outputFileSync(`${stagingDir}/.roku-deploy-staging/components/SomeComponent.xml`, ''); + fsExtra.outputFileSync(`${rootDir}/components/SomeComponent.xml`, ''); + + const stub = sinon.stub(session, 'sendEvent').callsFake(() => { }); + await session['handleDiagnostics']([{ + message: 'Crash', + path: 'SomeComponent.xml', + range: bscUtil.createRange(1, 2, 3, 4) + }]); + expect(stub.getCall(0).args[0]?.body).to.eql({ + diagnostics: [{ + message: 'Crash', + path: s`${stagingDir}/.roku-deploy-staging/components/SomeComponent.xml`, + range: bscUtil.createRange(1, 2, 1, 4) + }] + }); + }); + }); + describe('evaluateRequest', () => { const frameId = 12; let evalStub: SinonStub; @@ -620,7 +647,5 @@ describe('BrightScriptDebugSession', () => { expect(getVarStub.calledWith('person.name', frameId, true)); }); }); - }); - }); diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index a65e021f..c3b39d39 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -29,12 +29,12 @@ import { ProjectManager, Project, ComponentLibraryProject } from '../managers/Pr import type { EvaluateContainer } from '../adapters/DebugProtocolAdapter'; import { DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter'; import { TelnetAdapter } from '../adapters/TelnetAdapter'; -import type { BrightScriptDebugCompileError } from '../CompileErrorProcessor'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import { LaunchStartEvent, LogOutputEvent, RendezvousEvent, - CompileFailureEvent, + DiagnosticsEvent, StoppedEventReason, ChanperfEvent, DebugServerLogOutputEvent, @@ -276,28 +276,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { }); // handle any compile errors - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.rokuAdapter.on('compile-errors', async (errors: BrightScriptDebugCompileError[]) => { - // remove redundant errors and adjust the line number: - // - Roku device and sourcemap work with 1-based line numbers, - // - VS expects 0-based lines. - const compileErrors = util.filterGenericErrors(errors); - for (let compileError of compileErrors) { - let sourceLocation = await this.projectManager.getSourceLocation(compileError.path, compileError.lineNumber); - if (sourceLocation) { - compileError.path = sourceLocation.filePath; - compileError.lineNumber = sourceLocation.lineNumber - 1; //0-based - } else { - // TODO: may need to add a custom event if the source location could not be found by the ProjectManager - compileError.path = fileUtils.removeLeadingSlash(util.removeFileScheme(compileError.path)); - compileError.lineNumber = (compileError.lineNumber || 1) - 1; //0-based - } - } - - this.sendEvent(new CompileFailureEvent(compileErrors)); - //stop the roku adapter and exit the channel - void this.rokuAdapter.destroy(); - void this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + this.rokuAdapter.on('diagnostics', (diagnostics: BSDebugDiagnostic[]) => { + void this.handleDiagnostics(diagnostics); }); // close disconnect if required when the app is exited @@ -388,6 +368,29 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } + /** + * Anytime a roku adapter emits diagnostics, this methid is called to handle it. + */ + private async handleDiagnostics(diagnostics: BSDebugDiagnostic[]) { + // Roku device and sourcemap work with 1-based line numbers, VSCode expects 0-based lines. + for (let diagnostic of diagnostics) { + let sourceLocation = await this.projectManager.getSourceLocation(diagnostic.path, diagnostic.range.start.line + 1); + if (sourceLocation) { + diagnostic.path = sourceLocation.filePath; + diagnostic.range.start.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based + diagnostic.range.end.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based + } else { + // TODO: may need to add a custom event if the source location could not be found by the ProjectManager + diagnostic.path = fileUtils.removeLeadingSlash(util.removeFileScheme(diagnostic.path)); + } + } + + this.sendEvent(new DiagnosticsEvent(diagnostics)); + //stop the roku adapter and exit the channel + void this.rokuAdapter.destroy(); + void this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + } + private async connectAndPublish() { let connectPromise: Promise; //connect to the roku debug via sockets diff --git a/src/debugSession/Events.spec.ts b/src/debugSession/Events.spec.ts index 2aea1610..8eddc1e6 100644 --- a/src/debugSession/Events.spec.ts +++ b/src/debugSession/Events.spec.ts @@ -1,10 +1,10 @@ import { expect } from 'chai'; -import { isCompileFailureEvent, CompileFailureEvent, isLogOutputEvent, LogOutputEvent, isDebugServerLogOutputEvent, DebugServerLogOutputEvent, isRendezvousEvent, RendezvousEvent, isChanperfEvent, ChanperfEvent, isLaunchStartEvent, LaunchStartEvent, isPopupMessageEvent, PopupMessageEvent, isChannelPublishedEvent, ChannelPublishedEvent } from './Events'; +import { isDiagnosticsEvent, DiagnosticsEvent, isLogOutputEvent, LogOutputEvent, isDebugServerLogOutputEvent, DebugServerLogOutputEvent, isRendezvousEvent, RendezvousEvent, isChanperfEvent, ChanperfEvent, isLaunchStartEvent, LaunchStartEvent, isPopupMessageEvent, PopupMessageEvent, isChannelPublishedEvent, ChannelPublishedEvent } from './Events'; describe('Events', () => { it('is* methods work properly', () => { //match - expect(isCompileFailureEvent(new CompileFailureEvent(null))).to.be.true; + expect(isDiagnosticsEvent(new DiagnosticsEvent(null))).to.be.true; expect(isLogOutputEvent(new LogOutputEvent(null))).to.be.true; expect(isDebugServerLogOutputEvent(new DebugServerLogOutputEvent(null))).to.be.true; expect(isRendezvousEvent(new RendezvousEvent(null))).to.be.true; @@ -14,7 +14,7 @@ describe('Events', () => { expect(isChannelPublishedEvent(new ChannelPublishedEvent(null))).to.be.true; //not match - expect(isCompileFailureEvent(null)).to.be.false; + expect(isDiagnosticsEvent(null)).to.be.false; expect(isLogOutputEvent(null)).to.be.false; expect(isDebugServerLogOutputEvent(null)).to.be.false; expect(isRendezvousEvent(null)).to.be.false; diff --git a/src/debugSession/Events.ts b/src/debugSession/Events.ts index d8e711fd..5d713f2a 100644 --- a/src/debugSession/Events.ts +++ b/src/debugSession/Events.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-useless-constructor */ import type { DebugProtocol } from 'vscode-debugprotocol'; -import type { BrightScriptDebugCompileError } from '../CompileErrorProcessor'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { ChanperfData } from '../ChanperfTracker'; import type { RendezvousHistory } from '../RendezvousTracker'; @@ -29,17 +29,17 @@ export class CustomEvent implements DebugProtocol.Event { * Emitted when compile errors were encountered during the current debug session, * usually during the initial sideload process as the Roku is compiling the app. */ -export class CompileFailureEvent extends CustomEvent<{ compileErrors: BrightScriptDebugCompileError[] }> { - constructor(compileErrors: BrightScriptDebugCompileError[]) { - super({ compileErrors }); +export class DiagnosticsEvent extends CustomEvent<{ diagnostics: BSDebugDiagnostic[] }> { + constructor(diagnostics: BSDebugDiagnostic[]) { + super({ diagnostics }); } } /** - * Is the object a `CompileFailureEvent` + * Is the object a `DiagnosticsEvent` */ -export function isCompileFailureEvent(event: any): event is CompileFailureEvent { - return !!event && event.event === CompileFailureEvent.name; +export function isDiagnosticsEvent(event: any): event is DiagnosticsEvent { + return !!event && event.event === DiagnosticsEvent.name; } /** diff --git a/src/index.ts b/src/index.ts index 553afdd1..a1a6abef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ - -//export everything we need export * from './managers/BreakpointManager'; export * from './LaunchConfiguration'; export * from './debugProtocol/Debugger'; diff --git a/src/util.spec.ts b/src/util.spec.ts index e4a1a380..79c2c398 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -4,12 +4,12 @@ import * as fsExtra from 'fs-extra'; import * as getPort from 'get-port'; import * as net from 'net'; import * as path from 'path'; -import * as sinonActual from 'sinon'; -import type { BrightScriptDebugCompileError } from './CompileErrorProcessor'; -import { GENERAL_XML_ERROR } from './CompileErrorProcessor'; +import type { BSDebugDiagnostic } from './CompileErrorProcessor'; import * as dedent from 'dedent'; import { util } from './util'; -let sinon = sinonActual.createSandbox(); +import { util as bscUtil } from 'brighterscript'; +import { createSandbox } from 'sinon'; +const sinon = createSandbox(); beforeEach(() => { sinon.restore(); @@ -215,50 +215,6 @@ describe('Util', () => { }); }); - describe('filterGenericErrors', () => { - it('should remove generic errors IF a more specific exists', () => { - const err1: BrightScriptDebugCompileError = { - path: 'file1.xml', - lineNumber: 0, - charStart: 0, - charEnd: 0, - message: 'Some other error', - errorText: 'err1' - }; - const err2: BrightScriptDebugCompileError = { - path: 'file1.xml', - lineNumber: 0, - charStart: 0, - charEnd: 0, - message: GENERAL_XML_ERROR, - errorText: 'err2' - }; - const err3: BrightScriptDebugCompileError = { - path: 'file2.xml', - lineNumber: 0, - charStart: 0, - charEnd: 0, - message: GENERAL_XML_ERROR, - errorText: 'err3' - }; - const err4: BrightScriptDebugCompileError = { - path: 'file3.xml', - lineNumber: 0, - charStart: 0, - charEnd: 0, - message: 'Some other error', - errorText: 'err4' - }; - const expected = [ - err1, - err3, - err4 - ]; - const actual = util.filterGenericErrors([err1, err2, err3, err4]); - expect(actual).to.deep.equal(expected); - }); - }); - describe('getVariablePath', () => { it('detects valid patterns', () => { expect(util.getVariablePath('a')).to.eql(['a']); diff --git a/src/util.ts b/src/util.ts index 7d7ff278..26976a8e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,8 +7,6 @@ import type { BrightScriptDebugSession } from './debugSession/BrightScriptDebugS import { LogOutputEvent } from './debugSession/Events'; import type { AssignmentStatement, Position, Range } from 'brighterscript'; import { DiagnosticSeverity, isDottedGetExpression, isIndexedGetExpression, isLiteralExpression, isVariableExpression, Parser } from 'brighterscript'; -import type { BrightScriptDebugCompileError } from './CompileErrorProcessor'; -import { GENERAL_XML_ERROR } from './CompileErrorProcessor'; import { serializeError } from 'serialize-error'; import * as dns from 'dns'; import type { AdapterOptions } from './interfaces'; @@ -253,23 +251,6 @@ class Util { } } - public filterGenericErrors(errors: BrightScriptDebugCompileError[]) { - const specificErrors: Record = {}; - - //ignore generic errors when a specific error exists - return errors.filter(e => { - const path = e.path.toLowerCase(); - if (e.message === GENERAL_XML_ERROR) { - if (specificErrors[path]) { - return false; - } - } else { - specificErrors[path] = e; - } - return true; - }); - } - /** * Removes the trailing `Brightscript Debugger>` prompt if present. If not present, returns original value * @param value From bb0180dcc76e3367553afdc81d85bdb8239c516f Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 17 Oct 2022 07:19:11 -0400 Subject: [PATCH 046/197] Fix crash in rendezvous parser for missing files (#108) --- src/RendezvousTracker.spec.ts | 12 ++++++++++++ src/RendezvousTracker.ts | 20 +++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/RendezvousTracker.spec.ts b/src/RendezvousTracker.spec.ts index b0efb228..4f8dc6eb 100644 --- a/src/RendezvousTracker.spec.ts +++ b/src/RendezvousTracker.spec.ts @@ -286,6 +286,18 @@ describe('BrightScriptFileUtils ', () => { await rendezvousTracker.processLog(text) ).to.eql(text); }); + + it('does not crash for files not found by the source locator', async () => { + //return undefined for all sources requested + rendezvousTracker.registerSourceLocator(() => { + return undefined; + }); + expect( + (await rendezvousTracker.processLog(`10-16 01:42:27.126 [sg.node.BLOCK] Rendezvous[2442] at roku_ads_lib:/libsource/Roku_Ads_SG_Wrappers.brs(1262)\r\n` + )).trim() + ).to.eql(''); + //the test passes if it doesn't explode on the file path + }); }); describe('clearHistory', () => { diff --git a/src/RendezvousTracker.ts b/src/RendezvousTracker.ts index 6633f9b2..9e7fd200 100644 --- a/src/RendezvousTracker.ts +++ b/src/RendezvousTracker.ts @@ -90,7 +90,7 @@ export class RendezvousTracker { // detected the completion of a rendezvous event dataChanged = true; let blockInfo = this.rendezvousBlocks[id]; - let clientLineNumber: string = this.clientPathsMap[blockInfo.fileName].clientLines[blockInfo.lineNumber].toString(); + let clientLineNumber: string = this.clientPathsMap[blockInfo.fileName]?.clientLines[blockInfo.lineNumber].toString() ?? blockInfo.lineNumber; if (this.rendezvousHistory.occurrences[blockInfo.fileName]) { // file is in history @@ -189,13 +189,15 @@ export class RendezvousTracker { } } let sourceLocation = await this.getSourceLocation(fileName, lineNumber); - this.clientPathsMap[fileName] = { - clientPath: sourceLocation.filePath, - clientLines: { - //TODO - should the line be 1 or 0 based? - [lineNumber]: sourceLocation.lineNumber - } - }; + if (sourceLocation) { + this.clientPathsMap[fileName] = { + clientPath: sourceLocation.filePath, + clientLines: { + //TODO - should the line be 1 or 0 based? + [lineNumber]: sourceLocation.lineNumber + } + }; + } } else if (!this.clientPathsMap[fileName].clientLines[lineNumber]) { // Add new client line to clint path map this.clientPathsMap[fileName].clientLines[lineNumber] = (await this.getSourceLocation(fileName, lineNumber)).lineNumber; @@ -226,7 +228,7 @@ export class RendezvousTracker { private createLineObject(fileName: string, lineNumber: number, duration?: string): RendezvousLineInfo { return { clientLineNumber: lineNumber, - clientPath: this.clientPathsMap[fileName].clientPath, + clientPath: this.clientPathsMap[fileName]?.clientPath ?? fileName, hitCount: 1, totalTime: this.getTime(duration), type: 'lineInfo' From 1c983cb03347f8896c89a7a0e17731d94ace84e4 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 17 Oct 2022 07:23:05 -0400 Subject: [PATCH 047/197] Update changelog for v0.16.0 --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 36 ++++++++++++++++++------------------ package.json | 4 ++-- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d92497a5..6c02b697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.16.0](https://github.com/rokucommunity/roku-debug/compare/v0.15.0...0.16.0) - 2022-10-17 +### Changed + - Emit device diagnostics instead of compile errors ([#104](https://github.com/rokucommunity/roku-debug/pull/104)) + - Standardize custom events, add is* helpers ([#103](https://github.com/rokucommunity/roku-debug/pull/103)) + - upgrade to [brighterscript@0.60.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0600---2022-10-10). Notable changes since 0.56.0: + - upgrade to [roku-deploy@3.9.2](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#392---2022-10-03). Notable changes since 3.7.1: + - Replace minimatch with picomatch ([roku-deploy#101](https://github.com/rokucommunity/roku-deploy/pull/101)) + - Sync retainStagingFolder, stagingFolderPath with options. ([roku-deploy#100](https://github.com/rokucommunity/roku-deploy/pull/100)) + - Add stagingDir and retainStagingDir. ([roku-deploy#99](https://github.com/rokucommunity/roku-deploy/pull/99)) + - Remotedebug connect early ([roku-deploy#97](https://github.com/rokucommunity/roku-deploy/pull/97)) + - Better compile error handling ([roku-deploy#96](https://github.com/rokucommunity/roku-deploy/pull/96)) +### Fixed + - crash in rendezvous parser for missing files ([#108](https://github.com/rokucommunity/roku-debug/pull/108)) + - better debug protocol launch handling ([#102](https://github.com/rokucommunity/roku-debug/pull/102)) + + + ## [0.15.0](https://github.com/rokucommunity/roku-debug/compare/v0.14.2...0.15.0) - 2022-08-23 ### Added - support for conditional breakpoints over the debug protocol([#97](https://github.com/rokucommunity/roku-debug/pull/97)) diff --git a/package-lock.json b/package-lock.json index 29388b20..4f673444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.56.0", + "brighterscript": "^0.60.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -19,7 +19,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.8.0", + "roku-deploy": "^3.9.2", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.56.0.tgz", - "integrity": "sha512-SG8Oj3pDjtsbq1eZRnAI96T3PVSIDORPh8QfBtXVMPRG2ntJd2OfySpHsaItt5K9oveZi1OjPPguRGL7xaU0MQ==", + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.60.0.tgz", + "integrity": "sha512-LdwiUODQBzB5qd9TwAUFMyTNXzLU7pubYEUg4ZBH13/lKByqCQKPjYnC2NHQI4YHimgoXW9YqkDmWsZI9di2NQ==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -1197,7 +1197,7 @@ "p-settle": "^2.1.0", "parse-ms": "^2.1.0", "require-relative": "^0.8.7", - "roku-deploy": "^3.7.1", + "roku-deploy": "^3.9.2", "serialize-error": "^7.0.1", "source-map": "^0.7.3", "vscode-languageserver": "7.0.0", @@ -3973,9 +3973,9 @@ } }, "node_modules/roku-deploy": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.8.0.tgz", - "integrity": "sha512-pcrNUBklhN5t1JNdYPWCpOTcbU1H869DAj2N3SlM+udL7g+cQF73ZHSd6kNOEBg7xF6nvOmrSSgaoVbguxtuUg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.9.2.tgz", + "integrity": "sha512-2LZyR4EhaFrka1gVmcuJO/f42tqz4clGImboVLCNem1q/PcFV5cnXNZRfqDI+MZ/n8eJto71JlZkcc7TDLt/EQ==", "dependencies": { "chalk": "^2.4.2", "dateformat": "^3.0.3", @@ -3985,9 +3985,9 @@ "is-glob": "^4.0.3", "jsonc-parser": "^2.3.0", "jszip": "^3.6.0", - "minimatch": "^3.0.4", "moment": "^2.29.1", "parse-ms": "^2.1.0", + "picomatch": "^2.2.1", "request": "^2.88.0", "temp-dir": "^2.0.0", "xml2js": "^0.4.23" @@ -5559,9 +5559,9 @@ } }, "brighterscript": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.56.0.tgz", - "integrity": "sha512-SG8Oj3pDjtsbq1eZRnAI96T3PVSIDORPh8QfBtXVMPRG2ntJd2OfySpHsaItt5K9oveZi1OjPPguRGL7xaU0MQ==", + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.60.0.tgz", + "integrity": "sha512-LdwiUODQBzB5qd9TwAUFMyTNXzLU7pubYEUg4ZBH13/lKByqCQKPjYnC2NHQI4YHimgoXW9YqkDmWsZI9di2NQ==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5584,7 +5584,7 @@ "p-settle": "^2.1.0", "parse-ms": "^2.1.0", "require-relative": "^0.8.7", - "roku-deploy": "^3.7.1", + "roku-deploy": "^3.9.2", "serialize-error": "^7.0.1", "source-map": "^0.7.3", "vscode-languageserver": "7.0.0", @@ -7326,9 +7326,9 @@ } }, "roku-deploy": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.8.0.tgz", - "integrity": "sha512-pcrNUBklhN5t1JNdYPWCpOTcbU1H869DAj2N3SlM+udL7g+cQF73ZHSd6kNOEBg7xF6nvOmrSSgaoVbguxtuUg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.9.2.tgz", + "integrity": "sha512-2LZyR4EhaFrka1gVmcuJO/f42tqz4clGImboVLCNem1q/PcFV5cnXNZRfqDI+MZ/n8eJto71JlZkcc7TDLt/EQ==", "requires": { "chalk": "^2.4.2", "dateformat": "^3.0.3", @@ -7338,9 +7338,9 @@ "is-glob": "^4.0.3", "jsonc-parser": "^2.3.0", "jszip": "^3.6.0", - "minimatch": "^3.0.4", "moment": "^2.29.1", "parse-ms": "^2.1.0", + "picomatch": "^2.2.1", "request": "^2.88.0", "temp-dir": "^2.0.0", "xml2js": "^0.4.23" diff --git a/package.json b/package.json index 88de68f6..cca7da2a 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.56.0", + "brighterscript": "^0.60.0", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -94,7 +94,7 @@ "natural-orderby": "^2.0.3", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.8.0", + "roku-deploy": "^3.9.2", "semver": "^7.3.5", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", From ee4293e05f643d9012f6d501992ce85c675195c6 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 17 Oct 2022 07:23:38 -0400 Subject: [PATCH 048/197] 0.16.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f673444..8b51f313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.15.0", + "version": "0.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index cca7da2a..b139091e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.15.0", + "version": "0.16.0", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From 71a6454dca4c72e2bba149ba362d8b32554ab603 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 28 Oct 2022 13:15:35 -0400 Subject: [PATCH 049/197] Update changelog for v0.16.1 --- CHANGELOG.md | 9 +++++++++ package-lock.json | 14 +++++++------- package.json | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c02b697..cc2c2ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.16.1](https://github.com/rokucommunity/roku-debug/compare/v0.16.0...0.16.1) - 2022-10-28 +### Changed + - upgrade to [brighterscript@0.60.4](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0604---2022-10-28). Notable changes since 0.60.0: + - Allow `continue` as local var ([brighterscript#730](https://github.com/rokucommunity/brighterscript/pull/730)) + - better parse recover for unknown func params ([brighterscript#722](https://github.com/rokucommunity/brighterscript/pull/722)) + - Fix if statement block var bug ([brighterscript#698](https://github.com/rokucommunity/brighterscript/pull/698)) + + + ## [0.16.0](https://github.com/rokucommunity/roku-debug/compare/v0.15.0...0.16.0) - 2022-10-17 ### Changed - Emit device diagnostics instead of compile errors ([#104](https://github.com/rokucommunity/roku-debug/pull/104)) diff --git a/package-lock.json b/package-lock.json index 8b51f313..576ee137 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.60.0", + "brighterscript": "^0.60.4", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", @@ -1172,9 +1172,9 @@ } }, "node_modules/brighterscript": { - "version": "0.60.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.60.0.tgz", - "integrity": "sha512-LdwiUODQBzB5qd9TwAUFMyTNXzLU7pubYEUg4ZBH13/lKByqCQKPjYnC2NHQI4YHimgoXW9YqkDmWsZI9di2NQ==", + "version": "0.60.4", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.60.4.tgz", + "integrity": "sha512-lnzWwlm/19D+/TlO8mIeGCp7b/fGv2ZHkTQ5cqxPWTZjBdSU9gl5DlM0+OV9RTQQ6Jntv1IBKLHcLNZTOcgbMw==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", @@ -5559,9 +5559,9 @@ } }, "brighterscript": { - "version": "0.60.0", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.60.0.tgz", - "integrity": "sha512-LdwiUODQBzB5qd9TwAUFMyTNXzLU7pubYEUg4ZBH13/lKByqCQKPjYnC2NHQI4YHimgoXW9YqkDmWsZI9di2NQ==", + "version": "0.60.4", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.60.4.tgz", + "integrity": "sha512-lnzWwlm/19D+/TlO8mIeGCp7b/fGv2ZHkTQ5cqxPWTZjBdSU9gl5DlM0+OV9RTQQ6Jntv1IBKLHcLNZTOcgbMw==", "requires": { "@rokucommunity/bslib": "^0.1.1", "@xml-tools/parser": "^1.0.7", diff --git a/package.json b/package.json index b139091e..bf11479f 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.60.0", + "brighterscript": "^0.60.4", "dateformat": "^4.6.3", "eol": "^0.9.1", "eventemitter3": "^4.0.7", From 9f70a081d230d5dde38a70144b5699d8b7245ea5 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 28 Oct 2022 13:16:11 -0400 Subject: [PATCH 050/197] 0.16.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 576ee137..e739395f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.16.0", + "version": "0.16.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.16.0", + "version": "0.16.1", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index bf11479f..dcbd79c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.16.0", + "version": "0.16.1", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From 75eeacdf91beb3fdb1d24b7b24da2ef30ba9ba9d Mon Sep 17 00:00:00 2001 From: christopher Dwyer-Perkins Date: Wed, 2 Nov 2022 11:27:41 -0300 Subject: [PATCH 051/197] Added the brightscript_warnings command (#110) * Added the brightscript_warnings command * Fixed linting error --- src/SceneGraphDebugCommandController.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/SceneGraphDebugCommandController.ts b/src/SceneGraphDebugCommandController.ts index b599c217..c3c08547 100644 --- a/src/SceneGraphDebugCommandController.ts +++ b/src/SceneGraphDebugCommandController.ts @@ -225,6 +225,15 @@ export class SceneGraphDebugCommandController { } + /** + * Changes the number of brightscript warnings displayed on application install. + * @param warningLimit maximum number of warnings to show + */ + public async brightscriptWarnings(warningLimit: number): Promise { + return this.exec(`brightscript_warnings ${warningLimit ?? 100}`); + } + + /** * Send any custom command to the SceneGraph debug server. * From c10539168c04cb6ddb2d05abc7209fbd1d613a70 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 2 Nov 2022 10:41:21 -0400 Subject: [PATCH 052/197] Update changelog for v0.17.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2c2ced..9e6539d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.17.0](https://github.com/rokucommunity/roku-debug/compare/v0.16.1...0.17.0) - 2022-11-02 +### Changed + - Added the `brightscript_warnings` command ([#110](https://github.com/rokucommunity/roku-debug/pull/110)) + + + ## [0.16.1](https://github.com/rokucommunity/roku-debug/compare/v0.16.0...0.16.1) - 2022-10-28 ### Changed - upgrade to [brighterscript@0.60.4](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0604---2022-10-28). Notable changes since 0.60.0: From 89331a76b7b16837fb561bb861a0f334235c661d Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 2 Nov 2022 10:41:54 -0400 Subject: [PATCH 053/197] 0.17.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e739395f..fd5c909a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-debug", - "version": "0.16.1", + "version": "0.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { "@rokucommunity/logger": "^0.3.0", diff --git a/package.json b/package.json index dcbd79c3..9012e2a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.16.1", + "version": "0.17.0", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { From 5fadd87c5c318b1ef09f1b0a73ef6d4f686c550f Mon Sep 17 00:00:00 2001 From: Phil Anderson Date: Mon, 21 Nov 2022 14:48:04 +0000 Subject: [PATCH 054/197] Bugfix/do not alter out file path for libraries (#112) * do not alter file path if it is library * test to check file path not altered for libraries * refactor to smaller size plus only check url start * add .toLowerCase() * use regex to match a) pkg b) relative paths * amend test to have urls with spaces * Lint fixes Co-authored-by: Bronley Plumb --- src/managers/ProjectManager.spec.ts | 48 +++++++++++++++++++++++++++++ src/managers/ProjectManager.ts | 11 +++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 4a24e407..a5ee7d09 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -667,6 +667,54 @@ describe('ComponentLibraryProject', () => { }); }); + describe('addPostFixToPath', () => { + it('adds postfix if path is 1) pkg:/ or 2) relative - no spaces in url', async () => { + let project = new ComponentLibraryProject(params); + project.fileMappings = []; + fsExtra.outputFileSync(`${params.stagingFolderPath}/source/main.brs`, ''); + fsExtra.outputFileSync(`${params.stagingFolderPath}/components/Component1.xml`, ` + +