Skip to content

Commit

Permalink
Fix issues with detecting "check for updates required" (#181)
Browse files Browse the repository at this point in the history
* Fix issues detecting when a device needs to check for updates

* Fix failing test

* Improved error types

* Add unit tests to get to 100% coverage

---------

Co-authored-by: Christian Holbrook <cholbrook@fubo.tv>
  • Loading branch information
TwitchBronBron and Christian-Holbrook authored Dec 4, 2024
1 parent eb544d7 commit ab54c02
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 30 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"publish-coverage": "nyc report --reporter=text-lcov | coveralls"
},
"dependencies": {
"@types/request": "^2.47.0",
"chalk": "^2.4.2",
"dateformat": "^3.0.3",
"dayjs": "^1.11.0",
Expand All @@ -42,7 +43,6 @@
"@types/mocha": "^9.0.0",
"@types/node": "^16.11.3",
"@types/q": "^1.5.8",
"@types/request": "^2.47.0",
"@types/sinon": "^10.0.4",
"@types/xml2js": "^0.4.5",
"@typescript-eslint/eslint-plugin": "5.1.0",
Expand Down
44 changes: 37 additions & 7 deletions src/Errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RokuMessages } from './RokuDeploy';
import type { HttpResponse, RokuMessages } from './RokuDeploy';
import type * as requestType from 'request';

export class InvalidDeviceResponseCodeError extends Error {
constructor(message: string, public results?: any) {
Expand Down Expand Up @@ -56,16 +57,45 @@ export class MissingRequiredOptionError extends Error {
}
}

/**
* This error is thrown when a Roku device refuses to accept connections because it requires the user to check for updates (even if no updates are actually available).
*/
export class UpdateCheckRequiredError extends Error {
results: any;

cause: Error;
static MESSAGE = `Your device needs to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`;

constructor(originalError: Error) {
constructor(
public response: HttpResponse,
public requestOptions: requestType.OptionsWithUrl,
public cause?: Error
) {
super();
this.message = `Your device needs to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`;
this.results = { response: { statusCode: 577 } };
this.cause = originalError;
this.message = UpdateCheckRequiredError.MESSAGE;
Object.setPrototypeOf(this, UpdateCheckRequiredError.prototype);
}
}

export function isUpdateCheckRequiredError(e: any): e is UpdateCheckRequiredError {
return e?.constructor?.name === 'UpdateCheckRequiredError';
}

/**
* This error is thrown when a Roku device ends the connection unexpectedly, causing an 'ECONNRESET' error. Typically this happens when the device needs to check for updates (even if no updates are available), but it can also happen for other reasons.
*/
export class ConnectionResetError extends Error {

static MESSAGE = `The Roku device ended the connection unexpectedly and may need to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`;

constructor(error: Error, requestOptions: requestType.OptionsWithUrl) {
super();
this.message = ConnectionResetError.MESSAGE;
this.cause = error;
Object.setPrototypeOf(this, ConnectionResetError.prototype);
}

public cause?: Error;
}

export function isConnectionResetError(e: any): e is ConnectionResetError {
return e?.constructor?.name === 'ConnectionResetError';
}
72 changes: 64 additions & 8 deletions src/RokuDeploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1103,17 +1103,18 @@ describe('index', () => {
});
});

it('rejects when response contains invalid password status code', () => {
it('rejects when response contains invalid password status code', async () => {
options.failOnCompileError = true;
mockDoPostRequest('', 577);
mockDoPostRequest(`'Failed to check for software update'`, 200);

return rokuDeploy.publish(options).then(() => {
try {
await rokuDeploy.publish(options);
assert.fail('Should not have succeeded due to roku server compilation failure');
}, (err) => {
expect(err.message).to.be.a('string').and.satisfy(msg => msg.startsWith(`Your device needs to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.
https://support.roku.com/article/208755668.`));
});
} catch (err) {
expect((err as any).message).to.eql(
errors.UpdateCheckRequiredError.MESSAGE
);
}
});

it('handles successful deploy', () => {
Expand Down Expand Up @@ -1206,6 +1207,45 @@ https://support.roku.com/article/208755668.`));
}
assert.fail('Should not have succeeded');
});

it('Should throw an excpetion', async () => {
options.failOnCompileError = true;
mockDoPostRequest('', 577);

try {
await rokuDeploy.publish(options);
} catch (e) {
assert.ok('Exception was thrown as expected');
expect(e).to.be.instanceof(errors.UpdateCheckRequiredError);
return;
}
assert.fail('Should not have succeeded');
});

class ErrorWithConnectionResetCode extends Error {
code;

constructor(code = 'ECONNRESET') {
super();
this.code = code;
}
}

it('Should throw an excpetion', async () => {
options.failOnCompileError = true;
sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => {
throw new ErrorWithConnectionResetCode();
});

try {
await rokuDeploy.publish(options);
} catch (e) {
assert.ok('Exception was thrown as expected');
expect(e).to.be.instanceof(errors.ConnectionResetError);
return;
}
assert.fail('Should not have succeeded');
});
});

