UNPKG

galenframework

Version:

The command line tool to use the Galen Framework. This includes the wrapper above and webdriver downloads for different browsers

388 lines (346 loc) 12.7 kB
// Copyright 2016 Martin Reinhardt /* * This simply downloads Galen */ 'use strict'; const requestProgress = require('request-progress'); const replace = require('replace-in-file'); const Progress = require('progress'); const AdmZip = require('adm-zip'); const cp = require('child_process'); const fs = require('fs-extra'); const helper = require('./lib/helper'); const kew = require('kew'); const npmconf = require('npmconf'); const path = require('path'); const request = require('request'); const url = require('url'); const which = require('which'); const log = require('npmlog'); const cdnUrl = process.env.npm_config_galen_url || process.env.GALEN_CDNURL || 'https://github.com/galenframework/galen/releases/download/'; const downloadUrl = cdnUrl + '/galen-' + helper.version + '/galen-bin-' + helper.version + '.zip'; const originalPath = process.env.PATH; // If the process exits without going through exit(), then we did not complete. var validExit = false; process.on('exit', function () { if (!validExit) { log.error('Install exited unexpectedly'); exit(1); } }); // NPM adds bin directories to the path, which will cause `which` to find the // bin for this package not the actual galenframework bin. Also help out people who // put ./bin on their path process.env.PATH = helper.cleanPath(originalPath); const libPath = path.join(__dirname, 'lib'); const pkgPath = path.join(libPath, 'galen'); let galenPath = null; let tmpPath = null; // If the user manually installed galen, we want // to use the existing version. // // Do not re-use a manually-installed galen with // a different version. // // Do not re-use an npm-installed galen, because // that can lead to weird circular dependencies between // local versions and global versions. const whichDeferred = kew.defer(); which('galen', whichDeferred.makeNodeResolver()); whichDeferred.promise .then((result) => { galenPath = result; // Horrible hack to avoid problems during global install. We check to see if // the file `which` found is our own bin script. if (galenPath.indexOf(path.join('npm', 'galenframework')) !== -1) { log.info('Looks like an `npm install -g` on windows; unable to check for already installed version.'); throw new Error('Global install'); } const contents = fs.readFileSync(galenPath, 'utf8'); if (/NPM_INSTALL_MARKER/.test(contents)) { log.info('Looks like an `npm install -g`; unable to check for already installed version.'); throw new Error('Global install'); } else { const checkVersionDeferred = kew.defer(); cp.execFile(galenPath, ['--version'], checkVersionDeferred.makeNodeResolver()); return checkVersionDeferred.promise; } }) .then((stdout) => { const regex = /^Version: ([0-9\.]+)$/; const result = stdout.trim().match(regex); const version = result[1]; if (helper.version === version) { writeLocationFile(galenPath); log.info('galenframework is already installed at', galenPath + '.'); exit(0); } else { log.info('galenframework detected, but wrong version', stdout.trim(), '@', galenPath + '.'); downloadAndInstallGalen(); } }) .fail(downloadAndInstallGalen); function downloadAndInstallGalen() { // Trying to use a local file failed, so initiate download and install // steps instead. const npmconfDeferred = kew.defer(); npmconf.load(npmconfDeferred.makeNodeResolver()); return npmconfDeferred.promise.then((conf) => { tmpPath = findSuitableTempDirectory(conf); const fileName = downloadUrl.split('/').pop(); const downloadedFile = path.join(tmpPath, fileName); log.info('Running at platform: ' + process.platform); // Start the install. if (!fs.existsSync(downloadedFile)) { log.info('Downloading', downloadUrl); log.info('Saving to', downloadedFile); return requestBinary(getRequestOptions(conf), downloadedFile); } else { log.info('Download already available at', downloadedFile); return { requestOptions: getRequestOptions(conf), downloadedFile: downloadedFile }; } }) .then((response) => extractDownload(response.downloadedFile, response.requestOptions, false)) .then((extractedPath) => copyIntoPlace(extractedPath, pkgPath)) .then(() => { const location = libPath; writeLocationFile(location); log.info('Done. galen binary available at ', location); // Ensure executable is executable by all users fs.chmodSync(location, '755'); fs.chmodSync(location + '/galen/galen', '755'); fs.chmodSync(location + '/galen/galen.bat', '755'); replace({ files: location + '/galen/galen.bat', replace: 'com.galenframework.GalenMain %*', with: 'com.galenframework.GalenMain %* -Djna.nosys=true' }, (error, changedFiles) => { //Catch errors if (error) { log.error('Error occurred:', error); } //List changed files log.info('Modified files:', changedFiles.join(', ')); exit(0); }); }) .fail(function (err) { log.error('Galen installation failed', err, err.stack); exit(1); }); } /** * Save the destination directory back * @param {string} location - path of the directory */ function writeLocationFile(location) { log.info('Writing location.js file'); if (process.platform === 'win32') { location = location.replace(/\\/g, '\\\\'); } fs.writeFileSync(path.join(libPath, 'location.js'), 'module.exports.location = \'' + location + '\';'); } /** * Exit helper function * @param {int} code - exit code * @function */ function exit(code) { validExit = true; process.env.PATH = originalPath; process.exit(code || 0); } /** * Function to find an writable temp directory * @param {object} npmConf - current NPM configuration * @returns {string} representing suitable temp directory * @function */ function findSuitableTempDirectory(npmConf) { const now = Date.now(); const candidateTmpDirs = [ process.env.TMPDIR || process.env.TEMP || npmConf.get('tmp'), path.join(process.cwd(), 'tmp', '/tmp') ]; for (let i = 0; i < candidateTmpDirs.length; i++) { const candidatePath = path.join(candidateTmpDirs[i], 'galenframework'); try { fs.mkdirsSync(candidatePath, '0777'); // Make double sure we have 0777 permissions; some operating systems // default umask does not allow write by default. fs.chmodSync(candidatePath, '0777'); const testFile = path.join(candidatePath, now + '.tmp'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); return candidatePath; } catch (e) { log.info(candidatePath, 'is not writable:', e.message); } } log.error('Can not find a writable tmp directory.'); exit(1); } /** * Create request opions object * @param {object} conf - current NPM config * @returns {{uri: string, encoding: null, followRedirect: boolean, headers: {}, strictSSL: *}} * @function */ function getRequestOptions(conf) { const strictSSL = conf.get('strict-ssl'); let options = { uri: downloadUrl, encoding: null, // Get response as a buffer followRedirect: true, // The default download path redirects to a CDN URL. headers: {}, strictSSL: strictSSL }; let proxyUrl = conf.get('https-proxy') || conf.get('http-proxy') || conf.get('proxy'); if (proxyUrl) { // Print using proxy let proxy = url.parse(proxyUrl); if (proxy.auth) { // Mask password proxy.auth = proxy.auth.replace(/:.*$/, ':******'); } log.info('Using proxy ' + url.format(proxy)); // Enable proxy options.proxy = proxyUrl; // If going through proxy, use the user-agent string from the npm config options.headers['User-Agent'] = conf.get('user-agent'); } // Use certificate authority settings from npm const ca = conf.get('ca'); if (ca) { log.info('Using npmconf ca'); options.ca = ca; } return options; } /** * Downloads binary file * @param {object} requestOptions - to use for HTTP call * @param {string} filePath - download URL * @returns {*} * @function */ function requestBinary(requestOptions, filePath) { const deferred = kew.defer(); const writePath = filePath + '-download-' + Date.now(); log.info('Receiving...'); let bar = null; requestProgress(request(requestOptions, (error, response, body) => { log.info(''); if (!error && response.statusCode === 200) { fs.writeFileSync(writePath, body); log.info('Received ' + Math.floor(body.length / 1024) + 'K total.'); fs.renameSync(writePath, filePath); deferred.resolve({ requestOptions: requestOptions, downloadedFile: filePath }); } else if (response) { log.error('Error requesting archive.\n' + 'Status: ' + response.statusCode + '\n' + 'Request options: ' + JSON.stringify(requestOptions, null, 2) + '\n' + 'Response headers: ' + JSON.stringify(response.headers, null, 2) + '\n' + 'Make sure your network and proxy settings are correct.\n\n'); exit(1); } else if (error && error.stack && error.stack.indexOf('SELF_SIGNED_CERT_IN_CHAIN') != -1) { log.error('Error making request.'); exit(1); } else if (error) { log.error('Error making request.\n' + error.stack + '\n\n' + 'Please report this full log at https://github.com/hypery2k/galenframework-cli/issues'); exit(1); } else { log.error('Something unexpected happened, please report this full ' + 'log at https://github.com/hypery2k/galenframework-cli/issues'); exit(1); } })).on('progress', (state) => { if (!bar) { bar = new Progress(' [:bar] :percent :etas', { total: state.total, width: 40 }); } bar.curr = state.received; bar.tick(0); }); return deferred.promise; } /** * Extracts the given Archive * @param {string} filePath - path of the ZIP archive to extract * @param {object} requestOptions - request options for retry attempt * @param {boolean} retry - set to true if it's already an retry attempt * @returns {*} - path of the extracted archive content * @function */ function extractDownload(filePath, requestOptions, retry) { const deferred = kew.defer(); // extract to a unique directory in case multiple processes are // installing and extracting at once const extractedPath = filePath + '-extract-' + Date.now(); let options = { cwd: extractedPath }; fs.mkdirsSync(extractedPath, '0777'); // Make double sure we have 0777 permissions; some operating systems // default umask does not allow write by default. fs.chmodSync(extractedPath, '0777'); if (filePath.substr(-4) === '.zip') { log.info('Extracting zip contents'); try { let zip = new AdmZip(filePath); zip.extractAllTo(extractedPath, true); deferred.resolve(extractedPath); } catch (err) { log.error('Error extracting zip'); deferred.reject(err); } } else { log.info('Extracting tar contents (via spawned process)'); cp.execFile('tar', ['jxf', filePath], options, function (err) { if (err) { if (!retry) { log.info('Error during extracting. Trying to download again.'); fs.unlinkSync(filePath); return requestBinary(requestOptions, filePath).then(function (downloadedFile) { return extractDownload(downloadedFile, requestOptions, true); }); } else { deferred.reject(err); log.error('Error extracting archive'); } } else { deferred.resolve(extractedPath); } }); } return deferred.promise; } /** * Helper function to move folder contents to target directory * @param {string} extractedPath - source directory path * @param {string} targetPath - target directory path * @returns {string} {!Promise.<RESULT>} promise for chaing * @function */ function copyIntoPlace(extractedPath, targetPath) { log.info('Removing', targetPath); return kew.nfcall(fs.remove, targetPath).then(function () { // Look for the extracted directory, so we can rename it. const files = fs.readdirSync(extractedPath); for (let i = 0; i < files.length; i++) { const file = path.join(extractedPath, files[i]); if (fs.statSync(file).isDirectory() && file.indexOf(helper.version) !== -1) { log.info('Copying extracted folder', file, '->', targetPath); return kew.nfcall(fs.move, file, targetPath); } } log.info('Could not find extracted file', files); throw new Error('Could not find extracted file'); }); }