install.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. 'use strict';
  2. const extractZip = require('extract-zip');
  3. const fs = require('fs');
  4. const helper = require('./lib/chromedriver');
  5. const request = require('request');
  6. const mkdirp = require('mkdirp');
  7. const path = require('path');
  8. const del = require('del');
  9. const child_process = require('child_process');
  10. const os = require('os');
  11. const skipDownload = process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD;
  12. if (skipDownload) {
  13. console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.');
  14. process.exit(0);
  15. }
  16. const libPath = path.join(__dirname, 'lib', 'chromedriver');
  17. let cdnUrl = process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://chromedriver.storage.googleapis.com';
  18. const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH;
  19. // adapt http://chromedriver.storage.googleapis.com/
  20. cdnUrl = cdnUrl.replace(/\/+$/, '');
  21. let platform = process.platform;
  22. let chromedriver_version = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version;
  23. if (platform === 'linux') {
  24. if (process.arch === 'arm64' || process.arch === 'x64') {
  25. platform += '64';
  26. } else {
  27. console.log('Only Linux 64 bits supported.');
  28. process.exit(1);
  29. }
  30. } else if (platform === 'darwin' || platform === 'freebsd') {
  31. if (process.arch === 'x64') {
  32. // @ts-ignore
  33. platform = 'mac64';
  34. } else {
  35. console.log('Only Mac 64 bits supported.');
  36. process.exit(1);
  37. }
  38. } else if (platform !== 'win32') {
  39. console.log('Unexpected platform or architecture:', process.platform, process.arch);
  40. process.exit(1);
  41. }
  42. let tmpPath;
  43. const chromedriverBinaryFileName = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver';
  44. let chromedriverBinaryFilePath;
  45. let downloadedFile = '';
  46. Promise.resolve().then(function () {
  47. if (chromedriver_version === 'LATEST')
  48. return getLatestVersion(getRequestOptions(cdnUrl + '/LATEST_RELEASE'));
  49. })
  50. .then(() => {
  51. tmpPath = findSuitableTempDirectory();
  52. chromedriverBinaryFilePath = path.resolve(tmpPath, chromedriverBinaryFileName );
  53. })
  54. .then(verifyIfChromedriverIsAvailableAndHasCorrectVersion)
  55. .then(chromedriverIsAvailable => {
  56. if (chromedriverIsAvailable) return;
  57. console.log('Current existing ChromeDriver binary is unavailable, proceding with download and extraction.');
  58. return downloadFile().then(extractDownload);
  59. })
  60. .then(() => copyIntoPlace(tmpPath, libPath))
  61. .then(fixFilePermissions)
  62. .then(() => console.log('Done. ChromeDriver binary available at', helper.path))
  63. .catch(function (err) {
  64. console.error('ChromeDriver installation failed', err);
  65. process.exit(1);
  66. });
  67. function downloadFile() {
  68. if (configuredfilePath) {
  69. downloadedFile = configuredfilePath;
  70. console.log('Using file: ', downloadedFile);
  71. return Promise.resolve();
  72. } else {
  73. const fileName = `chromedriver_${platform}.zip`;
  74. const tempDownloadedFile = path.resolve(tmpPath, fileName);
  75. downloadedFile = tempDownloadedFile;
  76. const formattedDownloadUrl = `${cdnUrl}/${chromedriver_version}/${fileName}`;
  77. console.log('Downloading from file: ', formattedDownloadUrl);
  78. console.log('Saving to file:', downloadedFile);
  79. return requestBinary(getRequestOptions(formattedDownloadUrl), downloadedFile);
  80. }
  81. }
  82. function verifyIfChromedriverIsAvailableAndHasCorrectVersion() {
  83. if (!fs.existsSync(chromedriverBinaryFilePath))
  84. return false;
  85. const forceDownload = process.env.npm_config_chromedriver_force_download === 'true' || process.env.CHROMEDRIVER_FORCE_DOWNLOAD === 'true';
  86. if (forceDownload)
  87. return false;
  88. console.log('ChromeDriver binary exists. Validating...');
  89. const deferred = new Deferred();
  90. try {
  91. fs.accessSync(chromedriverBinaryFilePath, fs.constants.X_OK);
  92. const cp = child_process.spawn(chromedriverBinaryFilePath, ['--version']);
  93. let str = '';
  94. cp.stdout.on('data', function (data) {
  95. str += data;
  96. });
  97. cp.on('error', function () {
  98. deferred.resolve(false);
  99. });
  100. cp.on('close', function (code) {
  101. if (code !== 0)
  102. return deferred.resolve(false);
  103. const parts = str.split(' ');
  104. if (parts.length < 3)
  105. return deferred.resolve(false);
  106. if (parts[1].startsWith(chromedriver_version)) {
  107. console.log(str);
  108. console.log(`ChromeDriver is already available at '${chromedriverBinaryFilePath}'.`);
  109. return deferred.resolve(true);
  110. }
  111. deferred.resolve(false);
  112. });
  113. }
  114. catch (error) {
  115. deferred.resolve(false);
  116. }
  117. return deferred.promise;
  118. }
  119. function findSuitableTempDirectory() {
  120. const now = Date.now();
  121. const candidateTmpDirs = [
  122. process.env.TMPDIR || process.env.TMP || process.env.npm_config_tmp,
  123. os.tmpdir(),
  124. '/tmp',
  125. path.join(process.cwd(), 'tmp')
  126. ];
  127. for (let i = 0; i < candidateTmpDirs.length; i++) {
  128. if (!candidateTmpDirs[i]) continue;
  129. // Prevent collision with other versions in the dependency tree
  130. const namespace = chromedriver_version;
  131. const candidatePath = path.join(candidateTmpDirs[i], namespace, 'chromedriver');
  132. try {
  133. mkdirp.sync(candidatePath, '0777');
  134. const testFile = path.join(candidatePath, now + '.tmp');
  135. fs.writeFileSync(testFile, 'test');
  136. fs.unlinkSync(testFile);
  137. return candidatePath;
  138. } catch (e) {
  139. console.log(candidatePath, 'is not writable:', e.message);
  140. }
  141. }
  142. console.error('Can not find a writable tmp directory, please report issue on https://github.com/giggio/chromedriver/issues/ with as much information as possible.');
  143. process.exit(1);
  144. }
  145. function getRequestOptions(downloadPath) {
  146. const options = {uri: downloadPath, method: 'GET'};
  147. const protocol = options.uri.substring(0, options.uri.indexOf('//'));
  148. const proxyUrl = protocol === 'https:'
  149. ? process.env.npm_config_https_proxy
  150. : (process.env.npm_config_proxy || process.env.npm_config_http_proxy);
  151. if (proxyUrl) {
  152. options.proxy = proxyUrl;
  153. }
  154. options.strictSSL = !!process.env.npm_config_strict_ssl;
  155. // Use certificate authority settings from npm
  156. let ca = process.env.npm_config_ca;
  157. // Parse ca string like npm does
  158. if (ca && ca.match(/^".*"$/)) {
  159. try {
  160. ca = JSON.parse(ca.trim());
  161. } catch (e) {
  162. console.error('Could not parse ca string', process.env.npm_config_ca, e);
  163. }
  164. }
  165. if (!ca && process.env.npm_config_cafile) {
  166. try {
  167. ca = fs.readFileSync(process.env.npm_config_cafile, {encoding: 'utf8'})
  168. .split(/\n(?=-----BEGIN CERTIFICATE-----)/g);
  169. // Comments at the beginning of the file result in the first
  170. // item not containing a certificate - in this case the
  171. // download will fail
  172. if (ca.length > 0 && !/-----BEGIN CERTIFICATE-----/.test(ca[0])) {
  173. ca.shift();
  174. }
  175. } catch (e) {
  176. console.error('Could not read cafile', process.env.npm_config_cafile, e);
  177. }
  178. }
  179. if (ca) {
  180. console.log('Using npmconf ca');
  181. options.agentOptions = {
  182. ca: ca
  183. };
  184. options.ca = ca;
  185. }
  186. // Use specific User-Agent
  187. if (process.env.npm_config_user_agent) {
  188. options.headers = {'User-Agent': process.env.npm_config_user_agent};
  189. }
  190. return options;
  191. }
  192. function getLatestVersion(requestOptions) {
  193. const deferred = new Deferred();
  194. request(requestOptions, function (err, response, data) {
  195. if (err) {
  196. deferred.reject('Error with http(s) request: ' + err);
  197. } else {
  198. chromedriver_version = data.trim();
  199. deferred.resolve(true);
  200. }
  201. });
  202. return deferred.promise;
  203. }
  204. function requestBinary(requestOptions, filePath) {
  205. const deferred = new Deferred();
  206. let count = 0;
  207. let notifiedCount = 0;
  208. const outFile = fs.openSync(filePath, 'w');
  209. const client = request(requestOptions);
  210. client.on('error', function (err) {
  211. deferred.reject('Error with http(s) request: ' + err);
  212. });
  213. client.on('data', function (data) {
  214. fs.writeSync(outFile, data, 0, data.length, null);
  215. count += data.length;
  216. if ((count - notifiedCount) > 800000) {
  217. console.log('Received ' + Math.floor(count / 1024) + 'K...');
  218. notifiedCount = count;
  219. }
  220. });
  221. client.on('end', function () {
  222. console.log('Received ' + Math.floor(count / 1024) + 'K total.');
  223. fs.closeSync(outFile);
  224. deferred.resolve(true);
  225. });
  226. return deferred.promise;
  227. }
  228. function extractDownload() {
  229. if (path.extname(downloadedFile) !== '.zip') {
  230. fs.copyFileSync(downloadedFile, path.resolve(tmpPath, 'chromedriver'));
  231. console.log('Skipping zip extraction - binary file found.');
  232. return Promise.resolve();
  233. }
  234. const deferred = new Deferred();
  235. console.log('Extracting zip contents');
  236. extractZip(path.resolve(downloadedFile), { dir: tmpPath }, function (err) {
  237. if (err) {
  238. deferred.reject('Error extracting archive: ' + err);
  239. } else {
  240. deferred.resolve(true);
  241. }
  242. });
  243. return deferred.promise;
  244. }
  245. function copyIntoPlace(originPath, targetPath) {
  246. return del(targetPath)
  247. .then(function() {
  248. console.log("Copying to target path", targetPath);
  249. fs.mkdirSync(targetPath);
  250. // Look for the extracted directory, so we can rename it.
  251. const files = fs.readdirSync(originPath);
  252. const promises = files.map(function(name) {
  253. const deferred = new Deferred();
  254. const file = path.join(originPath, name);
  255. const reader = fs.createReadStream(file);
  256. const targetFile = path.join(targetPath, name);
  257. const writer = fs.createWriteStream(targetFile);
  258. writer.on("close", function() {
  259. deferred.resolve(true);
  260. });
  261. reader.pipe(writer);
  262. return deferred.promise;
  263. });
  264. return Promise.all(promises);
  265. });
  266. }
  267. function fixFilePermissions() {
  268. // Check that the binary is user-executable and fix it if it isn't (problems with unzip library)
  269. if (process.platform != 'win32') {
  270. const stat = fs.statSync(helper.path);
  271. // 64 == 0100 (no octal literal in strict mode)
  272. if (!(stat.mode & 64)) {
  273. console.log('Fixing file permissions');
  274. fs.chmodSync(helper.path, '755');
  275. }
  276. }
  277. }
  278. function Deferred() {
  279. this.resolve = null;
  280. this.reject = null;
  281. this.promise = new Promise(function (resolve, reject) {
  282. this.resolve = resolve;
  283. this.reject = reject;
  284. }.bind(this));
  285. Object.freeze(this);
  286. }