From 9dc14b05eef1f94ffcc6c9e0a6285f4ba36e3314 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 30 Aug 2022 12:31:47 -0400 Subject: [PATCH 1/4] Better compile error handling --- src/Errors.ts | 4 +++- src/RokuDeploy.ts | 55 +++++++++++++++++++++++++++++++++-------------- src/index.ts | 1 + 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/Errors.ts b/src/Errors.ts index 24ce943..e87ba2b 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -1,3 +1,5 @@ +import type { RokuMessages } from './RokuDeploy'; + export class InvalidDeviceResponseCodeError extends Error { constructor(message: string, public results?: any) { super(message); @@ -34,7 +36,7 @@ export class UnknownDeviceResponseError extends Error { } export class CompileError extends Error { - constructor(message: string, public results: any) { + constructor(message: string, public results: any, public rokuMessages: RokuMessages) { super(message); Object.setPrototypeOf(this, CompileError.prototype); } diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 6dc215b..54b5e8b 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -443,15 +443,23 @@ export class RokuDeploy { let response: HttpResponse; try { response = await this.doPostRequest(requestOptions); - } catch (replaceError) { - //"replace" failed, do an "install" instead - requestOptions.formData.mysubmit = 'Install'; - response = await this.doPostRequest(requestOptions); + } catch (replaceError: any) { + //fail if this is a compile error + if (this.isCompileError(replaceError.message)) { + if (options.failOnCompileError) { + throw new errors.CompileError('Compile error', replaceError, replaceError?.results); + } else { + throw new errors.UnknownDeviceResponseError(replaceError.message, response); + } + } else { + requestOptions.formData.mysubmit = 'Install'; + response = await this.doPostRequest(requestOptions); + } } if (options.failOnCompileError) { - if (response.body.indexOf('Install Failure: Compilation Failed.') > -1) { - throw new errors.CompileError('Compile error', response); + if (this.isCompileError(response.body)) { + throw new errors.CompileError('Compile error', response, this.getRokuMessagesFromResponseBody(response.body)); } } @@ -473,6 +481,13 @@ export class RokuDeploy { } } + /** + * Does the response look like a compile error + */ + private isCompileError(responseHtml: string) { + return responseHtml.includes('Install Failure: Compilation Failed.'); + } + /** * Converts existing loaded package to squashfs for faster loading packages * @param options @@ -635,7 +650,7 @@ export class RokuDeploy { throw new errors.UnparsableDeviceResponseError('Invalid response', results); } - this.logger.debug(results.body); + // this.logger.trace(results.body); if (results.response.statusCode === 401) { throw new errors.UnauthorizedDeviceResponseError('Unauthorized. Please verify username and password for target Roku.', results); @@ -652,27 +667,29 @@ export class RokuDeploy { } } - private getRokuMessagesFromResponseBody(body: string): { errors: Array; infos: Array; successes: Array } { - let errorMessages = []; - let infos = []; - let successes = []; + private getRokuMessagesFromResponseBody(body: string): RokuMessages { + const result = { + errors: [] as string[], + infos: [] as string[], + successes: [] as string[] + }; let errorRegex = /Shell\.create\('Roku\.Message'\)\.trigger\('[\w\s]+',\s+'(\w+)'\)\.trigger\('[\w\s]+',\s+'(.*?)'\)/igm; - let match; + let match: RegExpExecArray; // eslint-disable-next-line no-cond-assign while (match = errorRegex.exec(body)) { let [, messageType, message] = match; switch (messageType.toLowerCase()) { case 'error': - errorMessages.push(message); + result.errors.push(message); break; case 'info': - infos.push(message); + result.infos.push(message); break; case 'success': - successes.push(message); + result.successes.push(message); break; default: @@ -680,7 +697,7 @@ export class RokuDeploy { } } - return { errors: errorMessages, infos: infos, successes: successes }; + return result; } /** @@ -1032,6 +1049,12 @@ export interface StandardizedFileEntry { dest: string; } +export interface RokuMessages { + errors: string[]; + infos: string[]; + successes: string[]; +} + export const DefaultFiles = [ 'source/**/*.*', 'components/**/*.*', diff --git a/src/index.ts b/src/index.ts index 131266c..e9b719b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { RokuDeploy } from './RokuDeploy'; export * from './RokuDeploy'; export * from './util'; export * from './RokuDeployOptions'; +export * from './Errors'; //create a new static instance of RokuDeploy, and export those functions for backwards compatibility export const rokuDeploy = new RokuDeploy(); From 984ec3a3c489e44a8f1a6316f7d82285d146c369 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 30 Aug 2022 12:49:44 -0400 Subject: [PATCH 2/4] Fix test coverage --- src/RokuDeploy.spec.ts | 28 ++++++++++++++++++++++++++++ src/RokuDeploy.ts | 12 +++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 8e10df0..6114494 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -730,6 +730,34 @@ describe('index', () => { assert.fail('Should not have succeeded'); }); + it('rejects as CompileError when initial replace fails', () => { + options.failOnCompileError = true; + mockDoPostRequest(` + Install Failure: Compilation Failed. + Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Install Failure: Compilation Failed').trigger('Render', node); + `); + + return rokuDeploy.publish(options).then(() => { + assert.fail('Should not have succeeded due to roku server compilation failure'); + }, (err) => { + expect(err).to.be.instanceOf(errors.CompileError); + }); + }); + + it('rejects as CompileError when initial replace fails', () => { + options.failOnCompileError = true; + mockDoPostRequest(` + Install Failure: Compilation Failed. + Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Install Failure: Compilation Failed').trigger('Render', node); + `); + + return rokuDeploy.publish(options).then(() => { + assert.fail('Should not have succeeded due to roku server compilation failure'); + }, (err) => { + expect(err).to.be.instanceOf(errors.CompileError); + }); + }); + it('rejects when response contains compile error wording', () => { options.failOnCompileError = true; let body = 'Install Failure: Compilation Failed.'; diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 54b5e8b..997b7c4 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -445,12 +445,8 @@ export class RokuDeploy { response = await this.doPostRequest(requestOptions); } catch (replaceError: any) { //fail if this is a compile error - if (this.isCompileError(replaceError.message)) { - if (options.failOnCompileError) { - throw new errors.CompileError('Compile error', replaceError, replaceError?.results); - } else { - throw new errors.UnknownDeviceResponseError(replaceError.message, response); - } + if (this.isCompileError(replaceError.message) && options.failOnCompileError) { + throw new errors.CompileError('Compile error', replaceError, replaceError.results); } else { requestOptions.formData.mysubmit = 'Install'; response = await this.doPostRequest(requestOptions); @@ -485,7 +481,7 @@ export class RokuDeploy { * Does the response look like a compile error */ private isCompileError(responseHtml: string) { - return responseHtml.includes('Install Failure: Compilation Failed.'); + return !!/install\sfailure:\scompilation\sfailed/i.exec(responseHtml); } /** @@ -650,8 +646,6 @@ export class RokuDeploy { throw new errors.UnparsableDeviceResponseError('Invalid response', results); } - // this.logger.trace(results.body); - if (results.response.statusCode === 401) { throw new errors.UnauthorizedDeviceResponseError('Unauthorized. Please verify username and password for target Roku.', results); } From 2d08a74f5e2458a077a8dde458afcf25a1bb4f61 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 30 Aug 2022 13:00:10 -0400 Subject: [PATCH 3/4] Adds the new remotedebug_connect_early form field --- src/RokuDeploy.ts | 6 ++++++ src/RokuDeployOptions.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 997b7c4..2efe81b 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -439,6 +439,12 @@ export class RokuDeploy { requestOptions.formData.remotedebug = '1'; } + //attach the remotedebug_connect_early if present + if (options.remoteDebugConnectEarly) { + // eslint-disable-next-line camelcase + requestOptions.formData.remotedebug_connect_early = '1'; + } + //try to "replace" the channel first since that usually works. let response: HttpResponse; try { diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index 467108a..b02afd1 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -73,6 +73,12 @@ export interface RokuDeployOptions { */ remoteDebug?: boolean; + /** + * When publishing a sideloaded channel, this flag can be used to tell the Roku device that, should any compile errors occur, a client device (such as vscode) + * will be trying to attach to the debug protocol control port to consume those compile errors. This must be used in conjuction with the `remoteDebug` option + */ + remoteDebugConnectEarly?: boolean; + /** * The port used to send remote control commands (like home press, back, etc.). Defaults to 8060. * This is mainly useful for things like emulators that use alternate ports, From 3f39aa95a74b98d2e1641a14aac0e95dd0204783 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 30 Aug 2022 13:04:30 -0400 Subject: [PATCH 4/4] Add tests --- src/RokuDeploy.spec.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 6114494..7b6688b 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -797,10 +797,25 @@ describe('index', () => { it('handles successful deploy with remoteDebug', () => { options.failOnCompileError = true; options.remoteDebug = true; - mockDoPostRequest(); + const stub = mockDoPostRequest(); + + return rokuDeploy.publish(options).then((result) => { + expect(result.message).to.equal('Successful deploy'); + expect(stub.getCall(0).args[0].formData.remotedebug).to.eql('1'); + }, () => { + assert.fail('Should not have rejected the promise'); + }); + }); + + it('handles successful deploy with remotedebug_connect_early', () => { + options.failOnCompileError = true; + options.remoteDebug = true; + options.remoteDebugConnectEarly = true; + const stub = mockDoPostRequest(); return rokuDeploy.publish(options).then((result) => { expect(result.message).to.equal('Successful deploy'); + expect(stub.getCall(0).args[0].formData.remotedebug_connect_early).to.eql('1'); }, () => { assert.fail('Should not have rejected the promise'); }); @@ -3104,7 +3119,7 @@ describe('index', () => { } function mockDoPostRequest(body = '', statusCode = 200) { - sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { + return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { let results = { response: { statusCode: statusCode }, body: body }; rokuDeploy['checkRequest'](results); return Promise.resolve(results);