From cbb55cb202697b9596dfcf5b9d4c33510f87e1f9 Mon Sep 17 00:00:00 2001 From: qingzhou Date: Wed, 6 Sep 2017 13:07:45 -0700 Subject: [PATCH] Chore: Unblock spinner Fix #485 --- src/lib/connectors/jsdom/evaluate-runner.ts | 47 +++++++++++++++++++++ src/lib/connectors/jsdom/jsdom.ts | 45 ++++++++++++++++---- src/lib/rules/axe/axe.ts | 2 +- 3 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 src/lib/connectors/jsdom/evaluate-runner.ts diff --git a/src/lib/connectors/jsdom/evaluate-runner.ts b/src/lib/connectors/jsdom/evaluate-runner.ts new file mode 100644 index 00000000000..765d577e2e2 --- /dev/null +++ b/src/lib/connectors/jsdom/evaluate-runner.ts @@ -0,0 +1,47 @@ +import * as vm from 'vm'; + +import * as jsdom from 'jsdom/lib/old-api'; +import * as jsdomutils from 'jsdom/lib/jsdom/living/generated/utils'; + +const run = (data) => { + const { source } = data; + const result = { + error: null, + evaluate: 'result' + }; + const url = process.argv[2]; + const waitFor = process.argv[3]; + + jsdom.env({ + done: (error, window) => { + if (error) { + result.error = error; + + return process.send(result); + } + + /* Even though `done()` is called after window.onload (so all resoruces and scripts executed), + we might want to wait a few seconds if the site is lazy loading something. */ + return setTimeout(async () => { + try { + const script: vm.Script = new vm.Script(source); + const evaluteResult = await script.runInContext(jsdomutils.implForWrapper(window.document)._global); + + result.evaluate = evaluteResult; + } catch (err) { + result.error = err; + } + + process.send(result); + }, waitFor); + }, + features: { + FetchExternalResources: ['script', 'link', 'img'], + ProcessExternalResources: ['script'], + SkipExternalResources: false + }, + url + }); +}; + +process.on('message', run); diff --git a/src/lib/connectors/jsdom/jsdom.ts b/src/lib/connectors/jsdom/jsdom.ts index aac93d0827c..952b50f5f8e 100644 --- a/src/lib/connectors/jsdom/jsdom.ts +++ b/src/lib/connectors/jsdom/jsdom.ts @@ -26,10 +26,9 @@ import * as path from 'path'; import * as url from 'url'; import * as util from 'util'; -import * as vm from 'vm'; +import { fork, ChildProcess } from 'child_process'; import * as jsdom from 'jsdom/lib/old-api'; -import * as jsdomutils from 'jsdom/lib/jsdom/living/generated/utils'; import { debug as d } from '../../utils/debug'; import { resolveUrl } from '../utils/resolver'; @@ -464,15 +463,43 @@ class JSDOMConnector implements IConnector { return this._fetchUrl(parsedTarget, customHeaders); } - public evaluate(source: string): Promise { - const script: vm.Script = new vm.Script(source); - const result = script.runInContext(jsdomutils.implForWrapper(this._window.document)._global, { timeout: 60000 }); - - if (result[Symbol.toStringTag] === 'Promise') { - return result; + private killProcess = (runner: ChildProcess) => { + try { + runner.kill('SIGKILL'); + } catch (err) { + debug('Error closing evaluate process'); } + }; + + public evaluate(source: string, waitBeforeTimeOut: number = 60000): Promise { + return new Promise((resolve, reject) => { + const runner: ChildProcess = fork(path.join(__dirname, 'evaluate-runner'), [this._finalHref, this._options.waitFor], { execArgv: [] }); + let timeoutId; + + runner.on('message', (result) => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + + this.killProcess(runner); - return Promise.resolve(result); + if (result.error) { + return reject(result.error); + } + + return resolve(result.evaluate); + }); + + runner.send({ source }); + + timeoutId = setTimeout(() => { + debug(`Evaluation times out. Killing process and reporting and error.`); + this.killProcess(runner); + + return reject(new Error('TIMEOUT')); + }, waitBeforeTimeOut); + }); } public querySelectorAll(selector: string): Promise> { diff --git a/src/lib/rules/axe/axe.ts b/src/lib/rules/axe/axe.ts index d322c8bfff6..055da3f2ac7 100644 --- a/src/lib/rules/axe/axe.ts +++ b/src/lib/rules/axe/axe.ts @@ -11,7 +11,7 @@ import { AxeResults, Result as AxeResult, NodeResult as AxeNodeResult } from 'ax import { readFileAsync } from '../../utils/misc'; import { debug as d } from '../../utils/debug'; -import { IAsyncHTMLElement, ITraverseEnd, IRule, IRuleBuilder, Severity } from '../../types'; // eslint-disable-line no-unused-vars +import { IAsyncHTMLElement, IRule, IRuleBuilder, Severity, ITraverseEnd } from '../../types'; // eslint-disable-line no-unused-vars import { RuleContext } from '../../rule-context'; // eslint-disable-line no-unused-vars const debug = d(__filename);