├── test ├── exit.js ├── loadspeed.js └── tests.js ├── .eslintignore ├── .jshintrc ├── .gitignore ├── .travis.yml ├── README.md ├── .eslintrc ├── bin └── slimerjs ├── package.json ├── lib ├── slimerjs.js └── util.js ├── LICENSE.txt └── install.js /test/exit.js: -------------------------------------------------------------------------------- 1 | slimer.exit(123) 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/phantom 2 | lib/location.js -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "node": true 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/slimer 3 | /lib/location.js 4 | /tmp 5 | npm-debug.log 6 | *.swp 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "5" 5 | - "4" 6 | - "0.12" 7 | - "0.10" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | slimerjs has moved back to its offically suppoted home at: 5 | 6 | https://github.com/laurentj/slimerjs/blob/master/package.json#L2 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "quotes": [ 5 | 2, 6 | "single" 7 | ], 8 | "linebreak-style": [ 9 | 2, 10 | "unix" 11 | ], 12 | "semi": [ 13 | 2, 14 | "never" 15 | ] 16 | }, 17 | "env": { 18 | "node": true, 19 | "phantomjs": true 20 | }, 21 | "extends": "eslint:recommended" 22 | } -------------------------------------------------------------------------------- /test/loadspeed.js: -------------------------------------------------------------------------------- 1 | // slimerjs test script 2 | // opens url and reports time to load 3 | // requires an active internet connection 4 | var page = require('webpage').create() 5 | var system = require('system') 6 | var t 7 | var address 8 | 9 | if (system.args.length === 1) { 10 | console.log('Usage: loadspeed.js ') 11 | slimer.exit() 12 | } 13 | 14 | t = Date.now() 15 | address = system.args[1] 16 | page.open(address, function (status) { 17 | if (status !== 'success') { 18 | console.log('FAIL to load the address') 19 | } else { 20 | t = Date.now() - t 21 | console.log('Loading time ' + t + ' msec') 22 | } 23 | 24 | slimer.exit() 25 | }) -------------------------------------------------------------------------------- /bin/slimerjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Script that will execute the downloaded slimerjs binary. stdio are 5 | * forwarded to and from the child process. 6 | * 7 | * The following is for an ugly hack to avoid a problem where the installer 8 | * finds the bin script npm creates during global installation. 9 | * 10 | * {NPM_INSTALL_MARKER} 11 | */ 12 | 13 | var path = require('path') 14 | var spawn = require('child_process').spawn 15 | 16 | var binPath = require(path.join(__dirname, '..', 'lib', 'slimerjs')).path 17 | 18 | var args = process.argv.slice(2) 19 | 20 | // For Node 0.6 compatibility, pipe the streams manually, instead of using 21 | // `{ stdio: 'inherit' }`. 22 | var cp = spawn(binPath, args) 23 | cp.stdout.pipe(process.stdout) 24 | cp.stderr.pipe(process.stderr) 25 | process.stdin.pipe(cp.stdin) 26 | 27 | cp.on('error', function (err) { 28 | console.error('Error executing slimer at', binPath) 29 | console.error(err.stack) 30 | }) 31 | 32 | cp.on('exit', function(code){ 33 | // Wait few ms for error to be printed. 34 | setTimeout(function(){ 35 | process.exit(code) 36 | }, 20) 37 | }); 38 | 39 | process.on('SIGTERM', function() { 40 | cp.kill('SIGTERM') 41 | process.exit(1) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slimerjs", 3 | "version": "0.906.2", 4 | "keywords": [ 5 | "slimerjs", 6 | "headless", 7 | "gecko" 8 | ], 9 | "description": "Headless Gecko with JS API", 10 | "homepage": "https://github.com/graingert/slimerjs", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/graingert/slimerjs.git" 14 | }, 15 | "licenses": [ 16 | { 17 | "type": "Apache-2.0", 18 | "url": "https://www.apache.org/licenses/LICENSE-2.0.html" 19 | } 20 | ], 21 | "author": { 22 | "name": "Dan Pupius", 23 | "email": "dan@obvious.com", 24 | "url": "http://pupius.co.uk" 25 | }, 26 | "maintainers": [ 27 | { 28 | "name": "Thomas Grainger", 29 | "email": "tagrain@gmail.com", 30 | "web": "https://graingert.co.uk/" 31 | }, 32 | { 33 | "name": "David Hanskopeit", 34 | "email": "david@monkey-works.de", 35 | "web": "https://www.monkey-works.de/" 36 | } 37 | ], 38 | "main": "lib/slimerjs", 39 | "bin": { 40 | "slimerjs": "./bin/slimerjs" 41 | }, 42 | "scripts": { 43 | "install": "node install.js", 44 | "test": "nodeunit --reporter=minimal test/tests.js && eslint ." 45 | }, 46 | "dependencies": { 47 | "es6-promise": "^3.2.1", 48 | "extract-zip": "~1.5.0", 49 | "fs-extra": "~0.30.0", 50 | "hasha": "^2.2.0", 51 | "kew": "~0.7.0", 52 | "progress": "~1.1.8", 53 | "request": "~2.74.0", 54 | "request-progress": "~2.0.1", 55 | "which": "~1.2.10" 56 | }, 57 | "devDependencies": { 58 | "eslint": "2.7.0", 59 | "nodeunit": "0.9.1", 60 | "webdriverio": "^4.2.3" 61 | }, 62 | "bundledDependencies": [ 63 | "extract-zip", 64 | "fs-extra", 65 | "hasha", 66 | "kew", 67 | "progress", 68 | "request", 69 | "request-progress", 70 | "which" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /lib/slimerjs.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Obvious Corporation. 2 | 3 | /** 4 | * @fileoverview Helpers made available via require('slimerjs') once package is 5 | * installed. 6 | */ 7 | 8 | var fs = require('fs') 9 | var path = require('path') 10 | var spawn = require('child_process').spawn 11 | var Promise = require('es6-promise').Promise 12 | 13 | 14 | /** 15 | * Where the slimer binary can be found. 16 | * @type {string} 17 | */ 18 | try { 19 | var location = require('./location') 20 | exports.path = path.resolve(__dirname, location.location) 21 | exports.platform = location.platform 22 | exports.arch = location.arch 23 | } catch(e) { 24 | // Must be running inside install script. 25 | exports.path = null 26 | } 27 | 28 | 29 | /** 30 | * The version of slimerjs installed by this package. 31 | * @type {number} 32 | */ 33 | exports.version = '0.9.6' 34 | 35 | 36 | /** 37 | * Returns a clean path that helps avoid `which` finding bin files installed 38 | * by NPM for this repo. 39 | * @param {string} path 40 | * @return {string} 41 | */ 42 | exports.cleanPath = function (path) { 43 | return path 44 | .replace(/:[^:]*node_modules[^:]*/g, '') 45 | .replace(/(^|:)\.\/bin(\:|$)/g, ':') 46 | .replace(/^:+/, '') 47 | .replace(/:+$/, '') 48 | } 49 | 50 | 51 | // Make sure the binary is executable. For some reason doing this inside 52 | // install does not work correctly, likely due to some NPM step. 53 | if (exports.path) { 54 | try { 55 | // avoid touching the binary if it's already got the correct permissions 56 | var st = fs.statSync(exports.path) 57 | var mode = st.mode | parseInt('0555', 8) 58 | if (mode !== st.mode) { 59 | fs.chmodSync(exports.path, mode) 60 | } 61 | } catch (e) { 62 | // Just ignore error if we don't have permission. 63 | // We did our best. Likely because slimerjs was already installed. 64 | } 65 | } 66 | 67 | /** 68 | * Executes a script or just runs PhantomJS 69 | */ 70 | exports.exec = function () { 71 | var args = Array.prototype.slice.call(arguments) 72 | return spawn(exports.path, args) 73 | } 74 | 75 | /** 76 | * Runs PhantomJS with provided options 77 | * @example 78 | * // handy with WebDriver 79 | * phantomjs.run('--webdriver=4444').then(program => { 80 | * // do something 81 | * program.kill() 82 | * }) 83 | * @returns {Promise} the process of PhantomJS 84 | */ 85 | exports.run = function () { 86 | var args = arguments 87 | return new Promise(function (resolve, reject) { 88 | try { 89 | var program = exports.exec.apply(null, args) 90 | var isFirst = true 91 | var stderr = '' 92 | program.stdout.on('data', function () { 93 | // This detects PhantomJS instance get ready. 94 | if (!isFirst) return 95 | isFirst = false 96 | resolve(program) 97 | }) 98 | program.stderr.on('data', function (data) { 99 | stderr = stderr + data.toString('utf8') 100 | }) 101 | program.on('error', function (err) { 102 | if (!isFirst) return 103 | isFirst = false 104 | reject(err) 105 | }) 106 | program.on('exit', function (code) { 107 | if (!isFirst) return 108 | isFirst = false 109 | if (code == 0) { 110 | // PhantomJS doesn't use exit codes correctly :( 111 | if (stderr.indexOf('Error:') == 0) { 112 | reject(new Error(stderr)) 113 | } else { 114 | resolve(program) 115 | } 116 | } else { 117 | reject(new Error('Exit code: ' + code)) 118 | } 119 | }) 120 | } catch (err) { 121 | reject(err) 122 | } 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Nodeunit functional tests. Requires internet connection to validate slimer 3 | * functions correctly. 4 | */ 5 | 6 | var childProcess = require('child_process') 7 | var fs = require('fs') 8 | var path = require('path') 9 | var webdriverio = require('webdriverio') 10 | var slimerjs = require('../lib/slimerjs') 11 | var util = require('../lib/util') 12 | 13 | exports.testDownload = function (test) { 14 | test.expect(1) 15 | test.ok(fs.existsSync(slimerjs.path), 'Binary file should have been downloaded') 16 | test.done() 17 | } 18 | 19 | var baseArgs = [ 20 | // run SlimerJS using virtual frame buffer (xvfb) 21 | '--auto-servernum', 22 | '--server-num=1', 23 | slimerjs.path, 24 | ] 25 | 26 | exports.testSlimerExecutesTestScript = function (test) { 27 | test.expect(2) 28 | 29 | var childArgs = [ 30 | // SlimerJS arguments 31 | path.join(__dirname, 'loadspeed.js'), 32 | 'https://www.google.com/' 33 | ] 34 | 35 | childProcess.execFile('xvfb-run', baseArgs.concat(childArgs), function (err, stdout) { 36 | var value = (stdout.indexOf('msec') !== -1) 37 | test.ok(err === null, 'Test script should complete without errors') 38 | test.ok(value, 'Test script should have executed and returned run time') 39 | test.done() 40 | }) 41 | } 42 | 43 | 44 | exports.testSlimerExitCode = function (test) { 45 | test.expect(1) 46 | childProcess.execFile('xvfb-run', baseArgs.concat([path.join(__dirname, 'exit.js')]), function (err) { 47 | test.equals(err.code, 123, 'Exit code should be returned from phantom script') 48 | test.done() 49 | }) 50 | } 51 | 52 | 53 | exports.testBinFile = function (test) { 54 | test.expect(1) 55 | 56 | var binPath = process.platform === 'win32' ? 57 | path.join(__dirname, '..', 'lib', 'slimer', 'slimerjs.exe') : 58 | path.join(__dirname, '..', 'bin', 'slimerjs') 59 | 60 | childProcess.execFile(binPath, ['--version'], function (err, stdout) { 61 | test.ok(stdout.trim().indexOf(slimerjs.version) >= -1, 'Version should be match') 62 | test.done() 63 | }) 64 | } 65 | 66 | 67 | exports.testCleanPath = function (test) { 68 | test.expect(5) 69 | test.equal('/Users/dan/bin', slimerjs.cleanPath('/Users/dan/bin:./bin')) 70 | test.equal('/Users/dan/bin:/usr/bin', slimerjs.cleanPath('/Users/dan/bin:./bin:/usr/bin')) 71 | test.equal('/usr/bin', slimerjs.cleanPath('./bin:/usr/bin')) 72 | test.equal('', slimerjs.cleanPath('./bin')) 73 | test.equal('/Work/bin:/usr/bin', slimerjs.cleanPath('/Work/bin:/Work/slimerjs/node_modules/.bin:/usr/bin')) 74 | test.done() 75 | } 76 | 77 | exports.testBogusReinstallLocation = function (test) { 78 | util.findValidPhantomJsBinary('./blargh') 79 | .then(function (binaryLocation) { 80 | test.ok(!binaryLocation, 'Expected link to fail') 81 | test.done() 82 | }) 83 | } 84 | 85 | exports.testSuccessfulReinstallLocation = function (test) { 86 | util.findValidPhantomJsBinary(path.resolve(__dirname, '../lib/location')) 87 | .then(function (binaryLocation) { 88 | test.ok(binaryLocation, 'Expected link to succeed') 89 | test.done() 90 | }) 91 | } 92 | 93 | exports.testBogusVerifyChecksum = function (test) { 94 | util.verifyChecksum(path.resolve(__dirname, './exit.js'), 'blargh') 95 | .then(function (success) { 96 | test.ok(!success, 'Expected checksum to fail') 97 | test.done() 98 | }) 99 | } 100 | 101 | exports.testSuccessfulVerifyChecksum = function (test) { 102 | util.verifyChecksum(path.resolve(__dirname, './exit.js'), 103 | '31dfa8fd11176e00d29fa27aa32c5af64de46c121c059fe41c2543fcae4318fd') 104 | .then(function (success) { 105 | test.ok(success, 'Expected checksum to succeed') 106 | test.done() 107 | }) 108 | } 109 | 110 | exports.testSlimerExec = function (test) { 111 | test.expect(1) 112 | var p = slimerjs.exec(path.join(__dirname, 'exit.js')) 113 | p.on('exit', function (code) { 114 | test.equals(code, 123, 'Exit code should be returned from phantom script') 115 | test.done() 116 | }) 117 | } 118 | 119 | exports.testSlimerRun = function (test) { 120 | test.expect(1) 121 | var wdOpts = { desiredCapabilities: { browserName: 'slimerjs' } } 122 | slimerjs.run('--webdriver=4444').then(function (p) { 123 | webdriverio.remote(wdOpts).init() 124 | .url('https://developer.mozilla.org/en-US/') 125 | .getTitle().then(function (title) { 126 | test.equals(title, 'Mozilla Developer Network', 'Page title') 127 | }) 128 | .then(function () { 129 | p.kill() 130 | test.done() 131 | }) 132 | }) 133 | } 134 | 135 | exports.testSlimerRunError = function (test) { 136 | test.expect(1) 137 | slimerjs.run('--bogus').then(function () { 138 | test.ok(false, 'Expected not to start') 139 | test.done() 140 | }, function (err) { 141 | test.equal('Error: Unknown option: bogus\n', err.message) 142 | test.done() 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Package-private helpers for the installer. 3 | */ 4 | 5 | 'use strict' 6 | 7 | var cp = require('child_process') 8 | var fs = require('fs-extra') 9 | var hasha = require('hasha') 10 | var helper = require('./slimerjs') 11 | var kew = require('kew') 12 | var path = require('path') 13 | 14 | var DEFAULT_CDN = 'https://github.com/graingert/slimer-downloads/releases/download/' 15 | var libPath = __dirname 16 | 17 | /** 18 | * Given a lib/location file of a SlimerJS previously installed with NPM, 19 | * is there a valid SlimerJS binary at this lib/location. 20 | * @return {Promise} resolved location of phantomjs binary on success 21 | */ 22 | function findValidSlimerJsBinary(libPath) { 23 | return kew.fcall(function () { 24 | var libModule = require(libPath) 25 | if (libModule.location && 26 | getTargetPlatform() == libModule.platform && 27 | getTargetArch() == libModule.arch) { 28 | var resolvedLocation = path.resolve(path.dirname(libPath), libModule.location) 29 | if (fs.statSync(resolvedLocation)) { 30 | return checkSlimerjsVersion(resolvedLocation).then(function (matches) { 31 | if (matches) { 32 | return kew.resolve(resolvedLocation) 33 | } 34 | }) 35 | } 36 | } 37 | return false 38 | }).fail(function () { 39 | return false 40 | }) 41 | } 42 | 43 | /** 44 | * Check to make sure a given binary is the right version. 45 | * @return {kew.Promise.} 46 | */ 47 | function checkSlimerjsVersion(slimerPath) { 48 | console.log('Found SlimerJS at', slimerPath, '...verifying') 49 | return kew.nfcall(cp.execFile, slimerPath, ['--version']).then(function (stdout) { 50 | if (stdout.indexOf('SlimerJS ' + helper.version + ',') !== -1) { 51 | return true 52 | } else { 53 | console.log('SlimerJS detected, but wrong version', stdout, '@', slimerPath + '.') 54 | return false 55 | } 56 | }).fail(function (err) { 57 | console.error('Error verifying slimerjs, continuing', err) 58 | return false 59 | }) 60 | } 61 | 62 | /** 63 | * Writes the location file with location and platform/arch metadata about the 64 | * binary. 65 | */ 66 | function writeLocationFile(location) { 67 | console.log('Writing location.js file') 68 | if (getTargetPlatform() === 'win32') { 69 | location = location.replace(/\\/g, '\\\\') 70 | } 71 | 72 | var platform = getTargetPlatform() 73 | var arch = getTargetArch() 74 | 75 | var contents = 'module.exports.location = "' + location + '"\n' 76 | 77 | if (/^[a-zA-Z0-9]*$/.test(platform) && /^[a-zA-Z0-9]*$/.test(arch)) { 78 | contents += 79 | 'module.exports.platform = "' + getTargetPlatform() + '"\n' + 80 | 'module.exports.arch = "' + getTargetArch() + '"\n' 81 | } 82 | 83 | fs.writeFileSync(path.join(libPath, 'location.js'), contents) 84 | } 85 | 86 | /** 87 | * @return {?{url: string, checksum: string}} Get the download URL and expected 88 | * SHA-256 checksum for phantomjs. May return null if no download url exists. 89 | */ 90 | function getDownloadSpec() { 91 | var cdnUrl = process.env.npm_config_phantomjs_cdnurl || 92 | process.env.PHANTOMJS_CDNURL || 93 | DEFAULT_CDN 94 | var downloadUrl = cdnUrl + '/' + helper.version +'/slimerjs-'+ helper.version +'-' 95 | var checksum = '' 96 | 97 | var platform = getTargetPlatform() 98 | var arch = getTargetArch() 99 | if (platform === 'linux' && arch === 'x64') { 100 | downloadUrl += 'linux-x86_64.tar.bz2' 101 | checksum = '14e707c838e85f8131fb59b8cc38b5d81b4d45c194db7432e97ff331c913b89d' 102 | } else if (platform === 'linux' && arch == 'ia32') { 103 | downloadUrl += 'linux-i686.tar.bz2' 104 | checksum = '4bc37cb8c58e5ddfa76ffd066ebceb04529d74a0e52066d9bed9759c30e5841b' 105 | } else if (platform === 'darwin' || platform === 'openbsd' || platform === 'freebsd') { 106 | downloadUrl += 'mac.tar.bz2' 107 | checksum = '5c3ba9a83328a54b1fc6a6106abdd6d6b2117768f36ad43b9b0230a3ad7113cd' 108 | } else if (platform === 'win32') { 109 | downloadUrl += 'win32.zip' 110 | checksum = '4eead5e92a87f655f999ae79bf3c4eac191ec4bd93a19ffd8bbb2a12a1cb1ef4' 111 | } else { 112 | return null 113 | } 114 | return {url: downloadUrl, checksum: checksum} 115 | } 116 | 117 | /** * Check to make sure that the file matches the checksum. 118 | * @param {string} fileName 119 | * @param {string} checksum 120 | * @return {Promise.} 121 | */ 122 | function verifyChecksum(fileName, checksum) { 123 | return kew.resolve(hasha.fromFile(fileName, {algorithm: 'sha256'})).then(function (hash) { 124 | var result = checksum == hash 125 | if (result) { 126 | console.log('Verified checksum of previously downloaded file') 127 | } else { 128 | console.log('Checksum did not match') 129 | } 130 | return result 131 | }).fail(function (err) { 132 | console.error('Failed to verify checksum: ', err) 133 | return false 134 | }) 135 | } 136 | 137 | /** 138 | * @return {string} 139 | */ 140 | function getTargetPlatform() { 141 | return process.env.SLIMERJS_PLATFORM || process.platform 142 | } 143 | 144 | /** 145 | * @return {string} 146 | */ 147 | function getTargetArch() { 148 | return process.env.SLIMERJS_ARCH || process.arch 149 | } 150 | 151 | module.exports = { 152 | checkSlimerjsVersion: checkSlimerjsVersion, 153 | getDownloadSpec: getDownloadSpec, 154 | getTargetPlatform: getTargetPlatform, 155 | getTargetArch: getTargetArch, 156 | findValidSlimerJsBinary: findValidPhantomJsBinary, 157 | verifyChecksum: verifyChecksum, 158 | writeLocationFile: writeLocationFile 159 | } 160 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012 The Obvious Corporation. 2 | http://obvious.com/ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | 17 | ------------------------------------------------------------------------- 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | 23 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 24 | 25 | 1. Definitions. 26 | 27 | "License" shall mean the terms and conditions for use, reproduction, 28 | and distribution as defined by Sections 1 through 9 of this document. 29 | 30 | "Licensor" shall mean the copyright owner or entity authorized by 31 | the copyright owner that is granting the License. 32 | 33 | "Legal Entity" shall mean the union of the acting entity and all 34 | other entities that control, are controlled by, or are under common 35 | control with that entity. For the purposes of this definition, 36 | "control" means (i) the power, direct or indirect, to cause the 37 | direction or management of such entity, whether by contract or 38 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 39 | outstanding shares, or (iii) beneficial ownership of such entity. 40 | 41 | "You" (or "Your") shall mean an individual or Legal Entity 42 | exercising permissions granted by this License. 43 | 44 | "Source" form shall mean the preferred form for making modifications, 45 | including but not limited to software source code, documentation 46 | source, and configuration files. 47 | 48 | "Object" form shall mean any form resulting from mechanical 49 | transformation or translation of a Source form, including but 50 | not limited to compiled object code, generated documentation, 51 | and conversions to other media types. 52 | 53 | "Work" shall mean the work of authorship, whether in Source or 54 | Object form, made available under the License, as indicated by a 55 | copyright notice that is included in or attached to the work 56 | (an example is provided in the Appendix below). 57 | 58 | "Derivative Works" shall mean any work, whether in Source or Object 59 | form, that is based on (or derived from) the Work and for which the 60 | editorial revisions, annotations, elaborations, or other modifications 61 | represent, as a whole, an original work of authorship. For the purposes 62 | of this License, Derivative Works shall not include works that remain 63 | separable from, or merely link (or bind by name) to the interfaces of, 64 | the Work and Derivative Works thereof. 65 | 66 | "Contribution" shall mean any work of authorship, including 67 | the original version of the Work and any modifications or additions 68 | to that Work or Derivative Works thereof, that is intentionally 69 | submitted to Licensor for inclusion in the Work by the copyright owner 70 | or by an individual or Legal Entity authorized to submit on behalf of 71 | the copyright owner. For the purposes of this definition, "submitted" 72 | means any form of electronic, verbal, or written communication sent 73 | to the Licensor or its representatives, including but not limited to 74 | communication on electronic mailing lists, source code control systems, 75 | and issue tracking systems that are managed by, or on behalf of, the 76 | Licensor for the purpose of discussing and improving the Work, but 77 | excluding communication that is conspicuously marked or otherwise 78 | designated in writing by the copyright owner as "Not a Contribution." 79 | 80 | "Contributor" shall mean Licensor and any individual or Legal Entity 81 | on behalf of whom a Contribution has been received by Licensor and 82 | subsequently incorporated within the Work. 83 | 84 | 2. Grant of Copyright License. Subject to the terms and conditions of 85 | this License, each Contributor hereby grants to You a perpetual, 86 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 87 | copyright license to reproduce, prepare Derivative Works of, 88 | publicly display, publicly perform, sublicense, and distribute the 89 | Work and such Derivative Works in Source or Object form. 90 | 91 | 3. Grant of Patent License. Subject to the terms and conditions of 92 | this License, each Contributor hereby grants to You a perpetual, 93 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 94 | (except as stated in this section) patent license to make, have made, 95 | use, offer to sell, sell, import, and otherwise transfer the Work, 96 | where such license applies only to those patent claims licensable 97 | by such Contributor that are necessarily infringed by their 98 | Contribution(s) alone or by combination of their Contribution(s) 99 | with the Work to which such Contribution(s) was submitted. If You 100 | institute patent litigation against any entity (including a 101 | cross-claim or counterclaim in a lawsuit) alleging that the Work 102 | or a Contribution incorporated within the Work constitutes direct 103 | or contributory patent infringement, then any patent licenses 104 | granted to You under this License for that Work shall terminate 105 | as of the date such litigation is filed. 106 | 107 | 4. Redistribution. You may reproduce and distribute copies of the 108 | Work or Derivative Works thereof in any medium, with or without 109 | modifications, and in Source or Object form, provided that You 110 | meet the following conditions: 111 | 112 | (a) You must give any other recipients of the Work or 113 | Derivative Works a copy of this License; and 114 | 115 | (b) You must cause any modified files to carry prominent notices 116 | stating that You changed the files; and 117 | 118 | (c) You must retain, in the Source form of any Derivative Works 119 | that You distribute, all copyright, patent, trademark, and 120 | attribution notices from the Source form of the Work, 121 | excluding those notices that do not pertain to any part of 122 | the Derivative Works; and 123 | 124 | (d) If the Work includes a "NOTICE" text file as part of its 125 | distribution, then any Derivative Works that You distribute must 126 | include a readable copy of the attribution notices contained 127 | within such NOTICE file, excluding those notices that do not 128 | pertain to any part of the Derivative Works, in at least one 129 | of the following places: within a NOTICE text file distributed 130 | as part of the Derivative Works; within the Source form or 131 | documentation, if provided along with the Derivative Works; or, 132 | within a display generated by the Derivative Works, if and 133 | wherever such third-party notices normally appear. The contents 134 | of the NOTICE file are for informational purposes only and 135 | do not modify the License. You may add Your own attribution 136 | notices within Derivative Works that You distribute, alongside 137 | or as an addendum to the NOTICE text from the Work, provided 138 | that such additional attribution notices cannot be construed 139 | as modifying the License. 140 | 141 | You may add Your own copyright statement to Your modifications and 142 | may provide additional or different license terms and conditions 143 | for use, reproduction, or distribution of Your modifications, or 144 | for any such Derivative Works as a whole, provided Your use, 145 | reproduction, and distribution of the Work otherwise complies with 146 | the conditions stated in this License. 147 | 148 | 5. Submission of Contributions. Unless You explicitly state otherwise, 149 | any Contribution intentionally submitted for inclusion in the Work 150 | by You to the Licensor shall be under the terms and conditions of 151 | this License, without any additional terms or conditions. 152 | Notwithstanding the above, nothing herein shall supersede or modify 153 | the terms of any separate license agreement you may have executed 154 | with Licensor regarding such Contributions. 155 | 156 | 6. Trademarks. This License does not grant permission to use the trade 157 | names, trademarks, service marks, or product names of the Licensor, 158 | except as required for reasonable and customary use in describing the 159 | origin of the Work and reproducing the content of the NOTICE file. 160 | 161 | 7. Disclaimer of Warranty. Unless required by applicable law or 162 | agreed to in writing, Licensor provides the Work (and each 163 | Contributor provides its Contributions) on an "AS IS" BASIS, 164 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 165 | implied, including, without limitation, any warranties or conditions 166 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 167 | PARTICULAR PURPOSE. You are solely responsible for determining the 168 | appropriateness of using or redistributing the Work and assume any 169 | risks associated with Your exercise of permissions under this License. 170 | 171 | 8. Limitation of Liability. In no event and under no legal theory, 172 | whether in tort (including negligence), contract, or otherwise, 173 | unless required by applicable law (such as deliberate and grossly 174 | negligent acts) or agreed to in writing, shall any Contributor be 175 | liable to You for damages, including any direct, indirect, special, 176 | incidental, or consequential damages of any character arising as a 177 | result of this License or out of the use or inability to use the 178 | Work (including but not limited to damages for loss of goodwill, 179 | work stoppage, computer failure or malfunction, or any and all 180 | other commercial damages or losses), even if such Contributor 181 | has been advised of the possibility of such damages. 182 | 183 | 9. Accepting Warranty or Additional Liability. While redistributing 184 | the Work or Derivative Works thereof, You may choose to offer, 185 | and charge a fee for, acceptance of support, warranty, indemnity, 186 | or other liability obligations and/or rights consistent with this 187 | License. However, in accepting such obligations, You may act only 188 | on Your own behalf and on Your sole responsibility, not on behalf 189 | of any other Contributor, and only if You agree to indemnify, 190 | defend, and hold each Contributor harmless for any liability 191 | incurred by, or claims asserted against, such Contributor by reason 192 | of your accepting any such warranty or additional liability. 193 | 194 | END OF TERMS AND CONDITIONS 195 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Obvious Corporation. 2 | 3 | /* 4 | * This simply fetches the right version of slimer for the current platform. 5 | */ 6 | 7 | 'use strict' 8 | 9 | var requestProgress = require('request-progress') 10 | var progress = require('progress') 11 | var extractZip = require('extract-zip') 12 | var cp = require('child_process') 13 | var fs = require('fs-extra') 14 | var helper = require('./lib/slimerjs') 15 | var kew = require('kew') 16 | var path = require('path') 17 | var request = require('request') 18 | var url = require('url') 19 | var util = require('./lib/util') 20 | var which = require('which') 21 | var os = require('os') 22 | 23 | var originalPath = process.env.PATH 24 | 25 | var checkSlimerjsVersion = util.checkSlimerjsVersion 26 | var getTargetPlatform = util.getTargetPlatform 27 | var getTargetArch = util.getTargetArch 28 | var getDownloadSpec = util.getDownloadSpec 29 | var findValidSlimerJsBinary = util.findValidSlimerJsBinary 30 | var verifyChecksum = util.verifyChecksum 31 | var writeLocationFile = util.writeLocationFile 32 | 33 | // If the process exits without going through exit(), then we did not complete. 34 | var validExit = false 35 | 36 | process.on('exit', function () { 37 | if (!validExit) { 38 | console.log('Install exited unexpectedly') 39 | exit(1) 40 | } 41 | }) 42 | 43 | // NPM adds bin directories to the path, which will cause `which` to find the 44 | // bin for this package not the actual slimerjs bin. Also help out people who 45 | // put ./bin on their path 46 | process.env.PATH = helper.cleanPath(originalPath) 47 | 48 | var libPath = path.join(__dirname, 'lib') 49 | var pkgPath = path.join(libPath, 'slimer') 50 | var slimerPath = null 51 | 52 | // If the user manually installed SlimerJS, we want 53 | // to use the existing version. 54 | // 55 | // Do not re-use a manually-installed SlimerJS with 56 | // a different version. 57 | // 58 | // Do not re-use an npm-installed SlimerJS, because 59 | // that can lead to weird circular dependencies between 60 | // local versions and global versions. 61 | // https://github.com/Obvious/phantomjs/issues/85 62 | // https://github.com/Medium/phantomjs/pull/184 63 | kew.resolve(true) 64 | .then(trySlimerjsInLib) 65 | .then(trySlimerjsOnPath) 66 | .then(downloadSlimerjs) 67 | .then(extractDownload) 68 | .then(function (extractedPath) { 69 | return copyIntoPlace(extractedPath, pkgPath) 70 | }) 71 | .then(function () { 72 | var location = getTargetPlatform() === 'win32' ? 73 | path.join(pkgPath, 'slimerjs.bat') : 74 | path.join(pkgPath, 'slimerjs') 75 | 76 | try { 77 | // Ensure executable is executable by all users 78 | fs.chmodSync(location, '755') 79 | } catch (err) { 80 | if (err.code == 'ENOENT') { 81 | console.error('chmod failed: slimerjs was not successfully copied to', location) 82 | exit(1) 83 | } 84 | throw err 85 | } 86 | 87 | var relativeLocation = path.relative(libPath, location) 88 | writeLocationFile(relativeLocation) 89 | 90 | console.log('Done. SlimerJS binary available at', location) 91 | exit(0) 92 | }) 93 | .fail(function (err) { 94 | console.error('Slimer installation failed', err, err.stack) 95 | exit(1) 96 | }) 97 | 98 | function exit(code) { 99 | validExit = true 100 | process.env.PATH = originalPath 101 | process.exit(code || 0) 102 | } 103 | 104 | 105 | function findSuitableTempDirectory() { 106 | var now = Date.now() 107 | var candidateTmpDirs = [ 108 | process.env.npm_config_tmp, 109 | os.tmpdir(), 110 | path.join(process.cwd(), 'tmp') 111 | ] 112 | 113 | for (var i = 0; i < candidateTmpDirs.length; i++) { 114 | var candidatePath = candidateTmpDirs[i] 115 | if (!candidatePath) continue 116 | 117 | try { 118 | candidatePath = path.join(path.resolve(candidatePath), 'slimerjs') 119 | fs.mkdirsSync(candidatePath, '0777') 120 | // Make double sure we have 0777 permissions; some operating systems 121 | // default umask does not allow write by default. 122 | fs.chmodSync(candidatePath, '0777') 123 | var testFile = path.join(candidatePath, now + '.tmp') 124 | fs.writeFileSync(testFile, 'test') 125 | fs.unlinkSync(testFile) 126 | return candidatePath 127 | } catch (e) { 128 | console.log(candidatePath, 'is not writable:', e.message) 129 | } 130 | } 131 | 132 | console.error('Can not find a writable tmp directory, please report issue ' + 133 | 'on https://github.com/graingert/slimerjs/issues with as much ' + 134 | 'information as possible.') 135 | exit(1) 136 | } 137 | 138 | 139 | function getRequestOptions() { 140 | var strictSSL = !!process.env.npm_config_strict_ssl 141 | if (process.version == 'v0.10.34') { 142 | console.log('Node v0.10.34 detected, turning off strict ssl due to https://github.com/joyent/node/issues/8894') 143 | strictSSL = false 144 | } 145 | 146 | var options = { 147 | uri: getDownloadUrl(), 148 | encoding: null, // Get response as a buffer 149 | followRedirect: true, // The default download path redirects to a CDN URL. 150 | headers: {}, 151 | strictSSL: strictSSL 152 | } 153 | 154 | var proxyUrl = process.env.npm_config_https_proxy || 155 | process.env.npm_config_http_proxy || 156 | process.env.npm_config_proxy 157 | if (proxyUrl) { 158 | 159 | // Print using proxy 160 | var proxy = url.parse(proxyUrl) 161 | if (proxy.auth) { 162 | // Mask password 163 | proxy.auth = proxy.auth.replace(/:.*$/, ':******') 164 | } 165 | console.log('Using proxy ' + url.format(proxy)) 166 | 167 | // Enable proxy 168 | options.proxy = proxyUrl 169 | } 170 | 171 | // Use the user-agent string from the npm config 172 | options.headers['User-Agent'] = process.env.npm_config_user_agent 173 | 174 | // Use certificate authority settings from npm 175 | var ca = process.env.npm_config_ca 176 | if (!ca && process.env.npm_config_cafile) { 177 | try { 178 | ca = fs.readFileSync(process.env.npm_config_cafile, {encoding: 'utf8'}) 179 | .split(/\n(?=-----BEGIN CERTIFICATE-----)/g) 180 | 181 | // Comments at the beginning of the file result in the first 182 | // item not containing a certificate - in this case the 183 | // download will fail 184 | if (ca.length > 0 && !/-----BEGIN CERTIFICATE-----/.test(ca[0])) { 185 | ca.shift() 186 | } 187 | 188 | } catch (e) { 189 | console.error('Could not read cafile', process.env.npm_config_cafile, e) 190 | } 191 | } 192 | 193 | if (ca) { 194 | console.log('Using npmconf ca') 195 | options.agentOptions = { 196 | ca: ca 197 | } 198 | options.ca = ca 199 | } 200 | 201 | return options 202 | } 203 | 204 | function handleRequestError(error) { 205 | if (error && error.stack && error.stack.indexOf('SELF_SIGNED_CERT_IN_CHAIN') != -1) { 206 | console.error('Error making request, SELF_SIGNED_CERT_IN_CHAIN. ' + 207 | 'Please read https://github.com/graingert/slimerjs#i-am-behind-a-corporate-proxy-that-uses-self-signed-ssl-certificates-to-intercept-encrypted-traffic') 208 | exit(1) 209 | } else if (error) { 210 | console.error('Error making request.\n' + error.stack + '\n\n' + 211 | 'Please report this full log at https://github.com/graingert/slimerjs') 212 | exit(1) 213 | } else { 214 | console.error('Something unexpected happened, please report this full ' + 215 | 'log at https://github.com/graingert/slimerjs') 216 | exit(1) 217 | } 218 | } 219 | 220 | function requestBinary(requestOptions, filePath) { 221 | var deferred = kew.defer() 222 | 223 | var writePath = filePath + '-download-' + Date.now() 224 | 225 | console.log('Receiving...') 226 | var bar = null 227 | requestProgress(request(requestOptions, function (error, response, body) { 228 | console.log('') 229 | if (!error && response.statusCode === 200) { 230 | fs.writeFileSync(writePath, body) 231 | console.log('Received ' + Math.floor(body.length / 1024) + 'K total.') 232 | fs.renameSync(writePath, filePath) 233 | deferred.resolve(filePath) 234 | 235 | } else if (response) { 236 | console.error('Error requesting archive.\n' + 237 | 'Status: ' + response.statusCode + '\n' + 238 | 'Request options: ' + JSON.stringify(requestOptions, null, 2) + '\n' + 239 | 'Response headers: ' + JSON.stringify(response.headers, null, 2) + '\n' + 240 | 'Make sure your network and proxy settings are correct.\n\n' + 241 | 'If you continue to have issues, please report this full log at ' + 242 | 'https://github.com/graingert/slimerjs') 243 | exit(1) 244 | } else { 245 | handleRequestError(error) 246 | } 247 | })).on('progress', function (state) { 248 | try { 249 | if (!bar) { 250 | bar = new progress(' [:bar] :percent', {total: state.size.total, width: 40}) 251 | } 252 | bar.curr = state.size.transferred 253 | bar.tick() 254 | } catch (e) { 255 | // It doesn't really matter if the progress bar doesn't update. 256 | } 257 | }) 258 | .on('error', handleRequestError) 259 | 260 | return deferred.promise 261 | } 262 | 263 | 264 | function extractDownload(filePath) { 265 | var deferred = kew.defer() 266 | // extract to a unique directory in case multiple processes are 267 | // installing and extracting at once 268 | var extractedPath = filePath + '-extract-' + Date.now() 269 | var options = {cwd: extractedPath} 270 | 271 | fs.mkdirsSync(extractedPath, '0777') 272 | // Make double sure we have 0777 permissions; some operating systems 273 | // default umask does not allow write by default. 274 | fs.chmodSync(extractedPath, '0777') 275 | 276 | if (filePath.substr(-4) === '.zip') { 277 | console.log('Extracting zip contents') 278 | extractZip(path.resolve(filePath), {dir: extractedPath}, function(err) { 279 | if (err) { 280 | console.error('Error extracting zip') 281 | deferred.reject(err) 282 | } else { 283 | deferred.resolve(extractedPath) 284 | } 285 | }) 286 | 287 | } else { 288 | console.log('Extracting tar contents (via spawned process)') 289 | cp.execFile('tar', ['jxf', path.resolve(filePath)], options, function (err) { 290 | if (err) { 291 | console.error('Error extracting archive') 292 | deferred.reject(err) 293 | } else { 294 | deferred.resolve(extractedPath) 295 | } 296 | }) 297 | } 298 | return deferred.promise 299 | } 300 | 301 | 302 | function copyIntoPlace(extractedPath, targetPath) { 303 | console.log('Removing', targetPath) 304 | return kew.nfcall(fs.remove, targetPath).then(function () { 305 | // Look for the extracted directory, so we can rename it. 306 | var files = fs.readdirSync(extractedPath) 307 | for (var i = 0; i < files.length; i++) { 308 | var file = path.join(extractedPath, files[i]) 309 | if (fs.statSync(file).isDirectory() && file.indexOf(helper.version) != -1) { 310 | console.log('Copying extracted folder', file, '->', targetPath) 311 | return kew.nfcall(fs.move, file, targetPath) 312 | } 313 | } 314 | 315 | console.log('Could not find extracted file', files) 316 | throw new Error('Could not find extracted file') 317 | }) 318 | } 319 | 320 | /** 321 | * Check to see if the binary in lib is OK to use. If successful, exit the process. 322 | */ 323 | function trySlimerjsInLib() { 324 | return kew.fcall(function () { 325 | return findValidSlimerJsBinary(path.resolve(__dirname, './lib/location.js')) 326 | }).then(function (binaryLocation) { 327 | if (binaryLocation) { 328 | console.log('SlimerJS is previously installed at', binaryLocation) 329 | exit(0) 330 | } 331 | }).fail(function () { 332 | // silently swallow any errors 333 | }) 334 | } 335 | 336 | /** 337 | * Check to see if the binary on PATH is OK to use. If successful, exit the process. 338 | */ 339 | function trySlimerjsOnPath() { 340 | if (getTargetPlatform() != process.platform || getTargetArch() != process.arch) { 341 | console.log('Building for target platform ' + getTargetPlatform() + '/' + getTargetArch() + 342 | '. Skipping PATH search') 343 | return kew.resolve(false) 344 | } 345 | 346 | return kew.nfcall(which, 'slimerjs') 347 | .then(function (result) { 348 | slimerPath = result 349 | console.log('Considering SlimerJS found at', slimerPath) 350 | 351 | // Horrible hack to avoid problems during global install. We check to see if 352 | // the file `which` found is our own bin script. 353 | if (slimerPath.indexOf(path.join('npm', 'slimerjs')) !== -1) { 354 | console.log('Looks like an `npm install -g` on windows; skipping installed version.') 355 | return 356 | } 357 | 358 | var contents = fs.readFileSync(slimerPath, 'utf8') 359 | if (/NPM_INSTALL_MARKER/.test(contents)) { 360 | console.log('Looks like an `npm install -g`') 361 | 362 | var slimerLibPath = path.resolve(fs.realpathSync(slimerPath), '../../lib/location') 363 | return findValidSlimerJsBinary(slimerLibPath) 364 | .then(function (binaryLocation) { 365 | if (binaryLocation) { 366 | writeLocationFile(binaryLocation) 367 | console.log('SlimerJS linked at', slimerLibPath) 368 | exit(0) 369 | } 370 | console.log('Could not link global install, skipping...') 371 | }) 372 | } else { 373 | return checkSlimerjsVersion(slimerPath).then(function (matches) { 374 | if (matches) { 375 | writeLocationFile(slimerPath) 376 | console.log('SlimerJS is already installed on PATH at', slimerPath) 377 | exit(0) 378 | } 379 | }) 380 | } 381 | }, function () { 382 | console.log('SlimerJS not found on PATH') 383 | }) 384 | .fail(function (err) { 385 | console.error('Error checking path, continuing', err) 386 | return false 387 | }) 388 | } 389 | 390 | /** 391 | * @return {?string} Get the download URL for slimerjs. 392 | * May return null if no download url exists. 393 | */ 394 | function getDownloadUrl() { 395 | var spec = getDownloadSpec() 396 | return spec && spec.url 397 | } 398 | 399 | /** 400 | * Download slimerjs, reusing the existing copy on disk if available. 401 | * Exits immediately if there is no binary to download. 402 | * @return {Promise.} The path to the downloaded file. 403 | */ 404 | function downloadSlimerjs() { 405 | var downloadSpec = getDownloadSpec() 406 | if (!downloadSpec) { 407 | console.error( 408 | 'Unexpected platform or architecture: ' + getTargetPlatform() + '/' + getTargetArch() + '\n' + 409 | 'It seems there is no binary available for your platform/architecture\n' + 410 | 'Try to install SlimerJS globally') 411 | exit(1) 412 | } 413 | 414 | var downloadUrl = downloadSpec.url 415 | var downloadedFile 416 | 417 | return kew.fcall(function () { 418 | // Can't use a global version so start a download. 419 | var tmpPath = findSuitableTempDirectory() 420 | var fileName = downloadUrl.split('/').pop() 421 | downloadedFile = path.join(tmpPath, fileName) 422 | 423 | if (fs.existsSync(downloadedFile)) { 424 | console.log('Download already available at', downloadedFile) 425 | return verifyChecksum(downloadedFile, downloadSpec.checksum) 426 | } 427 | return false 428 | }).then(function (verified) { 429 | if (verified) { 430 | return downloadedFile 431 | } 432 | 433 | // Start the install. 434 | console.log('Downloading', downloadUrl) 435 | console.log('Saving to', downloadedFile) 436 | return requestBinary(getRequestOptions(), downloadedFile) 437 | }) 438 | } 439 | --------------------------------------------------------------------------------