diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000000..89d89340b9 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,29 @@ +{ + "curly": true, + "noempty": true, + "newcap": true, + "eqeqeq": true, + "eqnull": true, + "esnext": true, + "undef": true, + "unused": true, + "devel": true, + "node": true, + "browser": true, + "evil": false, + "latedef": true, + "nonew": true, + "trailing": true, + "immed": true, + "smarttabs": true, + "strict": false, + "quotmark": false, + "nonbsp": true, + "noempty": true, + "camelcase": false, + "jasmine": true, + "globals": { + "define": true + }, + "predef": [ "-Promise" ] +} diff --git a/browser/main.js b/browser/main.js index a7552922c3..9a92d90b3e 100644 --- a/browser/main.js +++ b/browser/main.js @@ -1,15 +1,10 @@ -var child_process = require('child_process'); -var net = require('net'); -var os = require('os'); +var app = require('app'); var fs = require('fs'); var path = require('path'); var exec = require('exec'); - var autoUpdater = require('auto-updater'); -var app = require('app'); var BrowserWindow = require('browser-window'); var ipc = require('ipc'); - var argv = require('minimist')(process.argv); var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); @@ -23,6 +18,9 @@ if (argv.integration) { process.env.TEST_TYPE = 'test'; } +app.commandLine.appendSwitch('js-flags', '--harmony'); + +var mainWindow = null; app.on('activate-with-no-open-windows', function () { if (mainWindow) { mainWindow.show(); @@ -30,7 +28,7 @@ app.on('activate-with-no-open-windows', function () { return false; }); -app.on('ready', function() { +app.on('ready', function () { mainWindow = new BrowserWindow({ width: 1000, height: 700, @@ -44,12 +42,12 @@ app.on('ready', function() { var saveVMOnQuit = false; if (argv.test) { - mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'tests/tests.html'))); + mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/tests.html'))); } else { mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/index.html'))); - app.on('will-quit', function (e) { + app.on('will-quit', function () { if (saveVMOnQuit) { - exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {}); + exec('VBoxManage controlvm boot2docker-vm savestate', function () {}); } }); } @@ -69,7 +67,7 @@ app.on('ready', function() { if (process.env.NODE_ENV !== 'development' && !argv.test) { autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion() + '&beta=' + settingsjson.beta); - autoUpdater.on('checking-for-update', function (e) { + autoUpdater.on('checking-for-update', function () { console.log('Checking for update...'); }); @@ -78,11 +76,12 @@ app.on('ready', function() { console.log(e); }); - autoUpdater.on('update-not-available', function (e) { + autoUpdater.on('update-not-available', function () { console.log('Update not available.'); }); autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) { + console.log(e, releaseNotes, releaseName, releaseDate, updateURL); console.log('Update downloaded.'); mainWindow.webContents.send('notify', 'window:update-available'); }); diff --git a/gulpfile.js b/gulpfile.js index b167e2b8fc..be42980fb4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,18 +5,17 @@ var fs = require('fs'); var gulp = require('gulp'); var gulpif = require('gulp-if'); var gutil = require('gulp-util'); -var http = require('http'); var less = require('gulp-less'); var livereload = require('gulp-livereload'); var plumber = require('gulp-plumber'); var react = require('gulp-react'); +var to5 = require('gulp-6to5'); var runSequence = require('run-sequence'); var shell = require('gulp-shell'); var sourcemaps = require('gulp-sourcemaps'); var packagejson = require('./package.json'); var dependencies = Object.keys(packagejson.dependencies); -var devDependencies = Object.keys(packagejson.devDependencies); var isBeta = process.argv.indexOf('--beta') !== -1; var options = { dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, @@ -35,11 +34,31 @@ gulp.task('js', function () { // emit the end event, to properly end the task this.emit('end'); })) + .pipe(sourcemaps.init()) .pipe(react()) + .pipe(to5({blacklist: ['regenerator']})) + .pipe(sourcemaps.write('.')) .pipe(gulp.dest((options.dev || options.test) ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) .pipe(gulpif(options.dev, livereload())); }); +gulp.task('tests', function () { + gulp.src('tests/*.js') + .pipe(plumber(function(error) { + gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message)); + // emit the end event, to properly end the task + this.emit('end'); + })) + .pipe(sourcemaps.init()) + .pipe(react()) + .pipe(to5()) + .pipe(sourcemaps.write('.')) + .pipe(gulp.dest('./build')); + + gulp.src('./tests/tests.html').pipe(gulp.dest('./build')); + gulp.src('./tests/jasmine-2.1.3/*').pipe(gulp.dest('./build/jasmine-2.1.3')); +}); + gulp.task('images', function() { return gulp.src('images/*') .pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build')) @@ -79,7 +98,7 @@ gulp.task('copy', function () { .pipe(gulpif(options.dev, livereload())); }); -gulp.task('dist', function (cb) { +gulp.task('dist', function () { var stream = gulp.src('').pipe(shell([ 'rm -Rf ./dist', 'mkdir -p ./dist/osx', @@ -146,7 +165,7 @@ gulp.task('release', function () { runSequence('download', 'dist', ['copy', 'images', 'js', 'styles'], 'sign', 'zip'); }); -gulp.task('test', ['download', 'copy', 'js'], function () { +gulp.task('test', ['download', 'copy', 'js', 'tests'], function () { var env = process.env; env.NODE_ENV = 'test'; if (options.integration) { diff --git a/index.html b/index.html index 86d233b8db..5d205af613 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,6 @@ Kitematic - + diff --git a/package.json b/package.json index 371080b511..ab5fe8e3b0 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "bugs": "https://github.com/kitematic/kitematic/issues", "scripts": { "start": "gulp", - "test": "gulp test --silent", - "test:integration": "gulp test --silent --integration", - "all-tests": "npm test && npm run integration-tests", + "test": "gulp test", + "test:integration": "gulp test --integration", + "test:all": "npm test && npm run test:integration", "release": "gulp release", "release:beta": "gulp release --beta", "preinstall": "./deps" @@ -33,17 +33,17 @@ ] }, "boot2docker-version": "1.4.1", - "atom-shell-version": "0.21.0", + "atom-shell-version": "0.21.1", "virtualbox-version": "4.3.20", "virtualbox-filename": "VirtualBox-4.3.20-96996-OSX.dmg", "virtualbox-required-version": "4.3.18", "dependencies": { "ansi-to-html": "0.2.0", "async": "^0.9.0", + "bluebird": "^2.9.6", "bugsnag-js": "git+https://git@github.com/bugsnag/bugsnag-js", "dockerode": "2.0.4", "exec": "0.1.2", - "jest-cli": "^0.2.1", "jquery": "^2.1.3", "minimist": "^1.1.0", "node-uuid": "1.4.1", @@ -55,15 +55,17 @@ "react-router": "^0.11.6", "request": "^2.51.0", "request-progress": "0.3.1", + "request-promise": "^0.3.3", "retina.js": "^1.1.0", - "underscore": "^1.7.0", - "rimraf": "^2.2.8" + "rimraf": "^2.2.8", + "underscore": "^1.7.0" }, "devDependencies": { "browserify": "^6.2.0", "ecstatic": "^0.5.8", "glob": "^4.0.6", "gulp": "^3.8.10", + "gulp-6to5": "^3.0.0", "gulp-atom": "0.0.5", "gulp-concat": "^2.3.4", "gulp-cssmin": "^0.1.6", diff --git a/src/Boot2Docker.js b/src/Boot2Docker.js index 669a3f8f9f..2187130c45 100644 --- a/src/Boot2Docker.js +++ b/src/Boot2Docker.js @@ -1,123 +1,87 @@ -var exec = require('exec'); var path = require('path'); -var fs = require('fs'); -var path = require('path'); -var async = require('async'); - -var cmdExec = function (cmd, callback) { - exec(cmd, function (stderr, stdout, code) { - if (code !== 0) { - callback('Exit code ' + code + ': ' + stderr); - } else { - callback(null, stdout); - } - }); -}; - -var homeDir = function () { - return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; -}; +var Promise = require('bluebird'); +var _ = require('underscore'); +var fs = Promise.promisifyAll(require('fs')); +var util = require('./Util'); var Boot2Docker = { version: function () { return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))['boot2docker-version']; }, - cliVersion: function (callback) { - cmdExec([Boot2Docker.command(), 'version'], function (err, out) { - if (err) { - callback(err); - return; - } - var match = out.match(/version: v(\d+\.\d+\.\d+)/); + cliversion: function () { + return util.exec([Boot2Docker.command(), 'version']).then(stdout => { + var match = stdout.match(/version: v(\d+\.\d+\.\d+)/); if (!match || match.length < 2) { - callback('Could not parse the boot2docker cli version.'); + return Promise.reject('Could not parse the boot2docker cli version.'); } else { - callback(null, match[1]); + return Promise.resolve(match[1]); } + }).catch(err => { + return Promise.reject(err); }); }, - isoVersion: function (callback) { - fs.readFile(path.join(homeDir(), '.boot2docker', 'boot2docker.iso'), 'utf8', function (err, data) { - if (err) { - callback(err); - return; - } + isoversion: function () { + return fs.readFileAsync(path.join(util.home(), '.boot2docker', 'boot2docker.iso'), 'utf8').then(data => { var match = data.match(/Boot2Docker-v(\d+\.\d+\.\d+)/); - if (!match) { - callback('Could not parse boot2docker iso version'); - return; + if (match) { + return Promise.resolve(match[1]); + } else { + return Promise.resolve(null); } - callback (null, match[1]); }); }, command: function () { return path.join(process.cwd(), 'resources', 'boot2docker-' + this.version()); }, - exists: function (callback) { - cmdExec([Boot2Docker.command(), 'info'], function (err, out) { - if (err) { - callback(null, false); - } else { - callback(null, true); - } + exists: function () { + return util.exec([Boot2Docker.command(), 'status']).then(() => { + return Promise.resolve(true); + }).catch(() => { + return Promise.resolve(false); }); }, - status: function (callback) { - cmdExec([Boot2Docker.command(), 'status'], function (err, out) { - if (err) { - callback(err); - return; - } - callback(null, out.trim()); + status: function () { + return util.exec([Boot2Docker.command(), 'status']).then(stdout => { + return Promise.resolve(stdout.trim()); }); }, - init: function (callback) { - cmdExec([Boot2Docker.command(), 'init'], callback); + init: function () { + return util.exec([Boot2Docker.command(), 'init']); }, - start: function (callback) { - cmdExec([Boot2Docker.command(), 'start'], callback); + start: function () { + return util.exec([Boot2Docker.command(), 'start']); }, - stop: function (callback) { - cmdExec([Boot2Docker.command(), 'stop'], callback); + stop: function () { + return util.exec([Boot2Docker.command(), 'stop']); }, - upgrade: function (callback) { - cmdExec([Boot2Docker.command(), 'upgrade'], callback); + upgrade: function () { + return util.exec([Boot2Docker.command(), 'upgrade']); }, - ip: function (callback) { - exec([Boot2Docker.command(), 'ip'], function (stderr, stdout, code) { - if (code) { - callback(stderr); - } else { - callback(null, stdout.trim().replace('\n', '')); - } + destroy: function () { + return util.exec([Boot2Docker.command(), 'destroy']); + }, + ip: function () { + return util.exec([Boot2Docker.command(), 'ip']).then(stdout => { + return Promise.resolve(stdout.trim().replace('\n', '')); }); }, - erase: function (callback) { - var VMFileLocation = path.join(homeDir(), 'VirtualBox\\ VMs/boot2docker-vm'); - cmdExec(['rm', '-rf', VMFileLocation], callback); + erase: function () { + return util.exec(['rm', '-rf', path.join(util.home(), 'VirtualBox\\ VMs/boot2docker-vm')]); }, - state: function (callback) { - cmdExec([Boot2Docker.command(), 'info'], function (err, out) { - if (err) { - callback(err); - return; - } + state: function () { + util.exec([Boot2Docker.command(), 'info']).then(stdout => { try { - var info = JSON.parse(out); - callback(null, info.State); - } catch (e) { - callback(e, null); + var info = JSON.parse(stdout); + return Promise.resolve(info.State); + } catch (err) { + return Promise.reject(err); } }); }, - disk: function (callback) { - cmdExec([Boot2Docker.command(), 'ssh', 'df'], function (err, out) { - if (err) { - callback(err); - return; - } + disk: function () { + return util.exec([Boot2Docker.command(), 'ssh', 'df']).then(stdout => { try { - var lines = out.split('\n'); + var lines = stdout.split('\n'); var dataline = _.find(lines, function (line) { return line.indexOf('/dev/sda1') !== -1; }); @@ -128,24 +92,20 @@ var Boot2Docker = { var usedGb = parseInt(tokens[2], 10) / 1000000; var totalGb = parseInt(tokens[3], 10) / 1000000; var percent = parseInt(tokens[4].replace('%', ''), 10); - callback(null, { + Promise.resolve({ used_gb: usedGb.toFixed(2), total_gb: totalGb.toFixed(2), percent: percent }); - } catch (error) { - callback(error, null); + } catch (err) { + return Promise.reject(err); } }); }, - memory: function (callback) { - cmdExec([Boot2Docker.command(), 'ssh', 'free -m'], function (err, out) { - if (err) { - callback(err); - return; - } + memory: function () { + return util.exec([this.command(), 'ssh', 'free -m']).then(stdout => { try { - var lines = out.split('\n'); + var lines = stdout.split('\n'); var dataline = _.find(lines, function (line) { return line.indexOf('-/+ buffers') !== -1; }); @@ -157,74 +117,44 @@ var Boot2Docker = { var freeGb = parseInt(tokens[3], 10) / 1000; var totalGb = usedGb + freeGb; var percent = Math.round(usedGb / totalGb * 100); - callback(null, { + return Promise.resolve({ used_gb: usedGb.toFixed(2), total_gb: totalGb.toFixed(2), free_gb: freeGb.toFixed(2), percent: percent }); - } catch (error) { - callback(error); + } catch (err) { + return Promise.reject(err); } }); }, - createScratchImage: function (callback) { - cmdExec([Boot2Docker.command(), 'ssh', 'tar cv --files-from /dev/null | docker import - scratch'], function (err, out) { - callback(err); - }); - }, - stats: function (callback) { - var self = this; - self.state(function (err, state) { - if (err) { - callback(err); - return; - } + stats: function () { + Boot2Docker.state().then(state => { if (state === 'poweroff') { - callback(null, {state: state}); - return; + return Promise.resolve({state: state}); } - self.memoryUsage(function (err, mem) { - if (err) { - callback(null, {state: state}); - return; - } - self.diskUsage(function (err, disk) { - if (err) { - callback(null, {state: state, memory: mem}); - return; - } - callback(null, { - state: state, - memory: mem, - disk: disk - }); + var memory = Boot2Docker.memory(); + var disk = Boot2Docker.disk(); + return Promise.all([memory, disk]).spread((memory, disk) => { + return Promise.resolve({ + state: state, + memory: memory, + disk: disk }); }); }); }, - sshKeyExists: function () { - return fs.existsSync(path.join(homeDir(), '.ssh', 'id_boot2docker')); + haskeys: function () { + return fs.existsSync(path.join(util.home(), '.ssh', 'id_boot2docker')); }, - - // Todo: move me to setup - waitWhileStatus: function (status, callback) { - var current = status; - async.whilst(function () { - return current === status; - }, function (callback) { - Boot2Docker.status(function (err, vmStatus) { - if (err) { - callback(err); - } else { - current = vmStatus.trim(); - callback(); - } - }); - }, function (err) { - callback(err); - }); - } + waitstatus: Promise.coroutine(function* () { + while (true) { + var current = yield Boot2Docker.status(); + if (status !== current.trim()) { + return; + } + } + }) }; module.exports = Boot2Docker; diff --git a/src/ContainerModal.react.js b/src/ContainerModal.react.js index 27d853dfe2..e2d8d8ebc5 100644 --- a/src/ContainerModal.react.js +++ b/src/ContainerModal.react.js @@ -206,6 +206,7 @@ var ContainerModal = React.createClass({ ); var tagData = self.state.tags[this.state.active]; + var tags; if (tagData) { var list = tagData.map(function (t) { return
  • {t.name}
  • ; diff --git a/src/Main.js b/src/Main.js index 3932675ff2..86644ab120 100644 --- a/src/Main.js +++ b/src/Main.js @@ -1,12 +1,6 @@ -var module = require('module'); -require.main.paths.splice(0, 0, process.env.NODE_PATH); - var remote = require('remote'); var app = remote.require('app'); -var ipc = require('ipc'); var React = require('react'); -var Router = require('react-router'); -var RetinaImage = require('react-retina-image'); var fs = require('fs'); var path = require('path'); var docker = require('./Docker'); @@ -14,55 +8,43 @@ var router = require('./router'); var boot2docker = require('./boot2docker'); var ContainerStore = require('./ContainerStore'); var SetupStore = require('./ContainerStore'); -var Menu = require('./Menu'); -var Route = Router.Route; -var NotFoundRoute = Router.NotFoundRoute; -var DefaultRoute = Router.DefaultRoute; -var Link = Router.Link; -var RouteHandler = Router.RouteHandler; var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); if (process.env.NODE_ENV === 'development') { + var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'http://localhost:35729/livereload.js'; - var head = document.getElementsByTagName('head')[0]; head.appendChild(script); -} else { - var bugsnag = require('bugsnag-js'); - bugsnag.apiKey = settingsjson.bugsnag; - bugsnag.autoNotify = true; - bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; - bugsnag.notifyReleaseStages = ['production']; - bugsnag.appVersion = app.getVersion(); } +var bugsnag = require('bugsnag-js'); +bugsnag.apiKey = settingsjson.bugsnag; +bugsnag.autoNotify = true; +bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; +bugsnag.notifyReleaseStages = ['production']; +bugsnag.appVersion = app.getVersion(); + +router.run(Handler => React.render(, document.body)); if (!window.location.hash.length || window.location.hash === '#/') { - router.run(function (Handler) { - React.render(, document.body); - }); - SetupStore.run(function (err) { - if (err) { - bugsnag.notify(err); - return; - } - boot2docker.ip(function (err, ip) { - if (err) console.log(err); - docker.setHost(ip); - ContainerStore.init(function (err) { - router.transitionTo('containers'); - }); - }); - }); -} else { - router.run(function (Handler) { - React.render(, document.body); - }); - boot2docker.ip(function (err, ip) { - if (err) console.log(err); + SetupStore.run().then(boot2docker.ip).then(ip => { docker.setHost(ip); ContainerStore.init(function (err) { - if (err) console.log(err); + if (err) { console.log(err); } + router.transitionTo('containers'); }); + }).catch(err => { + bugsnag.notify(err); + }); +} else { + console.log('Skipping installer.'); + router.transitionTo('containers'); + boot2docker.ip().then(ip => { + docker.setHost(ip); + ContainerStore.init(function (err) { + if (err) { console.log(err); } + }); + }).catch(err => { + bugsnag.notify(err); }); } diff --git a/src/Routes.js b/src/Routes.js index 92b9df7d3a..4641ad29d5 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -25,8 +25,7 @@ var routes = ( - - + ); diff --git a/src/Setup.react.js b/src/Setup.react.js index f9f3f7cbbb..7bb1ddba3b 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -1,11 +1,6 @@ var React = require('react/addons'); var Router = require('react-router'); var Radial = require('./Radial.react.js'); -var async = require('async'); -var assign = require('object-assign'); -var fs = require('fs'); -var path = require('path'); -var virtualbox = require('./Virtualbox'); var SetupStore = require('./SetupStore'); var RetinaImage = require('react-retina-image'); @@ -27,8 +22,8 @@ var Setup = React.createClass({ }, update: function () { this.setState({ - progress: SetupStore.stepProgress(), - step: SetupStore.stepName(), + progress: SetupStore.percent(), + step: SetupStore.step(), error: SetupStore.error() }); }, @@ -118,9 +113,9 @@ var Setup = React.createClass({ }, renderStep: function () { switch(this.state.step) { - case 'downloading_virtualbox': + case 'download_virtualbox': return this.renderDownloadingVirtualboxStep(); - case 'installing_virtualbox': + case 'install_virtualbox': return this.renderInstallingVirtualboxStep(); case 'cleanup_kitematic': return this.renderInitBoot2DockerStep(); @@ -133,21 +128,11 @@ var Setup = React.createClass({ } }, render: function () { - var radial; - if (this.state.progress) { - radial = ; - } else if (this.state.error) { - radial = ; - } else { - radial = ; - } - var step = this.renderStep(); - if (this.state.error) { return (
    - {radial} + ;

    Error: {this.state.error}

    ); diff --git a/src/SetupStore.js b/src/SetupStore.js index bdea036e71..34a340af65 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -1,210 +1,127 @@ var EventEmitter = require('events').EventEmitter; var assign = require('object-assign'); -var async = require('async'); var fs = require('fs'); var path = require('path'); -var exec = require('exec'); +var Promise = require('bluebird'); var boot2docker = require('./Boot2Docker'); var virtualbox = require('./Virtualbox'); var setupUtil = require('./SetupUtil'); +var util = require('./Util'); var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); +var _percent = 0; var _currentStep = null; -var _progress = 0; var _error = null; +var VIRTUALBOX_FILE = `http://download.virtualbox.org/virtualbox/${packagejson['virtualbox-version']}/${packagejson['virtualbox-filename']}`; +var SUDO_PROMPT = 'Kitematic requires administrative privileges to install VirtualBox and copy itself to the Applications folder.'; + var SetupStore = assign(EventEmitter.prototype, { PROGRESS_EVENT: 'setup_progress', STEP_EVENT: 'setup_step', ERROR_EVENT: 'setup_error', - downloadVirtualboxStep: { - _download: function (callback, progressCallback) { - setupUtil.virtualboxSHA256(packagejson['virtualbox-version'], packagejson['virtualbox-filename'], function (err, checksum) { - if (err) { - callback(err); - return; - } - var url = 'http://download.virtualbox.org/virtualbox/' + packagejson['virtualbox-version'] + '/' + packagejson['virtualbox-filename']; - var downloadPath = path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']); - setupUtil.download(url, downloadPath, checksum, function (err) { - callback(err); - }, function (progress) { - progressCallback(progress); - }); - }); - }, - run: function (callback, progressCallback) { - if (virtualbox.installed()) { - virtualbox.version(function (err, version) { - if (err) {callback(err); return;} - if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) < 0) { - this._download(callback, progressCallback); - } else { - callback(); - } - }); - } else { - this._download(callback, progressCallback); + downloadVirtualboxStep: Promise.coroutine(function* () { + if (virtualbox.installed()) { + var version = yield virtualbox.version(); + if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) >= 0) { + return; } - }, - name: 'downloading_virtualbox', - }, - installVirtualboxStep: { - _install: function (callback) { - console.log('attaching'); - exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), packagejson['virtualbox-filename'])], function (stderr, stdout, code) { - if (code) { - callback(stderr); - return; - } - console.log('Attached.'); - var iconPath = path.join(setupUtil.resourceDir(), 'kitematic.icns'); - setupUtil.isSudo(function (err, isSudo) { - console.log(isSudo); - sudoCmd = isSudo ? ['sudo'] : [path.join(setupUtil.resourceDir(), 'cocoasudo'), '--icon=' + iconPath, '--prompt=Kitematic requires administrative privileges to install VirtualBox and copy itself to the Applications folder.']; - sudoCmd.push.apply(sudoCmd, ['installer', '-pkg', '/Volumes/VirtualBox/VirtualBox.pkg', '-target', '/']); - exec(sudoCmd, function (stderr, stdout, code) { - console.log(stdout); - console.log('Ran installer.'); - if (code) { - console.log(stderr); - console.log(stdout); - callback('Could not install virtualbox.'); - } else { - exec(['hdiutil', 'detach', '/Volumes/VirtualBox'], function(stderr, stdout, code) { - console.log('detaching'); - if (code) { - callback(stderr); - } else { - callback(); - } - }); - } - }); - }); - }); - }, - run: function (callback) { - var self = this; - if (virtualbox.installed()) { - virtualbox.version(function (err, version) { - if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) < 0) { - virtualbox.kill(function (err) { - if (err) {callback(err); return;} - self._install(function(err) { - callback(err); - }); - }); - } else { - callback(); - } - }); - } else { - self._install(function(err) { - callback(err); - }); + } + var checksum = yield setupUtil.virtualboxSHA256(packagejson['virtualbox-version'], packagejson['virtualbox-filename']); + yield setupUtil.download(VIRTUALBOX_FILE, path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']), checksum, percent => { + _percent = percent; + SetupStore.emit(SetupStore.PROGRESS_EVENT); + }); + }), + installVirtualboxStep: Promise.coroutine(function* () { + if (virtualbox.installed()) { + var version = yield virtualbox.version(); + if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) >= 0) { + return; } - }, - name: 'installing_virtualbox', + yield virtualbox.kill(); + } + yield util.exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), packagejson['virtualbox-filename'])]); + var isSudo = yield setupUtil.isSudo(); + var iconPath = path.join(setupUtil.resourceDir(), 'kitematic.icns'); + var sudoCmd = isSudo ? ['sudo'] : [path.join(setupUtil.resourceDir(), 'cocoasudo'), '--icon=' + iconPath, `--prompt=${SUDO_PROMPT}`]; + sudoCmd.push.apply(sudoCmd, ['installer', '-pkg', '/Volumes/VirtualBox/VirtualBox.pkg', '-target', '/']); + yield util.exec(sudoCmd); + yield util.exec(['hdiutil', 'detach', '/Volumes/VirtualBox']); + }), + cleanupKitematicStep: function () { + return virtualbox.vmdestroy('kitematic-vm'); }, - cleanupKitematicStep: { - run: function (callback) { - virtualbox.vmdestroy('kitematic-vm', function (err, removed) { - if (err) { - console.log(err); - } - callback(); - }); - }, - name: 'cleanup_kitematic', + initBoot2DockerStep: Promise.coroutine(function* () { + var exists = yield boot2docker.exists(); + if (!exists) { + yield boot2docker.init(); + return; + } + + if (!boot2docker.haskeys()) { + throw new Error('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.'); + } + + var isoversion = yield boot2docker.isoversion(); + if (!isoversion || setupUtil.compareVersions(isoversion, boot2docker.version()) < 0) { + yield boot2docker.stop(); + yield boot2docker.upgrade(); + } + }), + startBoot2DockerStep: function () { + return boot2docker.waitstatus('saving').then(boot2docker.status).then(status => { + if (status !== 'running') { + return boot2docker.start(); + } + }); }, - initBoot2DockerStep: { - run: function (callback) { - boot2docker.exists(function (err, exists) { - if (err) { callback(err); return; } - if (!exists) { - boot2docker.init(function (err) { - callback(err); - }); - } else { - if (!boot2docker.sshKeyExists()) { - callback('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.'); - } else { - boot2docker.isoVersion(function (err, version) { - if (err || setupUtil.compareVersions(version, boot2docker.version()) < 0) { - boot2docker.stop(function(err) { - boot2docker.upgrade(function (err) { - callback(err); - }); - }); - } else { - callback(); - } - }); - } - } - }); - }, - name: 'init_boot2docker', - }, - startBoot2DockerStep: { - run: function (callback) { - boot2docker.waitWhileStatus('saving', function (err) { - boot2docker.status(function (err, status) { - if (err) {callback(err); return;} - if (status !== 'running') { - boot2docker.start(function (err) { - callback(err); - }); - } else { - callback(); - } - }); - }); - }, - name: 'start_boot2docker', - }, - stepName: function () { + step: function () { if (_currentStep) { - return _currentStep.name; + return _currentStep; } else { return ''; } }, - stepProgress: function () { - return _progress; + percent: function () { + return _percent; }, error: function () { return _error; }, - run: function (callback) { - var self = this; - var steps = [this.downloadVirtualboxStep, this.installVirtualboxStep, this.cleanupKitematicStep, this.initBoot2DockerStep, this.startBoot2DockerStep]; - async.eachSeries(steps, function (step, callback) { - _currentStep = step; - _progress = 0; - self.emit(self.STEP_EVENT); + run: Promise.coroutine(function* () { + var steps = [{ + name: 'download_virtualbox', + run: this.downloadVirtualboxStep + }, { + name: 'install_virtualbox', + run: this.installVirtualboxStep + }, { + name: 'cleanup_kitematic', + run: this.cleanupKitematicStep + }, { + name: 'init_boot2docker', + run: this.initBoot2DockerStep + }, { + name: 'start_boot2docker', + run: this.startBoot2DockerStep + }]; - step.run(function (err) { - if (err) { - callback(err); - } else { - callback(); - } - }, function (progress) { - _progress = progress; - self.emit(self.PROGRESS_EVENT, progress); - }); - }, function (err) { - _error = err; - if (err) { - self.emit(self.ERROR_EVENT); - callback(err); - } else { - callback(); + _error = null; + for (let step of steps) { + console.log(step.name); + _currentStep = step.name; + _percent = 0; + this.emit(this.STEP_EVENT); + try { + yield step.run(); + } catch (err) { + _error = err; + this.emit(this.ERROR_EVENT); + throw err; } - }); - } + } + }) }); module.exports = SetupStore; diff --git a/src/SetupUtil.js b/src/SetupUtil.js index d3b8d696e9..fa56f8652d 100644 --- a/src/SetupUtil.js +++ b/src/SetupUtil.js @@ -4,9 +4,11 @@ var path = require('path'); var crypto = require('crypto'); var fs = require('fs'); var exec = require('exec'); +var rp = require('request-promise'); +var Promise = require('bluebird'); var SetupUtil = { - supportDir: function (callback) { + supportDir: function () { var dirs = ['Library', 'Application\ Support', 'Kitematic']; var acc = process.env.HOME; dirs.forEach(function (d) { @@ -17,64 +19,53 @@ var SetupUtil = { }); return acc; }, - resourceDir: function (callback) { + resourceDir: function () { return process.env.RESOURCES_PATH; }, - isSudo: function (callback) { - exec(['sudo', '-n', '-u', 'root', 'true'], function (stderr, stdout, code) { - if (code) { - callback(stderr); - } else { - var isSudo = stderr.indexOf('a password is required') === -1; - callback(null, isSudo); - } + isSudo: function () { + return new Promise((resolve, reject) => { + exec(['sudo', '-n', '-u', 'root', 'true'], (stderr, stdout, code) => { + if (code) { + reject(stderr); + } + resolve(stderr.indexOf('a password is required') === -1); + }); }); }, - download: function (url, filename, checksum, callback, progressCallback) { - var doDownload = function () { - progress(request({ - uri: url, - rejectUnauthorized: false - }), { - throttle: 250 - }).on('progress', function (state) { - progressCallback(state.percent); - }).on('error', function (err) { - callback(err); - }).pipe(fs.createWriteStream(filename)).on('error', function (err) { - callback(err); - }).on('close', function (err) { - callback(err); - }); - }; + download: function (url, filename, checksum, percentCallback) { + return new Promise((resolve, reject) => { + if (fs.existsSync(filename)) { + var existingChecksum = crypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex'); + if (existingChecksum === checksum) { + resolve(); + } else { + fs.unlinkSync(filename); + } + } - // Compare checksum to see if it already exists first - if (fs.existsSync(filename)) { - var existingChecksum = crypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex'); - if (existingChecksum !== checksum) { - fs.unlinkSync(filename); - doDownload(); - } else { - callback(); - } - } else { - doDownload(); - } + progress(request({ uri: url, rejectUnauthorized: false }), { throttle: 250 }).on('progress', state => { + percentCallback(state.percent); + }).on('error', err => { + reject(err); + }).pipe(fs.createWriteStream(filename)).on('error', err => { + reject(err); + }).on('close', err => { + if (err) { + reject(err); + } + resolve(); + }); + }); }, - virtualboxSHA256: function (version, filename, callback) { - var checksumUrl = 'http://dlc-cdn.sun.com/virtualbox/' + version + '/SHA256SUMS'; - request(checksumUrl, function (error, response, body) { - if (error) { - callback(error); - return; - } - var checksums = body.split('\n').map(function (line) { + virtualboxSHA256: function (version, filename) { + return rp(`http://dlc-cdn.sun.com/virtualbox/${version}/SHA256SUMS`).then((body) => { + var checksums = body.split('\n').map(line => { return line.split(' *'); - }).reduce(function (obj, pair) { + }).reduce((obj, pair) => { obj[pair[1]] = pair[0]; return obj; }, {}); - callback(null, checksums[filename]); + return Promise.resolve(checksums[filename]); }); }, compareVersions: function (v1, v2, options) { diff --git a/src/Util.js b/src/Util.js new file mode 100644 index 0000000000..b0144372e9 --- /dev/null +++ b/src/Util.js @@ -0,0 +1,18 @@ +var exec = require('exec'); +var Promise = require('bluebird'); + +module.exports = { + exec: function (args) { + return new Promise((resolve, reject) => { + exec(args, (stderr, stdout, code) => { + if (code) { + reject(stderr); + } + resolve(stdout); + }); + }); + }, + home: function () { + return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; + } +}; diff --git a/src/Virtualbox.js b/src/Virtualbox.js index e224764bf5..e665af8cd8 100644 --- a/src/Virtualbox.js +++ b/src/Virtualbox.js @@ -1,7 +1,6 @@ var fs = require('fs'); -var exec = require('exec'); -var path = require('path'); -var async = require('async'); +var util = require('./Util'); +var Promise = require('bluebird'); var VirtualBox = { command: function () { @@ -10,85 +9,57 @@ var VirtualBox = { installed: function () { return fs.existsSync('/usr/bin/VBoxManage') && fs.existsSync('/Applications/VirtualBox.app/Contents/MacOS/VirtualBox'); }, - version: function (callback) { + version: function () { if (!this.installed()) { - callback('VirtualBox not installed.'); - return; + return Promise.reject('VirtualBox not installed.'); } - exec([this.command(), '-v'], function (stderr, stdout, code) { - if (code) { - callback(stderr); - return; - } - // Output is x.x.xryyyyyy - var match = stdout.match(/(\d+\.\d+\.\d+).*/); - if (!match || match.length < 2) { - callback('VBoxManage -v output format not recognized.'); - return; - } - callback(null, match[1]); + return new Promise((resolve, reject) => { + util.exec([this.command(), '-v']).then(stdout => { + var match = stdout.match(/(\d+\.\d+\.\d+).*/); + if (!match || match.length < 2) { + reject('VBoxManage -v output format not recognized.'); + } + resolve(match[1]); + }).catch(reject); }); }, - poweroff: function (callback) { + poweroffall: function () { if (!this.installed()) { - callback('VirtualBox not installed.'); - return; + return Promise.reject('VirtualBox not installed.'); } - exec(this.command() + ' list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} ' + this.command() + ' controlvm {} acpipowerbutton', function (stderr, stdout, code) { - if (code) { - callback(stderr); - } else { - callback(); - } + return util.exec(this.command() + ' list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} ' + this.command() + ' controlvm {} poweroff'); + }, + kill: function () { + if (!this.installed()) { + return Promise.reject('VirtualBox not installed.'); + } + return this.poweroffall().then(() => { + return util.exec(['pkill', 'VirtualBox']); + }).then(() => { + return util.exec(['pkill', 'VBox']); }); }, - kill: function (callback) { - this.poweroff(function (err) { - if (err) {callback(err); return;} - exec('pkill VirtualBox', function (stderr, stdout, code) { - if (code) {callback(stderr); return;} - exec('pkill VBox', function (stderr, stdout, code) { - if (code) {callback(stderr); return;} - callback(); - }); - }); - }); - }, - vmstate: function (name, callback) { - exec(this.command() + ' showvminfo ' + name + ' --machinereadable', function (stderr, stdout, code) { - if (code) { callback(stderr); return; } - var match = stdout.match(/VMState="(\w+)"/); - if (!match) { - callback('Could not parse VMState'); - return; - } - callback(null, match[1]); - }); - }, - vmdestroy: function (name, callback) { - var self = this; - this.vmstate(name, function (err, state) { - // No VM found - if (err) { callback(null, false); return; } - exec('/usr/bin/VBoxManage controlvm ' + name + ' acpipowerbutton', function (stderr, stdout, code) { - if (code) { callback(stderr, false); return; } - var state = null; - - async.until(function () { - return state === 'poweroff'; - }, function (callback) { - self.vmstate(name, function (err, newState) { - if (err) { callback(err); return; } - state = newState; - setTimeout(callback, 250); - }); - }, function (err) { - exec('/usr/bin/VBoxManage unregistervm ' + name + ' --delete', function (stderr, stdout, code) { - if (code) { callback(err); return; } - callback(); - }); - }); + vmstate: function (name) { + return new Promise((resolve, reject) => { + util.exec([this.command(), 'showvminfo', name, '--machinereadable']).then(stdout => { + var match = stdout.match(/VMState="(\w+)"/); + if (!match) { + reject('Could not parse VMState'); + } + resolve(match[1]); + }).catch(reject); + }); + }, + vmdestroy: function (name) { + if (!this.installed()) { + throw Promise.reject('VirtualBox not installed.'); + } + return util.exec([this.command(), 'controlvm', name, 'poweroff']).then(() => { + return util.exec([this.command(), 'unregistervm', name, '--delete']).then(() => { + return true; }); + }).catch(() => { + return false; }); } }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000000..7e8c90d9d2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +require.main.paths.splice(0, 0, process.env.NODE_PATH); +require('./Main'); diff --git a/tests/Boot2Docker-integration.js b/tests/Boot2Docker-integration.js new file mode 100644 index 0000000000..c5ef5c5bd8 --- /dev/null +++ b/tests/Boot2Docker-integration.js @@ -0,0 +1,57 @@ +var boot2docker = require('../build/Boot2Docker'); +var path = require('path'); +var fs = require('fs'); +var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); + +describe('Boot2Docker', () => { + it('cli version is correct', done => { + boot2docker.cliversion().then(version => { + expect(version).toBe(packagejson['boot2docker-version']); + done(); + }); + }); + + describe('with an existing & running boot2docker vm', () => { + beforeAll(done => { + boot2docker.init().then(boot2docker.start).then(() => { + done(); + }); + }); + + it('creates a vm', done => { + boot2docker.exists().then(exists => { + expect(exists).toBe(true); + done(); + }); + }); + + it('detects the correct state of running vm', done => { + boot2docker.status().then(status => { + expect(status).toBe('running'); + done(); + }); + }); + + it('detects ssh keys', () => { + expect(boot2docker.haskeys()).toBe(true); + }); + + it('receives an ip address from the vm', done => { + boot2docker.ip().then(ip => { + expect(ip).toMatch(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/); + done(); + }); + }); + + it('reads a version from the boot2docker iso file', done => { + boot2docker.isoversion().then(version => { + expect(version).toMatch(/\d+\.\d+\.\d+/); + done(); + }); + }); + + afterAll(done => { + boot2docker.destroy().finally(done); + }); + }); +}); diff --git a/tests/SetupStore-integration.js b/tests/SetupStore-integration.js index 2dde66b446..a26c089e7c 100644 --- a/tests/SetupStore-integration.js +++ b/tests/SetupStore-integration.js @@ -3,57 +3,12 @@ var SetupStore = require('../build/SetupStore'); var setupUtil = require('../build/SetupUtil'); var path = require('path'); var fs = require('fs'); -var child_process = require('child_process'); -var exec = require('exec'); -var rimraf = require('rimraf'); +var Promise = require('bluebird'); var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000; // 5 minutes +jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000; // 5 minutes for integration tests describe('Setup', function () { - describe('without virtualbox installed or downloaded', function () { - var virtualboxFile = path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']); - beforeEach(function () { - if (fs.existsSync(virtualboxFile)) { - fs.unlinkSync(virtualboxFile); - } - spyOn(virtualbox, 'installed').and.returnValue(false); - }); - - it('downloads virtualbox from the official website', function (done) { - SetupStore.downloadVirtualboxStep.run(function (err) { - expect(err).toBeFalsy(); - expect(fs.existsSync(virtualboxFile)).toBe(true); - done(); - }, function (progress) { - - }); - }); - }); - - describe('with virtualbox downloaded but not installed', function () { - beforeEach(function (done) { - // 5 minute timeout per test - - SetupStore.downloadVirtualboxStep.run(function (err) { - if (virtualbox.installed()) { - virtualbox.kill(function (callback) { - done(); - }); - } else { - done(); - } - }, function (progress) {}); - }); - - it('does install virtualbox', function (done) { - SetupStore.installVirtualboxStep.run(function (err) { - expect(err).toBeFalsy(); - expect(fs.existsSync(virtualbox.command())).toBe(true); - done(); - }); - }); - }); describe('with virtualbox installed', function () { @@ -96,4 +51,41 @@ describe('Setup', function () { }); }); + /*describe('with virtualbox downloaded', function () { + beforeEach(function (done) { + Promise.coroutine(SetupStore.downloadVirtualboxStep)().finally(function () { + if (virtualbox.installed()) { + virtualbox.kill().finally(function () { + done(); + }); + } else { + done(); + } + }); + }); + + it('install virtualbox succeeds', function (done) { + Promise.coroutine(SetupStore.installVirtualboxStep)().finally(function () { + expect(virtualbox.installed()).toBe(true); + done(); + }); + }); + });*/ + + /*describe('without virtualbox installed or downloaded', function () { + var virtualboxFile = path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']); + beforeEach(function () { + if (fs.existsSync(virtualboxFile)) { + fs.unlinkSync(virtualboxFile); + } + spyOn(virtualbox, 'installed').and.returnValue(false); + }); + + it('downloads virtualbox from the official website', function (done) { + Promise.coroutine(SetupStore.downloadVirtualboxStep)().finally(function () { + expect(fs.existsSync(virtualboxFile)).toBe(true); + done(); + }); + }); + });*/ }); diff --git a/tests/SetupUtil-integration.js b/tests/SetupUtil-integration.js index b56ef2e2e0..ef1a4bbbdf 100644 --- a/tests/SetupUtil-integration.js +++ b/tests/SetupUtil-integration.js @@ -2,7 +2,7 @@ var setupUtil = require('../build/SetupUtil'); describe('SetupUtils', function() { it('returns live sha256 checksum for a given virtualbox version & filename', function (done) { - setupUtil.virtualboxSHA256('4.3.20', 'VirtualBox-4.3.20-96996-OSX.dmg', function (err, checksum) { + setupUtil.virtualboxSHA256('4.3.20', 'VirtualBox-4.3.20-96996-OSX.dmg').then(function (checksum) { expect(checksum).toBe('744e77119a640a5974160213c9912568a3d88dbd06a2fc6b6970070941732705'); done(); }); diff --git a/tests/Virtualbox-integration.js b/tests/Virtualbox-integration.js new file mode 100644 index 0000000000..5317c7bd09 --- /dev/null +++ b/tests/Virtualbox-integration.js @@ -0,0 +1,56 @@ +var virtualbox = require('../build/Virtualbox'); +var util = require('../build/Util'); + +describe('Virtualbox', function () { + beforeAll(function () { + // Make sure VirtualBox is installed + }); + + describe('with a running VM', function () { + beforeEach(function (done) { + return util.exec([virtualbox.command(), 'createvm', '--name', 'km-test', '--register']).finally(function () { + return util.exec([virtualbox.command(), 'startvm', 'km-test', '--type', 'headless']); + }).then(function() { + done(); + }).catch(function () { + done(); + }); + }); + + it('powers off all vms', function (done) { + virtualbox.poweroffall().then(function () { + return virtualbox.vmstate('km-test'); + }).then(function (state) { + expect(state).toBe('poweroff'); + done(); + }).catch(function (err) { + expect(err).toBeFalsy(); + done(); + }); + }); + + it('destroys a vm', function (done) { + virtualbox.vmdestroy('km-test').then(function () { + return util.exec([virtualbox.command(), 'showvminfo', 'km-test']).then(function () { + done(); + }).catch(function (err) { + expect(err).toBeTruthy(); + done(); + }); + }).catch(function (err) { + console.log(err); + done(); + }); + }); + + afterEach(function (done) { + util.exec([virtualbox.command(), 'controlvm', 'km-test', 'poweroff']).finally(function () { + return util.exec([virtualbox.command(), 'unregistervm', 'km-test', '--delete']); + }).then(function () { + done(); + }).catch(function () { + done(); + }); + }); + }); +}); diff --git a/tests/Virtualbox-test.js b/tests/Virtualbox-test.js new file mode 100644 index 0000000000..59ad644bd5 --- /dev/null +++ b/tests/Virtualbox-test.js @@ -0,0 +1,21 @@ +var virtualbox = require('../build/Virtualbox'); +var util = require('../build/Util'); +var Promise = require('bluebird'); + +describe('Virtualbox', function () { + it('returns the right command', function () { + expect(virtualbox.command()).toBe('/usr/bin/VBoxManage'); + }); + + describe('version 4.3.20r96996', function () { + beforeEach(function () { + spyOn(util, 'exec').and.returnValue(Promise.resolve('4.3.20r96996')); + }); + it('correctly parses virtualbox version', function (done) { + virtualbox.version().then(function (version) { + expect(version).toBe('4.3.20'); + done(); + }); + }); + }); +}); diff --git a/tests/tests.js b/tests/tests.js index e21261d882..495ca8d0fc 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -7,6 +7,7 @@ var app = require('remote').require('app'); jasmine.getEnv().addReporter(new consoleReporter.ConsoleReporter()({ showColors: true, timer: new jasmine.Timer(), + verbose: true, print: function() { process.stdout.write.apply(process.stdout, arguments); },