0.5.3 setup stability, errors & retrying

This commit is contained in:
Jeffrey Morgan 2015-03-01 17:08:46 -05:00
Родитель bf0abc1d2c
Коммит 9b32a6a680
13 изменённых файлов: 169 добавлений и 60 удалений

Просмотреть файл

@ -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/",

Просмотреть файл

@ -28,7 +28,7 @@ var ContainerDetail = React.createClass({
<div className="details">
<ContainerDetailsHeader container={this.props.container}/>
<ContainerDetailsSubheader container={this.props.container} />
<Router.RouteHandler container={this.props.container}/>
<Router.RouteHandler container={this.props.container} error={this.props.error}/>
</div>
);
}

Просмотреть файл

@ -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 = (
<div className="details-progress">
<h3>There was a problem connecting to the Docker Engine.<br/>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 <a onClick={this.handleErrorClick}>file a ticket on our GitHub repo.</a></h3>
<Radial progress={100} error={true} thick={true} transparent={true}/>
</div>
);
} else if (this.props.container && this.props.container.State.Downloading) {
if (this.state.progress) {
body = (
<div className="details-progress">

Просмотреть файл

@ -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;
}
});

Просмотреть файл

@ -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({
<div className="sidebar-buttons-padding"></div>
</section>
</div>
<Router.RouteHandler container={container}/>
<Router.RouteHandler container={container} error={this.state.error}/>
</div>
</div>
);

Просмотреть файл

@ -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));
},

Просмотреть файл

@ -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(<Handler/>, 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);

Просмотреть файл

@ -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 = (
<div className="percentage"></div>
);

Просмотреть файл

@ -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({
<h1>We&#39;re Sorry!</h1>
<p>There seems to have been an unexpected error with Kitematic:</p>
<p className="error">{this.state.error}<br />{this.state.error.message}</p>
<p><button className="btn btn-action" disabled={this.state.retrying} onClick={this.handleErrorRetry}>Retry Setup</button></p>
</div>
</div>
</div>
);
},
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;
}
}
});

Просмотреть файл

@ -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,30 @@ 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();
console.log(info);
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;
}
}
})
});

Просмотреть файл

@ -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);
});

Просмотреть файл

@ -122,7 +122,7 @@
.details-progress {
margin: 20% auto 0;
text-align: center;
width: 300px;
width: 400px;
h2 {
margin-bottom: 20px;
}

Просмотреть файл

@ -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