| LEFT | RIGHT | 
|    1 /* |    1 /* | 
|    2  * This file is part of Adblock Plus <https://adblockplus.org/>, |    2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 
|    3  * Copyright (C) 2006-present eyeo GmbH |    3  * Copyright (C) 2006-present eyeo GmbH | 
|    4  * |    4  * | 
|    5  * Adblock Plus is free software: you can redistribute it and/or modify |    5  * Adblock Plus is free software: you can redistribute it and/or modify | 
|    6  * it under the terms of the GNU General Public License version 3 as |    6  * it under the terms of the GNU General Public License version 3 as | 
|    7  * published by the Free Software Foundation. |    7  * published by the Free Software Foundation. | 
|    8  * |    8  * | 
|    9  * Adblock Plus is distributed in the hope that it will be useful, |    9  * Adblock Plus is distributed in the hope that it will be useful, | 
|   10  * but WITHOUT ANY WARRANTY; without even the implied warranty of |   10  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|   11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |   11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|   12  * GNU General Public License for more details. |   12  * GNU General Public License for more details. | 
|   13  * |   13  * | 
|   14  * You should have received a copy of the GNU General Public License |   14  * You should have received a copy of the GNU General Public License | 
|   15  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. |   15  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
|   16  */ |   16  */ | 
|   17  |   17  | 
|   18 /* eslint-env node */ |  | 
|   19 /* eslint no-console: "off" */ |  | 
|   20  |  | 
|   21 "use strict"; |   18 "use strict"; | 
|   22  |   19  | 
|   23 const childProcess = require("child_process"); |   20 const {Builder} = require("selenium-webdriver"); | 
|   24 const fs = require("fs"); |   21 const chrome = require("selenium-webdriver/chrome"); | 
|   25 const os = require("os"); |   22 require("chromedriver"); | 
|   26 const path = require("path"); |  | 
|   27  |   23  | 
|   28 const remoteInterface = require("chrome-remote-interface"); |   24 const {executeScript} = require("./webdriver"); | 
|   29  |  | 
|   30 const {ensureChromium} = require("./chromium_download"); |   25 const {ensureChromium} = require("./chromium_download"); | 
|   31  |   26  | 
|   32 // Chromium 60.0.3082.x |   27 // The Chromium version is a build number, quite obscure. | 
|   33 const CHROMIUM_REVISION = 467222; |   28 // Chromium 63.0.3239.x is 508578 | 
 |   29 // Chromium 65.0.3325.0 is 530368 | 
 |   30 // We currently want Chromiun 63, as we still support it and that's the | 
 |   31 // loweset version that supports WebDriver. | 
 |   32 const CHROMIUM_REVISION = 508578; | 