describe('convertToSquashfs', () => {
Expand Down Expand Up @@ -3642,6 +3682,22 @@ https://support.roku.com/article/208755668.`));
});
});

describe('isUpdateCheckRequiredResponse', () => {
it('matches on actual response from device', () => {
const response = `<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"HandheldFriendly\" content=\"True\">\n <title> Roku Development Kit </title>\n\n <link rel=\"stylesheet\" type=\"text/css\" media=\"screen\" href=\"css/global.css\" />\n</head>\n<body>\n <div id=\"root\" style=\"background: #fff\">\n\n </div>\n\n <script type=\"text/javascript\" src=\"css/global.js\"></script>\n <script type=\"text/javascript\">\n \n // Include core components and resounce bundle (needed)\n Shell.resource.set(null, {\n endpoints: {} \n });\n Shell.create('Roku.Event.Key');\n Shell.create('Roku.Events.Resize');\n Shell.create('Roku.Events.Scroll'); \n // Create global navigation and render it\n var nav = Shell.create('Roku.Nav')\n .trigger('Enable standalone and utility mode - hide user menu, shopping cart, and etc.')\n .trigger('Use compact footer')\n .trigger('Hide footer')\n .trigger('Render', document.getElementById('root'))\n .trigger('Remove all feature links from header')\n\n // Retrieve main content body node\n var node = nav.invoke('Get main body section mounting node');\n \n // Create page container and page header\n var container = Shell.create('Roku.Nav.Page.Standard').trigger('Render', node);\n node = container.invoke('Get main body node');\n container.invoke('Get headline node').innerHTML = 'Failed to check for software update';\n\t // Cannot reach Software Update Server\n node.innerHTML = '<p>Please make sure that your Roku device is connected to internet and running most recent software.</p> <p> After connecting to internet, go to system settings and check for software update.</p> ';\n\n var hrDiv = document.createElement('div');\n hrDiv.innerHTML = '<hr />';\n node.appendChild(hrDiv);\n\n var d = document.createElement('div');\n d.innerHTML = '<br />';\n node.appendChild(d);\n\n </script>\n\n\n <div style=\"display:none\">\n\n <font color=\"red\">Please make sure that your Roku device is connected to internet, and running most recent software version (d=953108)</font>\n\n </div>\n\n</body>\n</html>\n`;
expect(
rokuDeploy['isUpdateCheckRequiredResponse'](response)
).to.be.true;
});

it('matches with some variations to the message', () => {
const response = `" FAILED tocheck\tfor softwareupdate"`;
expect(
rokuDeploy['isUpdateCheckRequiredResponse'](response)
).to.be.true;
});
});

function mockDoGetRequest(body = '', statusCode = 200) {
return sinon.stub(rokuDeploy as any, 'doGetRequest').callsFake((params) => {
let results = { response: { statusCode: statusCode }, body: body };
Expand Down
45 changes: 31 additions & 14 deletions src/RokuDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,23 +462,33 @@ export class RokuDeploy {
//try to "replace" the channel first since that usually works.
let response: HttpResponse;
try {
response = await this.doPostRequest(requestOptions);
} catch (replaceError: any) {
//fail if this is a compile error
if (this.isCompileError(replaceError.message) && options.failOnCompileError) {
throw new errors.CompileError('Compile error', replaceError, replaceError.results);
} else {
try {
try {
response = await this.doPostRequest(requestOptions);
} catch (replaceError: any) {
//fail if this is a compile error
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);
} catch (installError: any) {
switch (installError.results.response.statusCode) {
case 577:
throw new errors.UpdateCheckRequiredError(installError);
default:
throw installError;
}
}
}
} catch (e: any) {
//if this is a 577 error, we have high confidence that the device needs to do an update check
if (e.results?.response?.statusCode === 577) {
throw new errors.UpdateCheckRequiredError(response, requestOptions, e);

//a reset connection could be cause by several things, but most likely it's due to the device needing to check for updates
} else if (e.code === 'ECONNRESET') {
throw new errors.ConnectionResetError(e, requestOptions);
} else {
throw e;
}
}

//if we got a non-error status code, but the body includes a message about needing to update, throw a special error
if (this.isUpdateCheckRequiredResponse(response.body)) {
throw new errors.UpdateCheckRequiredError(response, requestOptions);
}

if (options.failOnCompileError) {
Expand Down Expand Up @@ -512,6 +522,13 @@ export class RokuDeploy {
return !!/install\sfailure:\scompilation\sfailed/i.exec(responseHtml);
}

/**
* Does the response look like a compile error
*/
private isUpdateCheckRequiredResponse(responseHtml: string) {
return !!/["']\s*Failed\s*to\s*check\s*for\s*software\s*update\s*["']/i.exec(responseHtml);
}

/**
* Converts existing loaded package to squashfs for faster loading packages
* @param options
Expand Down

0 comments on commit ab54c02

Please sign in to comment.