diff --git a/__tests__/README.md b/__tests__/README.md new file mode 100644 index 0000000000..02f5488031 --- /dev/null +++ b/__tests__/README.md @@ -0,0 +1 @@ +### Manual tests diff --git a/__tests__/SetupStore-test.js b/__tests__/SetupStore-test.js index ee4fc5f071..2df2918ce1 100644 --- a/__tests__/SetupStore-test.js +++ b/__tests__/SetupStore-test.js @@ -13,7 +13,6 @@ describe('SetupStore', function () { virtualBox.installed.mockReturnValue(false); setupUtil.download.mockReturnValue(Promise.resolve()); return setupStore.steps().download.run().then(() => { - // TODO: make sure download was called with the right args expect(setupUtil.download).toBeCalled(); }); }); @@ -30,26 +29,38 @@ describe('SetupStore', function () { }); describe('install step', function () { + util.exec.mockReturnValue(Promise.resolve()); + util.copyBinariesCmd.mockReturnValue('copycmd'); + util.fixBinariesCmd.mockReturnValue('fixcmd'); + virtualBox.killall.mockReturnValue(Promise.resolve()); + setupUtil.installVirtualBoxCmd.mockReturnValue('installvb'); + setupUtil.macSudoCmd.mockImplementation(cmd => 'macsudo ' + cmd); + pit('installs virtualbox if it is not installed', function () { virtualBox.installed.mockReturnValue(false); - virtualBox.killall.mockReturnValue(Promise.resolve()); util.exec.mockReturnValue(Promise.resolve()); return setupStore.steps().install.run().then(() => { - // TODO: make sure that the right install command was executed - expect(util.exec).toBeCalled(); + expect(virtualBox.killall).toBeCalled(); + expect(util.exec).toBeCalledWith('macsudo copycmd && fixcmd && installvbcmd'); }); }); pit('installs virtualbox if it is installed but has an outdated version', function () { virtualBox.installed.mockReturnValue(true); virtualBox.version.mockReturnValue(Promise.resolve('4.3.16')); - virtualBox.killall.mockReturnValue(Promise.resolve()); setupUtil.compareVersions.mockReturnValue(-1); util.exec.mockReturnValue(Promise.resolve()); return setupStore.steps().install.run().then(() => { - // TODO: make sure the right install command was executed expect(virtualBox.killall).toBeCalled(); - expect(util.exec).toBeCalled(); + expect(util.exec).toBeCalledWith('macsudo copycmd && fixcmd && installvbcmd'); + }); + }); + + pit('only installs binaries if virtualbox is installed', function () { + virtualBox.installed.mockReturnValue(true); + setupUtil.compareVersions.mockReturnValue(0); + return setupStore.steps().install.run().then(() => { + expect(util.exec).toBeCalledWith('macsudo copycmd && fixcmd'); }); }); }); diff --git a/browser/main.js b/browser/main.js index 1a3a6a4852..04354c9407 100644 --- a/browser/main.js +++ b/browser/main.js @@ -16,6 +16,7 @@ try { process.env.NODE_PATH = __dirname + '/../node_modules'; process.env.RESOURCES_PATH = __dirname + '/../resources'; process.chdir(path.join(__dirname, '..')); +process.env.PATH = '/usr/local/bin:' + process.env.PATH; if (argv.integration) { process.env.TEST_TYPE = 'integration'; diff --git a/resources/cocoasudo b/resources/cocoasudo deleted file mode 100755 index ccf0bf8aa1..0000000000 Binary files a/resources/cocoasudo and /dev/null differ diff --git a/resources/macsudo b/resources/macsudo new file mode 100755 index 0000000000..a34962b716 Binary files /dev/null and b/resources/macsudo differ diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 1ec6b04b87..92fb1375aa 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -11,6 +11,7 @@ var ContainerUtil = require('./ContainerUtil'); var convert = new Convert(); var _recommended = []; +var _placeholders = {}; var _containers = {}; var _progress = {}; var _logs = {}; @@ -23,32 +24,8 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { SERVER_CONTAINER_EVENT: 'server_container_event', SERVER_PROGRESS_EVENT: 'server_progress_event', SERVER_LOGS_EVENT: 'server_logs_event', - _pullScratchImage: function (callback) { - var image = docker.client().getImage('scratch:latest'); - image.inspect(function (err, data) { - if (!data) { - docker.client().pull('scratch:latest', function (err, stream) { - if (err) { - callback(err); - return; - } - stream.setEncoding('utf8'); - stream.on('data', function () {}); - stream.on('end', function () { - callback(); - }); - }); - } else { - callback(); - } - }); - }, _pullImage: function (repository, tag, callback, progressCallback) { registry.layers(repository, tag, function (err, layerSizes) { - - // TODO: Support v2 registry API - // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs - // Use the per-layer pull progress % to update the total progress. docker.client().listImages({all: 1}, function(err, images) { var existingIds = new Set(images.map(function (image) { return image.Id.slice(0, 12); @@ -162,29 +139,14 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }); }, _createPlaceholderContainer: function (imageName, name, callback) { - var self = this; - this._pullScratchImage(function (err) { - if (err) { - callback(err); - return; + if (_placeholders[name]) { + delete _placeholders[name]; + } + _placeholders[name] = { + State: { } - docker.client().createContainer({ - Image: 'scratch:latest', - Tty: false, - Env: [ - 'KITEMATIC_DOWNLOADING=true', - 'KITEMATIC_DOWNLOADING_IMAGE=' + imageName - ], - Cmd: 'placeholder', - name: name - }, function (err) { - if (err) { - callback(err); - return; - } - self.fetchContainer(name, callback); - }); - }); + }; + return _placeholders[name]; }, _generateName: function (repository) { var base = _.last(repository.split('/')); @@ -386,28 +348,24 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var self = this; var imageName = repository + ':' + tag; var containerName = this._generateName(repository); - // Pull image - self._createPlaceholderContainer(imageName, containerName, function (err, container) { - if (err) { - callback(err); - return; - } - _containers[containerName] = container; - self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); - _muted[containerName] = true; - _progress[containerName] = 0; - self._pullImage(repository, tag, function () { - self._createContainer(containerName, {Image: imageName}, function () { - delete _progress[containerName]; - _muted[containerName] = false; - self.emit(self.CLIENT_CONTAINER_EVENT, containerName); - }); - }, function (progress) { - _progress[containerName] = progress; - self.emit(self.SERVER_PROGRESS_EVENT, containerName); + + // Create placeholder container + var container = this._createPlaceholderContainer(imageName, containerName); + _containers[containerName] = container; + self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); + _muted[containerName] = true; + _progress[containerName] = 0; + self._pullImage(repository, tag, function () { + self._createContainer(containerName, {Image: imageName}, function () { + delete _progress[containerName]; + _muted[containerName] = false; + self.emit(self.CLIENT_CONTAINER_EVENT, containerName); }); - callback(null, containerName); + }, function (progress) { + _progress[containerName] = progress; + self.emit(self.SERVER_PROGRESS_EVENT, containerName); }); + callback(null, containerName); }, updateContainer: function (name, data, callback) { _muted[name] = true; diff --git a/src/Setup.react.js b/src/Setup.react.js index bf8b430f64..ea48750a43 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -43,7 +43,7 @@ var Setup = React.createClass({ }, renderContents: function () { var img = 'virtualbox.png'; - if (SetupStore.step().name.indexOf('Boot2Docker') !== -1) { + if (SetupStore.step().name.indexOf('start') !== -1 || SetupStore.step().name.indexOf('init') !== -1) { img = 'boot2docker.png'; } return ( diff --git a/src/SetupStore.js b/src/SetupStore.js index d5af39b9d4..e94b39243c 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -1,6 +1,7 @@ var EventEmitter = require('events').EventEmitter; var _ = require('underscore'); var path = require('path'); +var fs = require('fs'); var Promise = require('bluebird'); var boot2docker = require('./Boot2Docker'); var virtualBox = require('./VirtualBox'); @@ -20,13 +21,12 @@ var _steps = [{ message: 'VirtualBox is being downloaded. Kitematic requires VirtualBox to run containers.', totalPercent: 35, percent: 0, - run: Promise.coroutine(function* (progressCallback) { + run: function (progressCallback) { var packagejson = util.packagejson(); - var virtualBoxFile = `https://github.com/kitematic/virtualbox/releases/download/${packagejson['virtualbox-version']}/${packagejson['virtualbox-filename']}`; - yield setupUtil.download(virtualBoxFile, path.join(util.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => { + return setupUtil.download(setupUtil.virtualBoxUrl(), path.join(util.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => { progressCallback(percent); }); - }) + } }, { name: 'install', title: 'Installing Docker & VirtualBox', @@ -34,16 +34,16 @@ var _steps = [{ totalPercent: 5, percent: 0, seconds: 5, - run: Promise.coroutine(function* () { + run: Promise.coroutine(function* (progressCallback) { var packagejson = util.packagejson(); - var base = util.copyBinariesCmd() + ' && ' + util.fixBinariesCmd(); + var cmd = util.copyBinariesCmd() + ' && ' + util.fixBinariesCmd(); if (!virtualBox.installed() || setupUtil.compareVersions(yield virtualBox.version(), packagejson['virtualbox-required-version']) < 0) { yield virtualBox.killall(); - base += ` && installer -pkg ${path.join(util.supportDir(), packagejson['virtualbox-filename'])} -target /`; + cmd += ' && ' + setupUtil.installVirtualBoxCmd(); } - var cmd = `${util.escapePath(path.join(util.resourceDir(), 'cocoasudo'))} --prompt="Kitematic requires administrative privileges to install VirtualBox." bash -c \"${base}\"`; try { - yield util.exec(cmd); + progressCallback(50); // TODO: detect when the installation has started so we can simulate progress + yield util.exec(setupUtil.macSudoCmd(cmd)); } catch (err) { throw null; } @@ -130,12 +130,16 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { _retryPromise = Promise.defer(); return _retryPromise.promise; }, - init: Promise.coroutine(function* () { + requiredSteps: Promise.coroutine(function* () { + if (_requiredSteps.length) { + return Promise.resolve(_requiredSteps); + } var packagejson = util.packagejson(); var isoversion = boot2docker.isoversion(); var required = {}; - required.download = !virtualBox.installed() || setupUtil.compareVersions(yield virtualBox.version(), packagejson['virtualbox-required-version']) < 0; - required.install = required.download || setupUtil.needsBinaryFix(); + var vboxfile = path.join(util.supportDir(), packagejson['virtualbox-filename']); + required.download = !virtualBox.installed() && (!fs.existsSync(vboxfile) || setupUtil.checksum(vboxfile) !== packagejson['virtualbox-checksum']); + required.install = !virtualBox.installed() || setupUtil.needsBinaryFix(); required.init = !(yield boot2docker.exists()) || !isoversion || setupUtil.compareVersions(isoversion, boot2docker.version()) < 0; required.start = required.init || (yield boot2docker.status()) !== 'running'; @@ -147,6 +151,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { _requiredSteps = _steps.filter(function (step) { return required[step.name]; }); + return Promise.resolve(_requiredSteps); }), updateBinaries: function () { if (setupUtil.needsBinaryFix()) { @@ -158,9 +163,9 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { return Promise.resolve(); }, run: Promise.coroutine(function* () { - yield this.init(); yield this.updateBinaries(); - for (let step of _requiredSteps) { + var steps = yield this.requiredSteps(); + for (let step of steps) { _currentStep = step; step.percent = 0; while (true) { @@ -176,7 +181,6 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { break; } catch (err) { if (err) { - console.log(err.stack); _error = err; this.emit(this.ERROR_EVENT); } else { diff --git a/src/SetupUtil.js b/src/SetupUtil.js index c18b0cab99..a2597bffd4 100644 --- a/src/SetupUtil.js +++ b/src/SetupUtil.js @@ -29,6 +29,17 @@ var SetupUtil = { this.checksum('/usr/local/bin/boot2docker') !== this.checksum(path.join(util.resourceDir(), 'boot2docker-' + packagejson['boot2docker-version'])) || this.checksum('/usr/local/bin/docker') !== this.checksum(path.join(util.resourceDir(), 'docker-' + packagejson['docker-version'])); }, + installVirtualBoxCmd: function () { + var packagejson = util.packagejson(); + return `installer -pkg ${util.escapePath(path.join(util.supportDir(), packagejson['virtualbox-filename']))} -target /`; + }, + virtualBoxUrl: function () { + var packagejson = util.packagejson(); + return `https://github.com/kitematic/virtualbox/releases/download/${packagejson['virtualbox-version']}/${packagejson['virtualbox-filename']}`; + }, + macSudoCmd: function (cmd) { + return `${util.escapePath(path.join(util.resourceDir(), 'macsudo'))} -p "Kitematic requires administrative privileges to install VirtualBox." sh -c \"${cmd}\"`; + }, simulateProgress: function (estimateSeconds, progress) { var times = _.range(0, estimateSeconds * 1000, 200); var timers = [];