|   34  |   33  | 
|   35 function rmdir(dirPath) |   34 function runScript(chromiumPath, script, scriptName, scriptArgs) | 
|   36 { |   35 { | 
|   37   for (let file of fs.readdirSync(dirPath)) |   36   const options = new chrome.Options() | 
|   38   { |   37         .headless() | 
|   39     let filePath = path.join(dirPath, file); |   38         .setChromeBinaryPath(chromiumPath); | 
|   40     try |  | 
|   41     { |  | 
|   42       if (fs.statSync(filePath).isDirectory()) |  | 
|   43         rmdir(filePath); |  | 
|   44       else |  | 
|   45         fs.unlinkSync(filePath); |  | 
|   46     } |  | 
|   47     catch (error) |  | 
|   48     { |  | 
|   49       console.error(error); |  | 
|   50     } |  | 
|   51   } |  | 
|   52  |   39  | 
|   53   try |   40   const driver = new Builder() | 
|   54   { |   41         .forBrowser("chrome") | 
|   55     fs.rmdirSync(dirPath); |   42         .setChromeOptions(options) | 
|   56   } |   43         .build(); | 
|   57   catch (error) |  | 
|   58   { |  | 
|   59     console.error(error); |  | 
|   60   } |  | 
|   61 } |  | 
|   62  |   44  | 
|   63 function startChromium(chromiumPath) |   45   return executeScript(driver, "Chromium (WebDriver)", | 
|   64 { |   46                        script, scriptName, scriptArgs); | 
|   65   fs.chmodSync(chromiumPath, fs.constants.S_IRWXU); |  | 
|   66  |  | 
|   67   let dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "chromium-data")); |  | 
|   68   let child = null; |  | 
|   69   return { |  | 
|   70     kill: () => child && child.kill(), |  | 
|   71     done: new Promise((resolve, reject) => |  | 
|   72     { |  | 
|   73       child = childProcess.execFile(chromiumPath, [ |  | 
|   74         "--headless", "--single-process", "--disable-gpu", "--no-sandbox", |  | 
|   75         "--allow-file-access-from-files", "--remote-debugging-port=9222", |  | 
|   76         "--user-data-dir=" + dataDir |  | 
|   77       ], error => |  | 
|   78       { |  | 
|   79         rmdir(dataDir); |  | 
|   80         if (error) |  | 
|   81           reject(error); |  | 
|   82         else |  | 
|   83           resolve(); |  | 
|   84       }); |  | 
|   85     }) |  | 
|   86   }; |  | 
|   87 } |  | 
|   88  |  | 
|   89 function throwException(details, url) |  | 
|   90 { |  | 
|   91   let text = details.exception ? details.exception.description : details.text; |  | 
|   92   if (!details.stackTrace) |  | 
|   93   { |  | 
|   94     // ExceptionDetails uses zero-based line and column numbers. |  | 
|   95     text += `\n    at ${details.url || url}:` + |  | 
|   96             (details.lineNumber + 1) + ":" + |  | 
|   97             (details.columnNumber + 1); |  | 
|   98   } |  | 
|   99   throw text; |  | 
|  100 } |  | 
|  101  |  | 
|  102 function reportMessage(text, level) |  | 
|  103 { |  | 
|  104   let method = { |  | 
|  105     log: "log", |  | 
|  106     warning: "warn", |  | 
|  107     error: "error", |  | 
|  108     debug: "log", |  | 
|  109     info: "info" |  | 
|  110   }[level] || "log"; |  | 
|  111   console[method](text); |  | 
|  112 } |  | 
|  113  |  | 
|  114 function connectRemoteInterface(attempt) |  | 
|  115 { |  | 
|  116   return remoteInterface().catch(error => |  | 
|  117   { |  | 
|  118     attempt = attempt || 1; |  | 
|  119     if (attempt > 50) |  | 
|  120     { |  | 
|  121       // Stop trying to connect after 10 seconds |  | 
|  122       throw error; |  | 
|  123     } |  | 
|  124  |  | 
|  125     return new Promise((resolve, reject) => |  | 
|  126     { |  | 
|  127       setTimeout(() => |  | 
|  128       { |  | 
|  129         connectRemoteInterface(attempt + 1).then(resolve).catch(reject); |  | 
|  130       }, 200); |  | 
|  131     }); |  | 
|  132   }); |  | 
|  133 } |  | 
|  134  |  | 
|  135 function runScript(script, scriptName, scriptArgs) |  | 
|  136 { |  | 
|  137   return connectRemoteInterface().then(async client => |  | 
|  138   { |  | 
|  139     try |  | 
|  140     { |  | 
|  141       let {Runtime, Log, Console} = client; |  | 
|  142  |  | 
|  143       console.log("\nTests in Chromium (Remote Interface)\n"); |  | 
|  144  |  | 
|  145       await Log.enable(); |  | 
|  146       Log.entryAdded(({entry}) => |  | 
|  147       { |  | 
|  148         reportMessage(entry.text, entry.level); |  | 
|  149       }); |  | 
|  150  |  | 
|  151       await Console.enable(); |  | 
|  152       Console.messageAdded(({message}) => |  | 
|  153       { |  | 
|  154         reportMessage(message.text, message.level); |  | 
|  155       }); |  | 
|  156  |  | 
|  157       await Runtime.enable(); |  | 
|  158       let compileResult = await Runtime.compileScript({ |  | 
|  159         expression: script, |  | 
|  160         sourceURL: scriptName, |  | 
|  161         persistScript: true |  | 
|  162       }); |  | 
|  163       if (compileResult.exceptionDetails) |  | 
|  164         throwException(compileResult.exceptionDetails, scriptName); |  | 
|  165  |  | 
|  166       let runResult = await Runtime.runScript({ |  | 
|  167         scriptId: compileResult.scriptId |  | 
|  168       }); |  | 
|  169       if (runResult.exceptionDetails) |  | 
|  170         throwException(runResult.exceptionDetails, scriptName); |  | 
|  171  |  | 
|  172       let callResult = await Runtime.callFunctionOn({ |  | 
|  173         objectId: runResult.result.objectId, |  | 
|  174         functionDeclaration: "function(...args) { return this(...args); }", |  | 
|  175         arguments: scriptArgs.map(arg => ({value: arg})) |  | 
|  176       }); |  | 
|  177       if (callResult.exceptionDetails) |  | 
|  178         throwException(callResult.exceptionDetails, scriptName); |  | 
|  179  |  | 
|  180       let promiseResult = await Runtime.awaitPromise({ |  | 
|  181         promiseObjectId: callResult.result.objectId |  | 
|  182       }); |  | 
|  183       if (promiseResult.exceptionDetails) |  | 
|  184         throwException(promiseResult.exceptionDetails, scriptName); |  | 
|  185     } |  | 
|  186     finally |  | 
|  187     { |  | 
|  188       client.close(); |  | 
|  189     } |  | 
|  190   }); |  | 
|  191 } |   47 } | 
|  192  |   48  | 
|  193 module.exports = function(script, scriptName, ...scriptArgs) |   49 module.exports = function(script, scriptName, ...scriptArgs) | 
|  194 { |   50 { | 
|  195   return ensureChromium(CHROMIUM_REVISION).then(chromiumPath => |   51   return ensureChromium(CHROMIUM_REVISION).then(chromiumPath => | 
|  196   { |   52   { | 
|  197     let child = startChromium(chromiumPath); |   53     return runScript(chromiumPath, script, scriptName, scriptArgs) | 
|  198     return Promise.race([ |   54       .then(result => result) | 
|  199       child.done, |   55       .catch(error => | 
|  200       runScript(script, scriptName, scriptArgs) |   56       { | 
|  201     ]).then(result => |   57         throw error; | 
|  202     { |   58       }); | 
|  203       child.kill(); |  | 
|  204       return result; |  | 
|  205     }).catch(error => |  | 
|  206     { |  | 
|  207       child.kill(); |  | 
|  208       throw error; |  | 
|  209     }); |  | 
|  210   }); |   59   }); | 
|  211 }; |   60 }; | 
| LEFT | RIGHT |