diff --git a/.travis.yml b/.travis.yml
index 20fd86b6a5..fed1f6b39e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,7 @@
language: node_js
node_js:
- 0.10
+cache:
+ directories:
+ - node_modules
+ - resources
diff --git a/__tests__/SetupStore-test.js b/__tests__/SetupStore-test.js
index bc44bb4bae..ee4fc5f071 100644
--- a/__tests__/SetupStore-test.js
+++ b/__tests__/SetupStore-test.js
@@ -12,7 +12,7 @@ describe('SetupStore', function () {
pit('downloads virtualbox if it is not installed', function () {
virtualBox.installed.mockReturnValue(false);
setupUtil.download.mockReturnValue(Promise.resolve());
- return setupStore.steps().downloadVirtualBox.run().then(() => {
+ return setupStore.steps().download.run().then(() => {
// TODO: make sure download was called with the right args
expect(setupUtil.download).toBeCalled();
});
@@ -23,30 +23,18 @@ describe('SetupStore', function () {
virtualBox.version.mockReturnValue(Promise.resolve('4.3.16'));
setupUtil.compareVersions.mockReturnValue(-1);
setupUtil.download.mockReturnValue(Promise.resolve());
- return setupStore.steps().downloadVirtualBox.run().then(() => {
+ return setupStore.steps().download.run().then(() => {
expect(setupUtil.download).toBeCalled();
});
});
-
- pit('skips download if virtualbox is already installed', function () {
- virtualBox.installed.mockReturnValue(true);
- virtualBox.version.mockReturnValue(Promise.resolve('4.3.20'));
- setupUtil.download.mockClear();
- setupUtil.download.mockReturnValue(Promise.resolve());
- setupUtil.compareVersions.mockReturnValue(1);
- return setupStore.steps().downloadVirtualBox.run().then(() => {
- expect(setupUtil.download).not.toBeCalled();
- });
- });
});
describe('install step', function () {
pit('installs virtualbox if it is not installed', function () {
virtualBox.installed.mockReturnValue(false);
virtualBox.killall.mockReturnValue(Promise.resolve());
- setupUtil.isSudo.mockReturnValue(Promise.resolve(false));
util.exec.mockReturnValue(Promise.resolve());
- return setupStore.steps().installVirtualBox.run().then(() => {
+ return setupStore.steps().install.run().then(() => {
// TODO: make sure that the right install command was executed
expect(util.exec).toBeCalled();
});
@@ -56,29 +44,14 @@ describe('SetupStore', function () {
virtualBox.installed.mockReturnValue(true);
virtualBox.version.mockReturnValue(Promise.resolve('4.3.16'));
virtualBox.killall.mockReturnValue(Promise.resolve());
- setupUtil.isSudo.mockReturnValue(Promise.resolve(false));
setupUtil.compareVersions.mockReturnValue(-1);
util.exec.mockReturnValue(Promise.resolve());
- return setupStore.steps().installVirtualBox.run().then(() => {
+ return setupStore.steps().install.run().then(() => {
// TODO: make sure the right install command was executed
expect(virtualBox.killall).toBeCalled();
expect(util.exec).toBeCalled();
});
});
-
- pit('skips install if virtualbox is already installed', function () {
- virtualBox.installed.mockReturnValue(true);
- virtualBox.version.mockReturnValue(Promise.resolve('4.3.20'));
- setupUtil.isSudo.mockReturnValue(Promise.resolve(false));
- setupUtil.compareVersions.mockReturnValue(-1);
- util.exec.mockReturnValue(Promise.resolve());
- return setupStore.steps().installVirtualBox.run().then(() => {
- virtualBox.killall.mockClear();
- util.exec.mockClear();
- expect(virtualBox.killall).not.toBeCalled();
- expect(util.exec).not.toBeCalled();
- });
- });
});
describe('init step', function () {
@@ -86,7 +59,7 @@ describe('SetupStore', function () {
pit('inintializes the boot2docker vm if it does not exist', function () {
boot2docker.exists.mockReturnValue(Promise.resolve(false));
boot2docker.init.mockReturnValue(Promise.resolve());
- return setupStore.steps().initBoot2Docker.run().then(() => {
+ return setupStore.steps().init.run().then(() => {
expect(boot2docker.init).toBeCalled();
});
});
@@ -98,7 +71,7 @@ describe('SetupStore', function () {
boot2docker.stop.mockReturnValue(Promise.resolve());
boot2docker.upgrade.mockReturnValue(Promise.resolve());
setupUtil.compareVersions.mockReturnValue(-1);
- return setupStore.steps().initBoot2Docker.run().then(() => {
+ return setupStore.steps().init.run().then(() => {
boot2docker.init.mockClear();
expect(boot2docker.init).not.toBeCalled();
expect(boot2docker.upgrade).toBeCalled();
@@ -111,7 +84,7 @@ describe('SetupStore', function () {
boot2docker.status.mockReturnValue(false);
boot2docker.waitstatus.mockReturnValue(Promise.resolve());
boot2docker.start.mockReturnValue(Promise.resolve());
- return setupStore.steps().startBoot2Docker.run().then(() => {
+ return setupStore.steps().start.run().then(() => {
expect(boot2docker.start).toBeCalled();
});
});
diff --git a/src/ContainerStore.js b/src/ContainerStore.js
index 865ab765c6..1ec6b04b87 100644
--- a/src/ContainerStore.js
+++ b/src/ContainerStore.js
@@ -17,7 +17,7 @@ var _logs = {};
var _streams = {};
var _muted = {};
-var ContainerStore = assign(EventEmitter.prototype, {
+var ContainerStore = assign(Object.create(EventEmitter.prototype), {
CLIENT_CONTAINER_EVENT: 'client_container_event',
CLIENT_RECOMMENDED_EVENT: 'client_recommended_event',
SERVER_CONTAINER_EVENT: 'server_container_event',
diff --git a/src/Main.js b/src/Main.js
index 87919ac609..69beea6499 100644
--- a/src/Main.js
+++ b/src/Main.js
@@ -8,8 +8,10 @@ var docker = require('./Docker');
var router = require('./router');
var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore');
-var SetupStore = require('./ContainerStore');
+var SetupStore = require('./SetupStore');
var settingsjson;
+var Menu = require('./Menu');
+
try {
settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
} catch (err) {
@@ -34,6 +36,7 @@ bugsnag.appVersion = app.getVersion();
router.run(Handler => React.render(, document.body));
if (!window.location.hash.length || window.location.hash === '#/') {
SetupStore.run().then(boot2docker.ip).then(ip => {
+ console.log(ip);
docker.setHost(ip);
ContainerStore.init(function (err) {
if (err) { console.log(err); }
diff --git a/src/Menu.js b/src/Menu.js
index 7f1a85569e..2249087873 100644
--- a/src/Menu.js
+++ b/src/Menu.js
@@ -1,8 +1,11 @@
var remote = require('remote');
var app = remote.require('app');
+var path = require('path');
+var docker = require('./Docker');
var Menu = remote.require('menu');
var BrowserWindow = remote.require('browser-window');
var router = require('./Router');
+var util = require('./Util');
// main.js
var template = [
@@ -59,6 +62,28 @@ var template = [
},
]
},
+{
+ label: 'File',
+ submenu: [
+ {
+ label: 'New Container',
+ accelerator: 'Command+N',
+ selector: 'undo:'
+ },
+ {
+ type: 'separator'
+ },
+ {
+ label: 'Open Docker Terminal',
+ accelerator: 'Command+Shift+T',
+ click: function() {
+ var terminal = path.join(process.cwd(), 'resources', 'terminal');
+ var cmd = [terminal, `DOCKER_HOST=${'tcp://' + docker.host + ':2376'} DOCKER_CERT_PATH=${path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm')} DOCKER_TLS_VERIFY=1 $SHELL`];
+ util.exec(cmd).then(() => {});
+ }
+ },
+ ]
+},
{
label: 'Edit',
submenu: [
diff --git a/src/Setup.react.js b/src/Setup.react.js
index b94e7766dd..bf8b430f64 100644
--- a/src/Setup.react.js
+++ b/src/Setup.react.js
@@ -83,7 +83,7 @@ var Setup = React.createClass({
Installation Cancelled
Couldn't Install VirtualBox
-
Kitematic did not receive the administrative privileges required to install VirtualBox.
+
Kitematic did not receive the administrative privileges required to install VirtualBox & Docker.
Please retry or download & install VirutalBox manually from the official Oracle website.
diff --git a/src/SetupStore.js b/src/SetupStore.js
index 2489fcb35c..d5af39b9d4 100644
--- a/src/SetupStore.js
+++ b/src/SetupStore.js
@@ -1,5 +1,4 @@
var EventEmitter = require('events').EventEmitter;
-var assign = require('object-assign');
var _ = require('underscore');
var path = require('path');
var Promise = require('bluebird');
@@ -7,70 +6,60 @@ var boot2docker = require('./Boot2Docker');
var virtualBox = require('./VirtualBox');
var setupUtil = require('./SetupUtil');
var util = require('./Util');
+var assign = require('object-assign');
-var SUDO_PROMPT = 'Kitematic requires administrative privileges to install VirtualBox.';
var _currentStep = null;
var _error = null;
var _cancelled = false;
var _retryPromise = null;
+var _requiredSteps = [];
var _steps = [{
- name: 'downloadVirtualBox',
+ name: 'download',
title: 'Downloading VirtualBox',
message: 'VirtualBox is being downloaded. Kitematic requires VirtualBox to run containers.',
totalPercent: 35,
percent: 0,
run: Promise.coroutine(function* (progressCallback) {
var packagejson = util.packagejson();
- if (virtualBox.installed()) {
- var version = yield virtualBox.version();
- if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) >= 0) {
- return;
- }
- }
var virtualBoxFile = `https://github.com/kitematic/virtualbox/releases/download/${packagejson['virtualbox-version']}/${packagejson['virtualbox-filename']}`;
- yield setupUtil.download(virtualBoxFile, path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => {
+ yield setupUtil.download(virtualBoxFile, path.join(util.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => {
progressCallback(percent);
});
})
}, {
- name: 'installVirtualBox',
- title: 'Installing VirtualBox',
- message: "VirtualBox is being installed in the background. We may need you to type in your password to continue.",
+ name: 'install',
+ title: 'Installing Docker & VirtualBox',
+ message: 'VirtualBox is being installed in the background. We may need you to type in your password to continue.',
totalPercent: 5,
percent: 0,
seconds: 5,
run: Promise.coroutine(function* () {
var packagejson = util.packagejson();
- if (virtualBox.installed()) {
- var version = yield virtualBox.version();
- if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) >= 0) {
- return;
- }
+ var base = 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 /`;
}
- 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', path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']), '-target', '/']);
+ var cmd = `${util.escapePath(path.join(util.resourceDir(), 'cocoasudo'))} --prompt="Kitematic requires administrative privileges to install VirtualBox." bash -c \"${base}\"`;
try {
- yield util.exec(sudoCmd);
+ yield util.exec(cmd);
} catch (err) {
throw null;
}
})
}, {
- name: 'initBoot2Docker',
+ name: 'init',
title: 'Setting up Docker VM',
- message: "To run Docker containers on your computer, we are setting up a Linux virtual machine provided by boot2docker.",
+ message: 'To run Docker containers on your computer, we are setting up a Linux virtual machine provided by boot2docker.',
totalPercent: 15,
percent: 0,
seconds: 11,
run: Promise.coroutine(function* (progressCallback) {
+ setupUtil.simulateProgress(this.seconds, progressCallback);
yield virtualBox.vmdestroy('kitematic-vm');
var exists = yield boot2docker.exists();
if (!exists) {
- setupUtil.simulateProgress(this.seconds, progressCallback);
yield boot2docker.init();
return;
}
@@ -81,21 +70,20 @@ var _steps = [{
var isoversion = boot2docker.isoversion();
if (!isoversion || setupUtil.compareVersions(isoversion, boot2docker.version()) < 0) {
- setupUtil.simulateProgress(this.seconds, progressCallback);
yield boot2docker.stop();
yield boot2docker.upgrade();
}
})
}, {
- name: 'startBoot2Docker',
+ name: 'start',
title: 'Starting Docker VM',
message: "Kitematic is starting the boot2docker VM. This may take about a minute.",
totalPercent: 45,
percent: 0,
seconds: 35,
run: function (progressCallback) {
+ setupUtil.simulateProgress(this.seconds, progressCallback);
return boot2docker.waitstatus('saving').then(boot2docker.status).then(status => {
- setupUtil.simulateProgress(this.seconds, progressCallback);
if (status !== 'running') {
return boot2docker.start();
}
@@ -103,28 +91,29 @@ var _steps = [{
}
}];
-var SetupStore = assign(EventEmitter.prototype, {
+var SetupStore = assign(Object.create(EventEmitter.prototype), {
PROGRESS_EVENT: 'setup_progress',
STEP_EVENT: 'setup_step',
ERROR_EVENT: 'setup_error',
step: function () {
- return _currentStep || _steps[0];
+ return _currentStep;
},
steps: function () {
return _.indexBy(_steps, 'name');
},
stepCount: function () {
- return _steps.length;
+ return _requiredSteps.length;
},
number: function () {
- return _.indexOf(_steps, _currentStep) + 1;
+ return _.indexOf(_requiredSteps, _currentStep) + 1;
},
percent: function () {
- var total = 0;
- _.each(_steps, step => {
- total += step.totalPercent * step.percent / 100;
+ var sofar = 0;
+ var totalPercent = _requiredSteps.reduce((prev, step) => prev + step.totalPercent, 0);
+ _.each(_requiredSteps, step => {
+ sofar += step.totalPercent * step.percent / 100;
});
- return Math.min(Math.round(total), 99);
+ return Math.min(Math.round(100 * sofar / totalPercent), 99);
},
error: function () {
return _error;
@@ -141,13 +130,37 @@ var SetupStore = assign(EventEmitter.prototype, {
_retryPromise = Promise.defer();
return _retryPromise.promise;
},
- run: Promise.coroutine(function* () {
+ init: Promise.coroutine(function* () {
+ 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();
+ required.init = !(yield boot2docker.exists()) || !isoversion || setupUtil.compareVersions(isoversion, boot2docker.version()) < 0;
+ required.start = required.init || (yield boot2docker.status()) !== 'running';
+
var exists = yield boot2docker.exists();
if (exists) {
- this.steps().startBoot2Docker.seconds = 13;
+ this.steps().start.seconds = 13;
}
- for (let step of _steps) {
+ _requiredSteps = _steps.filter(function (step) {
+ return required[step.name];
+ });
+ }),
+ updateBinaries: function () {
+ if (setupUtil.needsBinaryFix()) {
+ return Promise.resolve();
+ }
+ if (setupUtil.shouldUpdateBinaries()) {
+ return util.exec(util.copyBinariesCmd());
+ }
+ return Promise.resolve();
+ },
+ run: Promise.coroutine(function* () {
+ yield this.init();
+ yield this.updateBinaries();
+ for (let step of _requiredSteps) {
_currentStep = step;
step.percent = 0;
while (true) {
@@ -163,6 +176,7 @@ var SetupStore = assign(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 31931d99b6..c18b0cab99 100644
--- a/src/SetupUtil.js
+++ b/src/SetupUtil.js
@@ -1,36 +1,33 @@
var _ = require('underscore');
var crypto = require('crypto');
-var exec = require('exec');
var fs = require('fs');
+var path = require('path');
var request = require('request');
var progress = require('request-progress');
-var path = require('path');
var Promise = require('bluebird');
+var util = require('./Util');
var SetupUtil = {
- supportDir: function () {
- var dirs = ['Library', 'Application\ Support', 'Kitematic'];
- var acc = process.env.HOME;
- dirs.forEach(function (d) {
- acc = path.join(acc, d);
- if (!fs.existsSync(acc)) {
- fs.mkdirSync(acc);
- }
- });
- return acc;
+ needsBinaryFix: function () {
+ if (!fs.existsSync('/usr/local/bin/docker') && !fs.existsSync('/usr/local/bin/boot2docker')) {
+ return fs.statSync('/usr/local/bin').gid !== 80 || fs.statSync('/usr/local/bin').uid !== process.getuid();
+ }
+
+ if (fs.existsSync('/usr/local/bin/docker') && (fs.statSync('/usr/local/bin/docker').gid !== 80 || fs.statSync('/usr/local/bin/docker').uid !== process.getuid())) {
+ return true;
+ }
+
+ if (fs.existsSync('/usr/local/bin/boot2docker') && (fs.statSync('/usr/local/bin/boot2docker').gid !== 80 || fs.statSync('/usr/local/bin/boot2docker').uid !== process.getuid())) {
+ return true;
+ }
+ return false;
},
- resourceDir: function () {
- return process.env.RESOURCES_PATH;
- },
- 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);
- });
- });
+ shouldUpdateBinaries: function () {
+ var packagejson = util.packagejson();
+ return !fs.existsSync('/usr/local/bin/docker') ||
+ !fs.existsSync('/usr/local/bin/boot2docker') ||
+ 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']));
},
simulateProgress: function (estimateSeconds, progress) {
var times = _.range(0, estimateSeconds * 1000, 200);
@@ -42,10 +39,13 @@ var SetupUtil = {
timers.push(timer);
});
},
+ checksum: function (filename) {
+ return crypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex');
+ },
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');
+ var existingChecksum = this.checksum(filename);
if (existingChecksum === checksum) {
resolve();
return;
diff --git a/src/Util.js b/src/Util.js
index d83cc2fa01..8bc3b88458 100644
--- a/src/Util.js
+++ b/src/Util.js
@@ -4,9 +4,11 @@ var fs = require('fs');
var path = require('path');
module.exports = {
- exec: function (args) {
+ exec: function (args, options) {
+ options = options || {};
return new Promise((resolve, reject) => {
- exec(args, (stderr, stdout, code) => {
+ console.log(options);
+ exec(args, options, (stderr, stdout, code) => {
if (code) {
reject(stderr);
}
@@ -17,7 +19,42 @@ module.exports = {
home: function () {
return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
},
+ supportDir: function () {
+ var dirs = ['Library', 'Application\ Support', 'Kitematic'];
+ var acc = process.env.HOME;
+ dirs.forEach(function (d) {
+ acc = path.join(acc, d);
+ if (!fs.existsSync(acc)) {
+ fs.mkdirSync(acc);
+ }
+ });
+ return acc;
+ },
+ resourceDir: function () {
+ return process.env.RESOURCES_PATH;
+ },
packagejson: function () {
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
+ },
+ copycmd: function (src, dest) {
+ return ['rm', '-f', dest, '&&', 'cp', src, dest];
+ },
+ copyBinariesCmd: function () {
+ var packagejson = this.packagejson();
+ var cmd = [];
+ cmd.push.apply(cmd, this.copycmd(this.escapePath(path.join(this.resourceDir(), 'boot2docker-' + packagejson['boot2docker-version'])), '/usr/local/bin/boot2docker'));
+ cmd.push('&&');
+ cmd.push.apply(cmd, this.copycmd(this.escapePath(path.join(this.resourceDir(), 'docker-' + packagejson['docker-version'])), '/usr/local/bin/docker'));
+ return cmd.join(' ');
+ },
+ fixBinariesCmd: function () {
+ var cmd = [];
+ cmd.push.apply(cmd, ['chown', `${process.getuid()}:${80}`, this.escapePath(path.join('/usr/local/bin', 'boot2docker'))]);
+ cmd.push('&&');
+ cmd.push.apply(cmd, ['chown', `${process.getuid()}:${80}`, this.escapePath(path.join('/usr/local/bin', 'docker'))]);
+ return cmd.join(' ');
+ },
+ escapePath: function (str) {
+ return str.replace(/ /g, '\\ ').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
};
diff --git a/src/VirtualBox.js b/src/VirtualBox.js
index a1d20bb427..4831870280 100644
--- a/src/VirtualBox.js
+++ b/src/VirtualBox.js
@@ -27,13 +27,12 @@ var VirtualBox = {
return util.exec(this.command() + ' list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} ' + this.command() + ' controlvm {} poweroff');
},
killall: 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']);
+ }).catch(err => {
+
});
},
vmstate: function (name) {