diff --git a/__tests__/SetupStore-test.js b/__tests__/SetupStore-test.js index e0a393bfb1..de3c2d4dd1 100644 --- a/__tests__/SetupStore-test.js +++ b/__tests__/SetupStore-test.js @@ -61,7 +61,9 @@ describe('SetupStore', function () { describe('init step', function () { virtualBox.vmdestroy.mockReturnValue(Promise.resolve()); - pit('inintializes the boot2docker vm if it does not exist', function () { + pit('inintializes the machine vm if it does not exist', function () { + util.home.mockReturnValue('home'); + machine.name.mockReturnValue('name'); machine.exists.mockReturnValue(Promise.resolve(false)); machine.create.mockReturnValue(Promise.resolve()); return setupStore.steps().init.run().then(() => { diff --git a/package.json b/package.json index d3214731de..352aa09b10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Kitematic", - "version": "0.5.2", + "version": "0.5.3", "author": "Kitematic", "description": "Simple Docker Container management for Mac OS X.", "homepage": "https://kitematic.com/", diff --git a/src/ContainerDetails.react.js b/src/ContainerDetails.react.js index 1063adf9c3..f101b603ba 100644 --- a/src/ContainerDetails.react.js +++ b/src/ContainerDetails.react.js @@ -28,7 +28,7 @@ var ContainerDetail = React.createClass({
- +
); } diff --git a/src/ContainerHome.react.js b/src/ContainerHome.react.js index 2d9a4f4091..45edefcf3d 100644 --- a/src/ContainerHome.react.js +++ b/src/ContainerHome.react.js @@ -8,7 +8,7 @@ var ContainerHomePreview = require('./ContainerHomePreview.react'); var ContainerHomeLogs = require('./ContainerHomeLogs.react'); var ContainerHomeFolders = require('./ContainerHomeFolders.react'); var ContainerUtil = require('./ContainerUtil'); -var webPorts = require('./Util').webPorts; +var util = require('./Util'); var resizeWindow = function () { $('.left .wrapper').height(window.innerHeight - 240); @@ -27,6 +27,9 @@ var ContainerHome = React.createClass({ handleResize: function () { resizeWindow(); }, + handleErrorClick: function () { + util.exec(['open', 'https://github.com/kitematic/kitematic/issues/new']); + }, componentWillReceiveProps: function () { this.init(); }, @@ -52,7 +55,7 @@ var ContainerHome = React.createClass({ this.setState({ ports: ports, defaultPort: _.find(_.keys(ports), function (port) { - return webPorts.indexOf(port) !== -1; + return util.webPorts.indexOf(port) !== -1; }), progress: ContainerStore.progress(this.getParams().name), blocked: ContainerStore.blocked(this.getParams().name) @@ -68,7 +71,14 @@ var ContainerHome = React.createClass({ }, render: function () { var body; - if (this.props.container && this.props.container.State.Downloading) { + if (this.props.error) { + body = ( +
+

There was a problem connecting to the Docker Engine.
Either the VirtualBox VM was removed, is not responding or Docker is not running inside of it. Try restarting Kitematic. If the issue persists, please file a ticket on our GitHub repo.

+ +
+ ); + } else if (this.props.container && this.props.container.State.Downloading) { if (this.state.progress) { body = (
diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 25fdce97b3..1a07b76131 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -7,17 +7,20 @@ var docker = require('./Docker'); var metrics = require('./Metrics'); var registry = require('./Registry'); var LogStore = require('./LogStore'); +var bugsnag = require('bugsnag-js'); var _placeholders = {}; var _containers = {}; var _progress = {}; var _muted = {}; var _blocked = {}; +var _error = null; var ContainerStore = assign(Object.create(EventEmitter.prototype), { CLIENT_CONTAINER_EVENT: 'client_container_event', SERVER_CONTAINER_EVENT: 'server_container_event', SERVER_PROGRESS_EVENT: 'server_progress_event', + SERVER_ERROR_EVENT: 'server_error_event', _pullImage: function (repository, tag, callback, progressCallback, blockedCallback) { registry.layers(repository, tag, (err, layerSizes) => { @@ -35,6 +38,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0); docker.client().pull(repository + ':' + tag, (err, stream) => { + if (err) { + callback(err); + return; + } stream.setEncoding('utf8'); var layerProgress = layersToDownload.reduce(function (r, layer) { @@ -50,7 +57,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var data = JSON.parse(str); console.log(data); - if (data.status === 'Pulling dependent layers') { + if (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1) { blockedCallback(); return; } @@ -153,7 +160,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { } } }, - _resumePulling: function () { + _resumePulling: function (callback) { var downloading = _.filter(_.values(this.containers()), function (container) { return container.State.Downloading; }); @@ -163,12 +170,20 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { downloading.forEach(function (container) { _progress[container.Name] = 99; docker.client().pull(container.Config.Image, function (err, stream) { + if (err) { + callback(err); + return; + } stream.setEncoding('utf8'); stream.on('data', function () {}); stream.on('end', function () { delete _placeholders[container.Name]; localStorage.setItem('store.placeholders', JSON.stringify(_placeholders)); - self._createContainer(container.Name, {Image: container.Config.Image}, function () { + self._createContainer(container.Name, {Image: container.Config.Image}, err => { + if (err) { + callback(err); + return; + } self.emit(self.SERVER_PROGRESS_EVENT, container.Name); self.emit(self.CLIENT_CONTAINER_EVENT, container.Name); }); @@ -176,13 +191,17 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }); }); }, - _startListeningToEvents: function () { - docker.client().getEvents(function (err, stream) { + _startListeningToEvents: function (callback) { + docker.client().getEvents((err, stream) => { + if (err) { + callback(err); + return; + } if (stream) { stream.setEncoding('utf8'); stream.on('data', this._dockerEvent.bind(this)); } - }.bind(this)); + }); }, _dockerEvent: function (json) { var data = JSON.parse(json); @@ -200,7 +219,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { this.emit(this.SERVER_CONTAINER_EVENT, data.status); } } else { - this.fetchContainer(data.id, function (err) { + this.fetchContainer(data.id, err => { if (err) { return; } @@ -209,13 +228,16 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { return; } this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status); - }.bind(this)); + }); } }, init: function (callback) { // TODO: Load cached data from db on loading - this.fetchAllContainers(function (err) { + this.fetchAllContainers(err => { if (err) { + _error = err; + this.emit(this.SERVER_ERROR_EVENT, err); + bugsnag.notify(err, 'Container Store failed to init', err); callback(err); return; } else { @@ -227,12 +249,20 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { localStorage.setItem('store.placeholders', JSON.stringify(_placeholders)); } this.emit(this.CLIENT_CONTAINER_EVENT); - this._resumePulling(); - this._startListeningToEvents(); - }.bind(this)); + this._resumePulling(err => { + _error = err; + this.emit(this.SERVER_ERROR_EVENT, err); + bugsnag.notify(err, 'Container Store failed to resume pulling', err); + }); + this._startListeningToEvents(err => { + _error = err; + this.emit(this.SERVER_ERROR_EVENT, err); + bugsnag.notify(err, 'Container Store failed to listen to events', err); + }); + }); }, fetchContainer: function (id, callback) { - docker.client().getContainer(id).inspect(function (err, container) { + docker.client().getContainer(id).inspect((err, container) => { if (err) { callback(err); } else { @@ -245,11 +275,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { _containers[container.Name] = container; callback(null, container); } - }.bind(this)); + }); }, fetchAllContainers: function (callback) { - var self = this; - docker.client().listContainers({all: true}, function (err, containers) { + docker.client().listContainers({all: true}, (err, containers) => { if (err) { callback(err); return; @@ -260,8 +289,8 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { delete _containers[name]; } }); - async.each(containers, function (container, callback) { - self.fetchContainer(container.Id, function (err) { + async.each(containers, (container, callback) => { + this.fetchContainer(container.Id, function (err) { callback(err); }); }, function (err) { @@ -289,14 +318,28 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { _muted[containerName] = true; _progress[containerName] = 0; - this._pullImage(repository, tag, () => { + this._pullImage(repository, tag, err => { + if (err) { + _error = err; + this.emit(this.SERVER_ERROR_EVENT, err); + bugsnag.notify(err, 'Container Store failed to create container', err); + return; + } + _error = null; _blocked[containerName] = false; if (!_placeholders[containerName]) { return; } delete _placeholders[containerName]; localStorage.setItem('store.placeholders', JSON.stringify(_placeholders)); - this._createContainer(containerName, {Image: imageName}, () => { + this._createContainer(containerName, {Image: imageName}, err => { + if (err) { + console.log(err); + _error = err; + this.emit(this.SERVER_ERROR_EVENT, err); + return; + } + _error = null; metrics.track('Container Finished Creating'); delete _progress[containerName]; _muted[containerName] = false; @@ -321,10 +364,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { LogStore.rename(name, data.name); } var fullData = assign(_containers[name], data); - this._createContainer(name, fullData, function (err) { + this._createContainer(name, fullData, function () { _muted[name] = false; this.emit(this.CLIENT_CONTAINER_EVENT, name); - callback(err); + callback(); }.bind(this)); }, rename: function (name, newName, callback) { @@ -417,6 +460,9 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }, blocked: function (name) { return !!_blocked[name]; + }, + error: function () { + return _error; } }); diff --git a/src/Containers.react.js b/src/Containers.react.js index eabe21e139..a3f935f4c8 100644 --- a/src/Containers.react.js +++ b/src/Containers.react.js @@ -20,11 +20,13 @@ var Containers = React.createClass({ containers: ContainerStore.containers(), sorted: ContainerStore.sorted(), updateAvailable: false, - currentButtonLabel: '' + currentButtonLabel: '', + error: ContainerStore.error() }; }, componentDidMount: function () { this.update(); + ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, this.updateError); ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); @@ -52,6 +54,11 @@ var Containers = React.createClass({ this.transitionTo('containers'); } }, + updateError: function (err) { + this.setState({ + error: err + }); + }, update: function (name, status) { this.setState({ containers: ContainerStore.containers(), @@ -175,7 +182,7 @@ var Containers = React.createClass({
- + ); diff --git a/src/ImageCard.react.js b/src/ImageCard.react.js index 7c83fa73be..b132aec25f 100644 --- a/src/ImageCard.react.js +++ b/src/ImageCard.react.js @@ -23,10 +23,7 @@ var ImageCard = React.createClass({ }, handleClick: function (name) { metrics.track('Created Container'); - ContainerStore.create(name, this.state.chosenTag, function (err) { - if (err) { - throw err; - } + ContainerStore.create(name, this.state.chosenTag, function () { $(document.body).find('.new-container-item').parent().fadeOut(); }.bind(this)); }, diff --git a/src/Main.js b/src/Main.js index 5a443bcff8..0853f811f5 100644 --- a/src/Main.js +++ b/src/Main.js @@ -6,13 +6,13 @@ var fs = require('fs'); var path = require('path'); var docker = require('./Docker'); var router = require('./Router'); -var machine = require('./DockerMachine'); var ContainerStore = require('./ContainerStore'); var SetupStore = require('./SetupStore'); var metrics = require('./Metrics'); var template = require('./MenuTemplate'); var util = require('./Util'); var Menu = remote.require('menu'); +var bugsnag = require('bugsnag-js'); window.addEventListener('resize', function () { fs.writeFileSync(path.join(util.supportDir(), 'size'), JSON.stringify({ @@ -38,7 +38,6 @@ if (process.env.NODE_ENV === 'development') { head.appendChild(script); } -var bugsnag = require('bugsnag-js'); bugsnag.apiKey = settingsjson.bugsnag; bugsnag.autoNotify = true; bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; @@ -81,18 +80,25 @@ setInterval(function () { }, 14400000); router.run(Handler => React.render(, document.body)); -SetupStore.run().then(machine.info).then(machine => { +SetupStore.setup().then(machine => { + console.log('setup complete'); + console.log(machine); docker.setup(machine.url, machine.name); Menu.setApplicationMenu(Menu.buildFromTemplate(template())); + ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, (err) => { + bugsnag.notify(err); + }); ContainerStore.init(function (err) { if (err) { + console.log(err); bugsnag.notify(err); } router.transitionTo('containers'); }); }).catch(err => { metrics.track('Setup Failed', { - step: SetupStore.step(), + step: 'catch', + message: err.message }); bugsnag.notify(err); console.log(err); diff --git a/src/Radial.react.js b/src/Radial.react.js index d54cac6b3b..cc826167cd 100644 --- a/src/Radial.react.js +++ b/src/Radial.react.js @@ -3,7 +3,7 @@ var React = require('react/addons'); var Radial = React.createClass({ render: function () { var percentage; - if ((this.props.progress !== null && this.props.progress !== undefined) && !this.props.spin) { + if ((this.props.progress !== null && this.props.progress !== undefined) && !this.props.spin && !this.props.error) { percentage = (
); diff --git a/src/Setup.react.js b/src/Setup.react.js index da7264785f..c684dffffa 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -6,13 +6,15 @@ var RetinaImage = require('react-retina-image'); var Header = require('./Header.react'); var Util = require('./Util'); var metrics = require('./Metrics'); +var machine = require('./DockerMachine'); var Setup = React.createClass({ mixins: [ Router.Navigation ], getInitialState: function () { return { progress: 0, - name: '' + name: '', + retrying: false }; }, componentWillMount: function () { @@ -28,10 +30,26 @@ var Setup = React.createClass({ SetupStore.removeListener(SetupStore.STEP_EVENT, this.update); SetupStore.removeListener(SetupStore.ERROR_EVENT, this.update); }, - handleRetry: function () { - metrics.track('Setup Retried'); + handleCancelRetry: function () { + metrics.track('Setup Retried', { + from: 'cancel' + }); SetupStore.retry(); }, + handleErrorRetry: function () { + this.setState({ + retrying: true + }); + metrics.track('Setup Retried', { + from: 'error' + }); + machine.stop().finally(() => { + this.setState({ + retrying: false + }); + SetupStore.retry(); + }); + }, handleOpenWebsite: function () { Util.exec(['open', 'https://www.virtualbox.org/wiki/Downloads']); }, @@ -110,21 +128,21 @@ var Setup = React.createClass({

We're Sorry!

There seems to have been an unexpected error with Kitematic:

{this.state.error}
{this.state.error.message}

+

); }, render: function () { - if (!SetupStore.step()) { - return false; - } if (this.state.cancelled) { return this.renderCancelled(); } else if (this.state.error) { return this.renderError(); - } else { + } else if (SetupStore.step()) { return this.renderStep(); + } else { + return false; } } }); diff --git a/src/SetupStore.js b/src/SetupStore.js index 11478e593d..b2b04cd7f6 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -10,6 +10,7 @@ var util = require('./Util'); var assign = require('object-assign'); var metrics = require('./Metrics'); var bugsnag = require('bugsnag-js'); +var rimraf = require('rimraf'); var _currentStep = null; var _error = null; @@ -59,26 +60,25 @@ var _steps = [{ message: 'To run Docker containers on your computer, Kitematic is starting a Linux virutal machine. This may take a minute...', totalPercent: 60, percent: 0, - seconds: 52, + seconds: 53, run: Promise.coroutine(function* (progressCallback) { setupUtil.simulateProgress(this.seconds, progressCallback); yield virtualBox.vmdestroy('kitematic-vm'); var exists = yield machine.exists(); - if (!exists) { - yield machine.create(); - return; - } else if ((yield machine.state()) === 'Error') { + if (!exists || (yield machine.state()) === 'Error') { try { yield machine.rm(); - } catch (err) {} - yield machine.create(); + yield machine.create(); + } catch (err) { + rimraf.sync(path.join(util.home(), '.docker', 'machine', 'machines', machine.name())); + yield machine.create(); + } return; } var isoversion = machine.isoversion(); var packagejson = util.packagejson(); if (!isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) { - console.log('upgrading'); yield machine.stop(); yield machine.upgrade(); } @@ -113,15 +113,21 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { error: function () { return _error; }, + setError: function (error) { + _error = error; + this.emit(this.ERROR_EVENT); + }, cancelled: function () { return _cancelled; }, retry: function () { _error = null; _cancelled = false; - _retryPromise.resolve(); + if (_retryPromise) { + _retryPromise.resolve(); + } }, - wait: function () { + pause: function () { _retryPromise = Promise.defer(); return _retryPromise.promise; }, @@ -141,7 +147,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { if (isoversion && setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) { this.steps().init.seconds = 33; } else if (exists && (yield machine.state()) !== 'Error') { - this.steps().init.seconds = 13; + this.steps().init.seconds = 23; } _requiredSteps = _steps.filter(function (step) { @@ -196,12 +202,29 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { _cancelled = true; this.emit(this.STEP_EVENT); } - yield this.wait(); + yield this.pause(); } } } - metrics.track('Finished Setup'); _currentStep = null; + return yield machine.info(); + }), + setup: Promise.coroutine(function * () { + while (true) { + var info = yield this.run(); + if (!info.url) { + metrics.track('Setup Failed', { + step: 'done', + message: 'Machine URL not set' + }); + bugsnag.notify('SetupError', 'Machine url was not set', machine); + SetupStore.setError('Could not reach the Docker Engine inside the VirtualBox VM'); + yield this.pause(); + } else { + metrics.track('Finished Setup'); + return info; + } + } }) }); diff --git a/src/Util.js b/src/Util.js index 949ac410c5..baa565bfca 100644 --- a/src/Util.js +++ b/src/Util.js @@ -9,7 +9,7 @@ module.exports = { return new Promise((resolve, reject) => { exec(args, options, (stderr, stdout, code) => { if (code) { - reject(stderr); + reject(stderr || args.join(' ').replace(this.home(), '') + 'returned non zero exit code'); } resolve(stdout); }); diff --git a/styles/right-panel.less b/styles/right-panel.less index 3afe086760..2d5af45524 100644 --- a/styles/right-panel.less +++ b/styles/right-panel.less @@ -122,7 +122,7 @@ .details-progress { margin: 20% auto 0; text-align: center; - width: 300px; + width: 400px; h2 { margin-bottom: 20px; } diff --git a/util/reset b/util/reset index 56eb096e45..8e97b804bf 100755 --- a/util/reset +++ b/util/reset @@ -8,4 +8,5 @@ pkill VBox rm -rf ~/Library/Application\ Support/Kitematic/ rm -rf ~/.docker rm -rf ~/Library/VirtualBox/ +rm -rf ~/Kitematic $DIR/VirtualBox_Uninstall.tool