selenium-standalone
Version:
installs a `selenium-standalone` command line to install and start a standalone selenium server
275 lines (241 loc) • 8.42 kB
JavaScript
const tarStream = require('tar-stream');
const os = require('os');
const { default: mkdirp } = require('mkdirp');
const path = require('path');
const yauzl = require('yauzl');
const fs = require('fs');
const zlib = require('zlib');
const { default: got } = require('got');
const debug = require('debug')('selenium-standalone:install');
const { logError } = require('./log-error');
const md5 = require('md5');
const { getProxtAgent } = require('./proxyManager');
const installers = ['selenium', 'chrome', 'ie', 'firefox', 'edge', 'chromiumedge'];
/**
* @param {import('yauzl').Entry} entry
* @returns {boolean}
*/
const isExecutable = (entry) => {
// Windows .exe files are executables
if (path.extname(entry.fileName) === '.exe') {
return true;
}
// Extract the Unix file permissions.
// See https://github.com/thejoshwolfe/yauzl/issues/102 and https://unix.stackexchange.com/a/14727.
const permissions = entry.externalFileAttributes >>> 16;
// Directories are not executable
if (permissions & fs.constants.S_IFDIR) {
return false;
}
// Return true when the user, group, or other executable bits are set.
return (
!!(permissions & fs.constants.S_IXUSR) ||
!!(permissions & fs.constants.S_IXGRP) ||
!!(permissions & fs.constants.S_IXOTH)
);
};
const basePath = (fullPath) => {
return path.dirname(fullPath);
};
const logInstallSummary = (logger, paths, urls) => {
installers.forEach((name) => {
if (!paths[name]) {
return;
}
logger('---');
logger(name + ' install:');
logger('from: ' + urls[name]);
logger('to: ' + paths[name].installPath);
});
};
function asyncLogEnd(logger) {
logger('');
logger('-----');
logger('selenium-standalone installation finished');
logger('-----');
}
async function createDirs(paths) {
const installDirectories = Object.keys(paths).map((name) => {
return paths[name].installPath;
});
for (const d of installDirectories.map(basePath)) {
await mkdirp(d);
}
}
const chmod = (where) =>
new Promise((resolve, reject) => {
debug('chmod 0755 on', where);
fs.chmod(where, '0755', (err) => {
if (err) {
return reject(logError('chmod', err));
}
resolve();
});
});
async function setDriverFilePermissions(where) {
debug('setDriverFilePermissions', where);
const requireChmod = await new Promise((resolve) =>
fs.access(where, fs.constants.R_OK | fs.constants.X_OK, (err) => {
if (err) {
debug('error in fs.access', where, err);
}
resolve(!!err);
})
);
if (requireChmod) {
await chmod(where);
}
}
async function isUpToDate(url, file, pathToFile) {
if (!file) {
return false;
}
try {
const response = await got.head(url, {
...getProxtAgent(url),
timeout: 2500,
});
if (response.headers['content-length'] === `${fs.statSync(pathToFile).size}`) {
return true;
}
return response.headers.etag.includes(md5(file).toString());
} catch (err) {
logError(`Remote file size/hash in ${url} don't match with local file ${pathToFile}`);
return false;
}
}
function getTempFileName(suffix) {
return os.tmpdir() + path.sep + os.uptime() + suffix;
}
async function uncompressDownloadedFile(zipFilePath) {
debug('unzip ' + zipFilePath);
return new Promise((resolve, reject) =>
yauzl.open(zipFilePath, function onOpenZipFile(err, zipFile) {
if (err) {
return reject(logError('uncompressDownloadedFile:yauzl.open', err));
}
zipFile.on('entry', (entry) => {
if (fs.existsSync(entry.fileName) && fs.lstatSync(entry.fileName).isDirectory()) {
return; // ignore folders, i.e. release notes folder in edge driver zip
}
if (!isExecutable(entry)) {
return;
}
zipFile.openReadStream(entry, { autoClose: true }, function onOpenZipFileEntryReadStream(errRead, readStream) {
if (errRead) {
return reject(logError('uncompressDownloadedFile:zipFile.openReadStream', err));
}
// The .zip file usually contain an executable with the same name as the driver.
// For example, we expect to extract "msedgedriver.exe" as "msedgedriver.exe".
//
// There are three known exceptions:
//
// 1. The .zip files from Chrome for Testing have files in a directory.
// For example, we want to extract "chromedriver-mac-arm64/chromedriver" as "chromedriver" from "chromedriver-mac-arm64.zip".
//
// 2. msedgedriver v86 and v87 for mac64 include a second executable file, libc++.dylib, It is also extracted.
//
// 3. The exectuables for geckodriver <= v0.7.1 are called "wires.exe" or "wires-<version>-win.exe".
// These releases are so old we can ignore them.
//
const extractPath = path.join(path.dirname(zipFilePath), path.basename(entry.fileName));
const extractWriteStream = fs
.createWriteStream(extractPath)
.once('error', (errWs) => reject(logError('uncompressDownloadedFile:readStream.pipe', errWs)));
readStream
.pipe(extractWriteStream)
.once('error', (errPipe) => reject(logError('uncompressDownloadedFile:readStream.pipe', errPipe)));
});
});
zipFile.on('close', resolve);
})
);
}
async function uncompressGzippedFile(from, gzipFilePath) {
return new Promise((resolve, reject) => {
const gunzip = zlib.createGunzip();
const extractPath = path.join(path.dirname(gzipFilePath), path.basename(gzipFilePath, '.gz'));
const writeStream = fs
.createWriteStream(extractPath)
.once('error', (err) => reject(logError('uncompressGzippedFile:createWriteStream', err)));
const gunzippedContent = fs.createReadStream(gzipFilePath).pipe(gunzip).once('error', reject);
if (from.substr(-7) === '.tar.gz') {
const extractor = tarStream.extract();
let fileAlreadyUnarchived = false;
let cbCalled = false;
extractor
.on('entry', (_header, stream, callback) => {
if (fileAlreadyUnarchived) {
if (!cbCalled) {
cbCalled = true;
return reject(new Error('Tar archive contains more than one file'));
}
fileAlreadyUnarchived = true;
}
stream.pipe(writeStream);
stream.on('end', () => {
callback();
});
stream.resume();
})
.on('finish', () => {
if (!cbCalled) {
cbCalled = true;
resolve();
}
});
gunzippedContent.pipe(extractor);
} else {
gunzippedContent.pipe(writeStream).on('finish', resolve);
}
});
}
async function runInstaller(installerFile, _from, to) {
const logFile = getTempFileName('installer.log');
const options = [
'/passive', // no user interaction, only show progress bar
'/l*',
logFile, // save install log to this file
'/i',
installerFile, // msi file to install
];
const spawn = require('cross-spawn');
const runner = spawn('msiexec', options, { stdio: 'inherit' });
return new Promise((resolve, reject) => {
runner.on('exit', () => {
fs.readFile(logFile, 'utf16le', (err, data) => {
if (err) {
return reject(logError('runInstaller:readFile', err));
}
const installDir = data
.split(os.EOL)
.map((line) => {
const match = line.match(/INSTALLDIR = (.+)$/);
return match && match[1];
})
.filter((line) => line != null)[0];
if (!installDir) {
return reject(new Error('Could not find installed driver'));
}
fs.createReadStream(installDir + 'MicrosoftWebDriver.exe', {
autoClose: true,
})
.pipe(fs.createWriteStream(to, { autoClose: true }))
.once('finish', resolve)
.once('error', (errWs) => reject(logError('runInstaller:createWriteStream', errWs)));
});
});
runner.on('error', (errRunner) => reject(logError('runInstaller:runner', errRunner)));
});
}
module.exports = {
asyncLogEnd,
createDirs,
setDriverFilePermissions,
logInstallSummary,
isUpToDate,
getTempFileName,
uncompressDownloadedFile,
uncompressGzippedFile,
runInstaller,
};