| OLD | NEW | 
|---|
| 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 https = require("https"); | 22 require("chromedriver"); | 
| 26 const os = require("os"); |  | 
| 27 const path = require("path"); |  | 
| 28 | 23 | 
| 29 const remoteInterface = require("chrome-remote-interface"); | 24 const {executeScript} = require("./webdriver"); | 
| 30 const extractZip = require("extract-zip"); | 25 const {ensureChromium} = require("./chromium_download"); | 
| 31 | 26 | 
| 32 const CHROMIUM_REVISION = 467222; | 27 // The Chromium version is a build number, quite obscure. | 
|  | 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; | 
| 33 | 33 | 
| 34 function rmdir(dirPath) | 34 function runScript(chromiumPath, script, scriptName, scriptArgs) | 
| 35 { | 35 { | 
| 36   for (let file of fs.readdirSync(dirPath)) | 36   const options = new chrome.Options() | 
| 37   { | 37         .headless() | 
| 38     let filePath = path.join(dirPath, file); | 38         .setChromeBinaryPath(chromiumPath); | 
| 39     try |  | 
| 40     { |  | 
| 41       if (fs.statSync(filePath).isDirectory()) |  | 
| 42         rmdir(filePath); |  | 
| 43       else |  | 
| 44         fs.unlinkSync(filePath); |  | 
| 45     } |  | 
| 46     catch (error) |  | 
| 47     { |  | 
| 48       console.error(error); |  | 
| 49     } |  | 
| 50   } |  | 
| 51 | 39 | 
| 52   try | 40   const driver = new Builder() | 
| 53   { | 41         .forBrowser("chrome") | 
| 54     fs.rmdirSync(dirPath); | 42         .setChromeOptions(options) | 
| 55   } | 43         .build(); | 
| 56   catch (error) |  | 
| 57   { |  | 
| 58     console.error(error); |  | 
| 59   } |  | 
| 60 } |  | 
| 61 | 44 | 
| 62 function getChromiumExecutable(chromiumDir) | 45   return executeScript(driver, "Chromium (WebDriver)", | 
| 63 { | 46                        script, scriptName, scriptArgs); | 
| 64   switch (process.platform) |  | 
| 65   { |  | 
| 66     case "win32": |  | 
| 67       return path.join(chromiumDir, "chrome-win32", "chrome.exe"); |  | 
| 68     case "linux": |  | 
| 69       return path.join(chromiumDir, "chrome-linux", "chrome"); |  | 
| 70     case "darwin": |  | 
| 71       return path.join(chromiumDir, "chrome-mac", "Chromium.app", "Contents", |  | 
| 72                        "MacOS", "Chromium"); |  | 
| 73     default: |  | 
| 74       throw new Error("Unexpected platform"); |  | 
| 75   } |  | 
| 76 } |  | 
| 77 |  | 
| 78 function ensureChromium() |  | 
| 79 { |  | 
| 80   let {platform} = process; |  | 
| 81   if (platform == "win32") |  | 
| 82     platform += "-" + process.arch; |  | 
| 83   let buildTypes = { |  | 
| 84     "win32-ia32": ["Win", "chrome-win32.zip"], |  | 
| 85     "win32-x64": ["Win_x64", "chrome-win32.zip"], |  | 
| 86     "linux": ["Linux_x64", "chrome-linux.zip"], |  | 
| 87     "darwin": ["Mac", "chrome-mac.zip"] |  | 
| 88   }; |  | 
| 89 |  | 
| 90   if (!buildTypes.hasOwnProperty(platform)) |  | 
| 91   { |  | 
| 92     let err = new Error(`Cannot run browser tests, ${platform} is unsupported`); |  | 
| 93     return Promise.reject(err); |  | 
| 94   } |  | 
| 95 |  | 
| 96 |  | 
| 97   return Promise.resolve().then(() => |  | 
| 98   { |  | 
| 99     let snapshotsDir = path.join(__dirname, "chromium-snapshots"); |  | 
| 100     let chromiumDir = path.join(snapshotsDir, |  | 
| 101                                 `chromium-${platform}-${CHROMIUM_REVISION}`); |  | 
| 102     if (fs.existsSync(chromiumDir)) |  | 
| 103       return chromiumDir; |  | 
| 104 |  | 
| 105     if (!fs.existsSync(path.dirname(chromiumDir))) |  | 
| 106       fs.mkdirSync(path.dirname(chromiumDir)); |  | 
| 107 |  | 
| 108     let [dir, fileName] = buildTypes[platform]; |  | 
| 109     let archive = path.join(snapshotsDir, "download-cache", |  | 
| 110                             `${CHROMIUM_REVISION}-${fileName}`); |  | 
| 111 |  | 
| 112     return Promise.resolve() |  | 
| 113       .then(() => |  | 
| 114       { |  | 
| 115         if (!fs.existsSync(archive)) |  | 
| 116         { |  | 
| 117           let url = `https://www.googleapis.com/download/storage/v1/b/chromium-b
     rowser-snapshots/o/${dir}%2F${CHROMIUM_REVISION}%2F${fileName}?alt=media`; |  | 
| 118           console.info("Downloading Chromium..."); |  | 
| 119           return download(url, archive); |  | 
| 120         } |  | 
| 121         console.info(`Reusing cached archive ${archive}`); |  | 
| 122       }) |  | 
| 123       .then(() => unzipArchive(archive, chromiumDir)) |  | 
| 124       .then(() => chromiumDir); |  | 
| 125   }).then(dir => getChromiumExecutable(dir)); |  | 
| 126 } |  | 
| 127 |  | 
| 128 function download(url, destFile) |  | 
| 129 { |  | 
| 130   return new Promise((resolve, reject) => |  | 
| 131   { |  | 
| 132     let cacheDir = path.dirname(destFile); |  | 
| 133     if (!fs.existsSync(cacheDir)) |  | 
| 134       fs.mkdirSync(cacheDir); |  | 
| 135     let tempDest = destFile + "-" + process.pid; |  | 
| 136     let writable = fs.createWriteStream(tempDest); |  | 
| 137 |  | 
| 138     https.get(url, response => |  | 
| 139     { |  | 
| 140       if (response.statusCode != 200) |  | 
| 141       { |  | 
| 142         reject( |  | 
| 143           new Error(`Unexpected server response: ${response.statusCode}`)); |  | 
| 144         response.resume(); |  | 
| 145         return; |  | 
| 146       } |  | 
| 147 |  | 
| 148       response.pipe(writable) |  | 
| 149               .on("error", error => |  | 
| 150               { |  | 
| 151                 writable.close(); |  | 
| 152                 fs.unlinkSync(tempDest); |  | 
| 153                 reject(error); |  | 
| 154               }) |  | 
| 155               .on("close", () => |  | 
| 156               { |  | 
| 157                 writable.close(); |  | 
| 158                 fs.renameSync(tempDest, destFile); |  | 
| 159                 resolve(); |  | 
| 160               }); |  | 
| 161     }).on("error", reject); |  | 
| 162   }); |  | 
| 163 } |  | 
| 164 |  | 
| 165 function unzipArchive(archive, destDir) |  | 
| 166 { |  | 
| 167   return new Promise((resolve, reject) => |  | 
| 168   { |  | 
| 169     extractZip(archive, {dir: destDir}, err => |  | 
| 170     { |  | 
| 171       if (err) |  | 
| 172         reject(err); |  | 
| 173       else |  | 
| 174         resolve(); |  | 
| 175     }); |  | 
| 176   }); |  | 
| 177 } |  | 
| 178 |  | 
| 179 function startChromium(chromiumPath) |  | 
| 180 { |  | 
| 181   fs.chmodSync(chromiumPath, fs.constants.S_IRWXU); |  | 
| 182 |  | 
| 183   let dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "chromium-data")); |  | 
| 184   let child = null; |  | 
| 185   return { |  | 
| 186     kill: () => child && child.kill(), |  | 
| 187     done: new Promise((resolve, reject) => |  | 
| 188     { |  | 
| 189       child = childProcess.execFile(chromiumPath, [ |  | 
| 190         "--headless", "--single-process", "--disable-gpu", "--no-sandbox", |  | 
| 191         "--allow-file-access-from-files", "--remote-debugging-port=9222", |  | 
| 192         "--user-data-dir=" + dataDir |  | 
| 193       ], error => |  | 
| 194       { |  | 
| 195         rmdir(dataDir); |  | 
| 196         if (error) |  | 
| 197           reject(error); |  | 
| 198         else |  | 
| 199           resolve(); |  | 
| 200       }); |  | 
| 201     }) |  | 
| 202   }; |  | 
| 203 } |  | 
| 204 |  | 
| 205 function throwException(details, url) |  | 
| 206 { |  | 
| 207   let text = details.exception ? details.exception.description : details.text; |  | 
| 208   if (!details.stackTrace) |  | 
| 209   { |  | 
| 210     // ExceptionDetails uses zero-based line and column numbers. |  | 
| 211     text += `\n    at ${details.url || url}:` + |  | 
| 212             (details.lineNumber + 1) + ":" + |  | 
| 213             (details.columnNumber + 1); |  | 
| 214   } |  | 
| 215   throw text; |  | 
| 216 } |  | 
| 217 |  | 
| 218 function reportMessage(text, level) |  | 
| 219 { |  | 
| 220   let method = { |  | 
| 221     log: "log", |  | 
| 222     warning: "warn", |  | 
| 223     error: "error", |  | 
| 224     debug: "log", |  | 
| 225     info: "info" |  | 
| 226   }[level] || "log"; |  | 
| 227   console[method](text); |  | 
| 228 } |  | 
| 229 |  | 
| 230 function connectRemoteInterface(attempt) |  | 
| 231 { |  | 
| 232   return remoteInterface().catch(error => |  | 
| 233   { |  | 
| 234     attempt = attempt || 1; |  | 
| 235     if (attempt > 50) |  | 
| 236     { |  | 
| 237       // Stop trying to connect after 10 seconds |  | 
| 238       throw error; |  | 
| 239     } |  | 
| 240 |  | 
| 241     return new Promise((resolve, reject) => |  | 
| 242     { |  | 
| 243       setTimeout(() => |  | 
| 244       { |  | 
| 245         connectRemoteInterface(attempt + 1).then(resolve).catch(reject); |  | 
| 246       }, 200); |  | 
| 247     }); |  | 
| 248   }); |  | 
| 249 } |  | 
| 250 |  | 
| 251 function runScript(script, scriptName, scriptArgs) |  | 
| 252 { |  | 
| 253   return connectRemoteInterface().then(async client => |  | 
| 254   { |  | 
| 255     try |  | 
| 256     { |  | 
| 257       let {Runtime, Log, Console} = client; |  | 
| 258 |  | 
| 259       await Log.enable(); |  | 
| 260       Log.entryAdded(({entry}) => |  | 
| 261       { |  | 
| 262         reportMessage(entry.text, entry.level); |  | 
| 263       }); |  | 
| 264 |  | 
| 265       await Console.enable(); |  | 
| 266       Console.messageAdded(({message}) => |  | 
| 267       { |  | 
| 268         reportMessage(message.text, message.level); |  | 
| 269       }); |  | 
| 270 |  | 
| 271       await Runtime.enable(); |  | 
| 272       let compileResult = await Runtime.compileScript({ |  | 
| 273         expression: script, |  | 
| 274         sourceURL: scriptName, |  | 
| 275         persistScript: true |  | 
| 276       }); |  | 
| 277       if (compileResult.exceptionDetails) |  | 
| 278         throwException(compileResult.exceptionDetails, scriptName); |  | 
| 279 |  | 
| 280       let runResult = await Runtime.runScript({ |  | 
| 281         scriptId: compileResult.scriptId |  | 
| 282       }); |  | 
| 283       if (runResult.exceptionDetails) |  | 
| 284         throwException(runResult.exceptionDetails, scriptName); |  | 
| 285 |  | 
| 286       let callResult = await Runtime.callFunctionOn({ |  | 
| 287         objectId: runResult.result.objectId, |  | 
| 288         functionDeclaration: "function(...args) { return this(...args); }", |  | 
| 289         arguments: scriptArgs.map(arg => ({value: arg})) |  | 
| 290       }); |  | 
| 291       if (callResult.exceptionDetails) |  | 
| 292         throwException(callResult.exceptionDetails, scriptName); |  | 
| 293 |  | 
| 294       let promiseResult = await Runtime.awaitPromise({ |  | 
| 295         promiseObjectId: callResult.result.objectId |  | 
| 296       }); |  | 
| 297       if (promiseResult.exceptionDetails) |  | 
| 298         throwException(promiseResult.exceptionDetails, scriptName); |  | 
| 299     } |  | 
| 300     finally |  | 
| 301     { |  | 
| 302       client.close(); |  | 
| 303     } |  | 
| 304   }); |  | 
| 305 } | 47 } | 
| 306 | 48 | 
| 307 module.exports = function(script, scriptName, ...scriptArgs) | 49 module.exports = function(script, scriptName, ...scriptArgs) | 
| 308 { | 50 { | 
| 309   return ensureChromium().then(chromiumPath => | 51   return ensureChromium(CHROMIUM_REVISION).then(chromiumPath => | 
| 310   { | 52   { | 
| 311     let child = startChromium(chromiumPath); | 53     return runScript(chromiumPath, script, scriptName, scriptArgs) | 
| 312     return Promise.race([ | 54       .then(result => result) | 
| 313       child.done, | 55       .catch(error => | 
| 314       runScript(script, scriptName, scriptArgs) | 56       { | 
| 315     ]).then(result => | 57         throw error; | 
| 316     { | 58       }); | 
| 317       child.kill(); |  | 
| 318       return result; |  | 
| 319     }).catch(error => |  | 
| 320     { |  | 
| 321       child.kill(); |  | 
| 322       throw error; |  | 
| 323     }); |  | 
| 324   }); | 59   }); | 
| 325 }; | 60 }; | 
| OLD | NEW | 
|---|