Merge remote-tracking branch 'kitematic/master'

This commit is contained in:
Alexandre Vázquez 2015-12-17 23:01:26 +01:00
commit 19090252b9
37 changed files with 474 additions and 402 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
.swp .swp
build build
dist dist
release
installer installer
node_modules node_modules
coverage coverage

View File

@ -1,31 +0,0 @@
{
"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,
"jest": true,
"pit": true
},
"predef": [ "-Promise" ]
}

View File

@ -1,18 +1,14 @@
sudo: false
language: node_js
node_js:
- "4.1"
cache: cache:
directories: directories:
- resources - node_modules
- node_modules
before_install:
- brew unlink node
- brew update
- brew install homebrew/versions/node4-lts
script: script:
- npm install - npm install
- npm test - npm test
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run integration || false'
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && ./ci/release || false'
os:
- osx

View File

@ -22,6 +22,7 @@ module.exports = function (grunt) {
var BASENAME = 'Kitematic'; var BASENAME = 'Kitematic';
var OSX_APPNAME = BASENAME + ' (Beta)'; var OSX_APPNAME = BASENAME + ' (Beta)';
var WINDOWS_APPNAME = BASENAME + ' (Alpha)'; var WINDOWS_APPNAME = BASENAME + ' (Alpha)';
var LINUX_APPNAME = BASENAME + ' (Alpha)';
var OSX_OUT = './dist'; var OSX_OUT = './dist';
var OSX_OUT_X64 = OSX_OUT + '/' + OSX_APPNAME + '-darwin-x64'; var OSX_OUT_X64 = OSX_OUT + '/' + OSX_APPNAME + '-darwin-x64';
var OSX_FILENAME = OSX_OUT_X64 + '/' + OSX_APPNAME + '.app'; var OSX_FILENAME = OSX_OUT_X64 + '/' + OSX_APPNAME + '.app';
@ -54,6 +55,18 @@ module.exports = function (grunt) {
platform: 'darwin', platform: 'darwin',
arch: 'x64', arch: 'x64',
asar: true, asar: true,
'app-version': packagejson.version
}
},
linux: {
options: {
name: LINUX_APPNAME,
dir: 'build/',
out: 'dist',
version: packagejson['electron-version'],
platform: 'linux',
arch: 'x64',
asar: true,
'app-bundle-id': 'com.kitematic.kitematic', 'app-bundle-id': 'com.kitematic.kitematic',
'app-version': packagejson.version 'app-version': packagejson.version
} }
@ -195,7 +208,7 @@ module.exports = function (grunt) {
].join(' && '), ].join(' && '),
}, },
zip: { zip: {
command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> dist/' + BASENAME + '-' + packagejson.version + '-Mac.zip', command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> release/' + BASENAME + '-Mac.zip',
} }
}, },
@ -206,7 +219,7 @@ module.exports = function (grunt) {
compress: { compress: {
windows: { windows: {
options: { options: {
archive: './dist/' + BASENAME + '-' + packagejson.version + '-Windows-Alpha.zip', archive: './release/' + BASENAME + '-Windows.zip',
mode: 'zip' mode: 'zip'
}, },
files: [{ files: [{
@ -244,7 +257,6 @@ module.exports = function (grunt) {
grunt.registerTask('default', ['newer:babel', 'less', 'newer:copy:dev', 'shell:electron', 'watchChokidar']); grunt.registerTask('default', ['newer:babel', 'less', 'newer:copy:dev', 'shell:electron', 'watchChokidar']);
grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron', 'copy:osx', 'shell:sign', 'shell:zip', 'copy:windows', 'rcedit:exes', 'compress']); grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron', 'copy:osx', 'shell:sign', 'shell:zip', 'copy:windows', 'rcedit:exes', 'compress']);
grunt.registerTask('release-mac', ['clean:release', 'babel', 'less', 'copy:dev', 'electron:osx', 'copy:osx', 'shell:sign', 'shell:zip']);
process.on('SIGINT', function () { process.on('SIGINT', function () {
grunt.task.run(['shell:electron:kill']); grunt.task.run(['shell:electron:kill']);

View File

@ -1,4 +1,46 @@
Jeff Morgan <jmorgan@docker.com> (@jeffdm) # Kitematic maintainers file
Sean Li <mail@shang.li> (@elesant) #
Michael Chiang <mchiang@docker.com> (@mchiang0610) # This file describes who runs the docker/kitematic project and how.
Ben French <me@frenchben.com> (@FrenchBen) # This is a living document - if you see something out of date or missing, speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant parser.
#
# This file is compiled into the MAINTAINERS file in docker/opensource.
#
[Org]
[Org."Core maintainers"]
people = [
"elesant",
"FrenchBen",
"jeffdm",
"mchiang0610",
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.elesant]
Name = "Sean Li"
Email = "mail@shang.li"
GitHub = "elesant"
[people.FrenchBen]
Name = "Ben French"
Email = "me@frenchben.com"
GitHub = "FrenchBen"
[people.jeffdm]
Name = "Jeff Morgan"
Email = "jmorgan@docker.com"
GitHub = "jeffdm"
[people.mchiang0610]
Name = "Michael Chiang"
Email = "mchiang@docker.com"
GitHub = "mchiang0610"

View File

@ -1,9 +1,9 @@
[![Build Status](https://travis-ci.org/kitematic/kitematic.svg?branch=master)](https://travis-ci.org/kitematic/kitematic) [![Build Status](https://travis-ci.org/docker/kitematic.svg?branch=master)](https://travis-ci.org/docker/kitematic)
[![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)](https://kitematic.com) [![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)](https://kitematic.com)
Kitematic is a simple application for managing Docker containers on Mac and Windows. Kitematic is a simple application for managing Docker containers on Mac, Linux and Windows.
![Kitematic Screenshot](https://cloud.githubusercontent.com/assets/251292/8246120/d3ab271a-15ed-11e5-8736-9a730a27c79a.png) ![Kitematic Screenshot](https://cloud.githubusercontent.com/assets/251292/8246120/d3ab271a-15ed-11e5-8736-9a730a27c79a.png)

View File

@ -1,5 +0,0 @@
echo $MAC_KEY_CONTENT > mac_key_content.hex && xxd -p -r mac_key_content.hex ~/Library/Keychains/keychain.keychain && rm mac_key_content.hex
security unlock-keychain -p "$MAC_KEY_SECRET" ~/Library/Keychains/keychain.keychain
security default-keychain -s keychain.keychain
security list-keychains
npm run release

View File

@ -1,3 +1,15 @@
machine: machine:
node: xcode:
version: 4.1.2 version: "7.0"
dependencies:
cache_directories:
- "node_modules"
deployment:
release:
tag: /v.*/
owner: docker
commands:
- github-release upload --user docker --repo kitematic --tag $CIRCLE_TAG --file release/Kitematic-Mac.zip --name Kitematic-Mac.zip
- github-release upload --user docker --repo kitematic --tag $CIRCLE_TAG --file release/Kitematic-Windows.zip --name Kitematic-Windows.zip

View File

@ -1,4 +1,4 @@
FROM docs/base:hugo-github-linking FROM docs/base:latest
MAINTAINER Mary Anthony <mary@docker.com> (@moxiegirl) MAINTAINER Mary Anthony <mary@docker.com> (@moxiegirl)
RUN svn checkout https://github.com/docker/compose/trunk/docs /docs/content/compose RUN svn checkout https://github.com/docker/compose/trunk/docs /docs/content/compose
@ -9,7 +9,8 @@ RUN svn checkout https://github.com/docker/tutorials/trunk/docs /docs/content
RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/opensource RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/opensource
RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine
ENV PROJECT=kitematic
# To get the git info for this repo # To get the git info for this repo
COPY . /src COPY . /src
COPY . /docs/content/kitematic/ COPY . /docs/content/$PROJECT/

View File

@ -17,7 +17,7 @@ using Kitematic and Docker.
### Create Minecraft Server Container ### Create Minecraft Server Container
First, if you haven't yet done so, [download and start First, if you haven't yet done so, [download and start
Kitematic](/). Once installed and running, the app should look like this: Kitematic](index.md). Once installed and running, the app should look like this:
Create a container from the recommended Minecraft image by clicking the "Create" Create a container from the recommended Minecraft image by clicking the "Create"
button. button.

View File

@ -25,7 +25,7 @@ Let's get to it!
#### Running the Nginx Web Server Container #### Running the Nginx Web Server Container
First, if you haven't yet done so, [download and start First, if you haven't yet done so, [download and start
Kitematic](/). Once installed and running, the app should look like this: Kitematic](index.md). Once installed and running, the app should look like this:
![Nginx create](images/nginx-create.png) ![Nginx create](images/nginx-create.png)

View File

@ -19,7 +19,7 @@ In this tutorial, you will:
### Setting up RethinkDB in Kitematic ### Setting up RethinkDB in Kitematic
First, if you haven't yet done so, [download and start First, if you haven't yet done so, [download and start
Kitematic](/). Once open, the app should look like Kitematic](index.md). Once open, the app should look like
this: this:
![Rethink create button](images/rethink-create.png) ![Rethink create button](images/rethink-create.png)

View File

@ -30,7 +30,7 @@ stream logs, and single click terminal into your Docker container all from the
GUI. GUI.
First, if you haven't yet done so, [download and start First, if you haven't yet done so, [download and start
Kitematic](/). Kitematic](index.md).
## Container list ## Container list

View File

@ -1,6 +1,6 @@
{ {
"name": "Kitematic", "name": "Kitematic",
"version": "0.9.3", "version": "0.9.4",
"author": "Kitematic", "author": "Kitematic",
"description": "Simple Docker Container management for Mac OS X.", "description": "Simple Docker Container management for Mac OS X.",
"homepage": "https://kitematic.com/", "homepage": "https://kitematic.com/",
@ -12,6 +12,7 @@
"bugs": "https://github.com/kitematic/kitematic/issues", "bugs": "https://github.com/kitematic/kitematic/issues",
"scripts": { "scripts": {
"start": "grunt", "start": "grunt",
"start-dev": "NODE_ENV=development grunt",
"test": "jest -c jest-unit.json", "test": "jest -c jest-unit.json",
"integration": "jest -c jest-integration.json", "integration": "jest -c jest-integration.json",
"release": "grunt release", "release": "grunt release",
@ -19,8 +20,9 @@
"lint": "jsxhint src" "lint": "jsxhint src"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"electron-version": "0.33.6", "electron-version": "0.35.4",
"dependencies": { "dependencies": {
"JSONStream": "^1.0.7",
"alt": "^0.16.2", "alt": "^0.16.2",
"ansi-to-html": "0.3.0", "ansi-to-html": "0.3.0",
"any-promise": "^0.1.0", "any-promise": "^0.1.0",
@ -30,8 +32,7 @@
"classnames": "^2.1.5", "classnames": "^2.1.5",
"coveralls": "^2.11.2", "coveralls": "^2.11.2",
"deep-extend": "^0.4.0", "deep-extend": "^0.4.0",
"dockerode": "^2.2.3", "dockerode": "^2.2.7",
"exec": "0.2.1",
"install": "^0.1.8", "install": "^0.1.8",
"jquery": "^2.1.3", "jquery": "^2.1.3",
"mixpanel": "kitematic/mixpanel-node", "mixpanel": "kitematic/mixpanel-node",
@ -54,7 +55,7 @@
"devDependencies": { "devDependencies": {
"babel": "^5.8.23", "babel": "^5.8.23",
"babel-jest": "^5.2.0", "babel-jest": "^5.2.0",
"electron-prebuilt": "^0.33.6", "electron-prebuilt": "^0.35.4",
"eslint": "^1.3.1", "eslint": "^1.3.1",
"eslint-plugin-react": "^3.3.0", "eslint-plugin-react": "^3.3.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",

View File

@ -38,6 +38,10 @@ class ContainerActions {
run (name, repo, tag) { run (name, repo, tag) {
dockerUtil.run(name, repo, tag); dockerUtil.run(name, repo, tag);
} }
active (name) {
dockerUtil.active(name);
}
} }
export default alt.createActions(ContainerActions); export default alt.createActions(ContainerActions);

View File

@ -15,7 +15,9 @@ class ContainerServerActions {
'updated', 'updated',
'waiting', 'waiting',
'kill', 'kill',
'stopped' 'stopped',
'log',
'logs'
); );
} }
} }

View File

@ -17,6 +17,7 @@ import Router from 'react-router';
import routes from './routes'; import routes from './routes';
import routerContainer from './router'; import routerContainer from './router';
import repositoryActions from './actions/RepositoryActions'; import repositoryActions from './actions/RepositoryActions';
import util from './utils/Util';
var app = remote.require('app'); var app = remote.require('app');
hubUtil.init(); hubUtil.init();

View File

@ -53,6 +53,10 @@ app.on('ready', function () {
show: false show: false
}); });
if (process.env.NODE_ENV === 'development') {
mainWindow.openDevTools({detach: true});
}
mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, 'index.html'))); mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, 'index.html')));
app.on('activate-with-no-open-windows', function () { app.on('activate-with-no-open-windows', function () {

View File

@ -28,17 +28,10 @@ var ContainerHomeFolder = React.createClass({
var mounts = _.clone(this.props.container.Mounts); var mounts = _.clone(this.props.container.Mounts);
var newSource = path.join(util.home(), util.documents(), 'Kitematic', this.props.container.Name, destination); var newSource = path.join(util.home(), util.documents(), 'Kitematic', this.props.container.Name, destination);
var binds = mounts.map(function (m) { mounts.forEach(m => {
let source = m.Source;
if (m.Destination === destination) { if (m.Destination === destination) {
source = newSource; m.Source = util.windowsToLinuxPath(newSource);
} }
if(util.isWindows()) {
return util.windowsToLinuxPath(source) + ':' + m.Destination;
}
return source + ':' + m.Destination;
}); });
mkdirp(newSource, function (err) { mkdirp(newSource, function (err) {
@ -48,7 +41,7 @@ var ContainerHomeFolder = React.createClass({
} }
}); });
containerActions.update(this.props.container.Name, {Binds: binds}); containerActions.update(this.props.container.Name, {Mounts: mounts});
} }
}); });
} else { } else {

View File

@ -1,74 +1,44 @@
import $ from 'jquery'; import $ from 'jquery';
import React from 'react/addons'; import React from 'react/addons';
import LogStore from '../stores/LogStore';
import Router from 'react-router'; import Router from 'react-router';
import metrics from '../utils/MetricsUtil'; import containerActions from '../actions/ContainerActions';
import Convert from 'ansi-to-html';
var _prevBottom = 0; let escape = function (html) {
var text = document.createTextNode(html);
var div = document.createElement('div');
div.appendChild(text);
return div.innerHTML;
};
let convert = new Convert();
let prevBottom = 0;
module.exports = React.createClass({ module.exports = React.createClass({
mixins: [Router.Navigation],
getInitialState: function () { componentDidUpdate: function () {
return { var node = $('.logs').get()[0];
logs: [] node.scrollTop = node.scrollHeight;
};
},
componentDidMount: function() {
if (!this.props.container) {
return;
}
this.update();
this.scrollToBottom();
LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update);
LogStore.fetch(this.props.container.Name);
}, },
componentWillReceiveProps: function (nextProps) { componentWillReceiveProps: function (nextProps) {
if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) { if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) {
LogStore.detach(this.props.container.Name); containerActions.active(nextProps.container.Name);
LogStore.fetch(nextProps.container.Name);
} }
}, },
componentWillUnmount: function() { componentDidMount: function () {
if (!this.props.container) { containerActions.active(this.props.container.Name);
return; },
}
LogStore.detach(this.props.container.Name); componentWillUnmount: function () {
LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update); containerActions.active(null);
},
componentDidUpdate: function () {
this.scrollToBottom();
},
scrollToBottom: function () {
var parent = $('.logs');
if (parent[0].scrollHeight - parent.height() >= _prevBottom - 50) {
parent.scrollTop(parent[0].scrollHeight - parent.height());
}
_prevBottom = parent[0].scrollHeight - parent.height();
},
handleClickLogs: function () {
metrics.track('Viewed Logs', {
from: 'preview'
});
this.context.router.transitionTo('containerLogs', {name: this.props.container.Name});
},
update: function () {
if (!this.props.container) {
return;
}
this.setState({
logs: LogStore.logs(this.props.container.Name)
});
}, },
render: function () { render: function () {
var logs = this.state.logs.map(function (l, i) { let logs = this.props.container.Logs ?
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>; this.props.container.Logs.map((l) => <div key={l.substr(0,l.indexOf(' '))} dangerouslySetInnerHTML={{__html: convert.toHtml(escape(l.substr(l.indexOf(' ')+1)).replace(/ /g, '&nbsp;<wbr>'))}}></div>) :
}); ['0 No logs for this container.'];
if (logs.length === 0) {
logs = "No logs for this container.";
}
return ( return (
<div className="mini-logs wrapper"> <div className="mini-logs wrapper">
<div className="widget"> <div className="widget">

View File

@ -96,7 +96,7 @@ var ContainerListItem = React.createClass({
return ( return (
<Router.Link to="container" params={{name: container.Name}}> <Router.Link to="container" params={{name: container.Name}}>
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave}> <li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave} onClick={self.handleClick}>
{state} {state}
<div className="info"> <div className="info">
<div className="name"> <div className="name">

View File

@ -1,61 +0,0 @@
import $ from 'jquery';
import React from 'react/addons';
import LogStore from '../stores/LogStore';
var _prevBottom = 0;
module.exports = React.createClass({
getInitialState: function () {
return {
logs: []
};
},
componentDidMount: function() {
if (!this.props.container) {
return;
}
this.update();
this.scrollToBottom();
LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update);
LogStore.fetch(this.props.container.Name);
},
componentWillUnmount: function() {
if (!this.props.container) {
return;
}
LogStore.detach(this.props.container.Name);
LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update);
},
componentDidUpdate: function () {
this.scrollToBottom();
},
scrollToBottom: function () {
var parent = $('.details-logs');
if (parent.scrollTop() >= _prevBottom - 50) {
parent.scrollTop(parent[0].scrollHeight - parent.height());
}
_prevBottom = parent[0].scrollHeight - parent.height();
},
update: function () {
if (!this.props.container) {
return;
}
this.setState({
logs: LogStore.logs(this.props.container.Name)
});
},
render: function () {
var logs = this.state.logs.map(function (l, i) {
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
});
if (logs.length === 0) {
logs = "No logs for this container.";
}
return (
<div className="details-panel details-logs logs">
{logs}
</div>
);
}
});

View File

@ -27,14 +27,10 @@ var ContainerSettingsVolumes = React.createClass({
metrics.track('Choose Directory for Volume'); metrics.track('Choose Directory for Volume');
if(util.isWindows()) {
directory = util.windowsToLinuxPath(directory);
}
var mounts = _.clone(this.props.container.Mounts); var mounts = _.clone(this.props.container.Mounts);
_.each(mounts, m => { _.each(mounts, m => {
if (m.Destination === dockerVol) { if (m.Destination === dockerVol) {
m.Source = directory; m.Source = util.windowsToLinuxPath(directory);
} }
}); });
@ -50,19 +46,14 @@ var ContainerSettingsVolumes = React.createClass({
from: 'settings' from: 'settings'
}); });
var hostConfig = _.clone(this.props.container.HostConfig);
var binds = hostConfig.Binds;
var mounts = _.clone(this.props.container.Mounts); var mounts = _.clone(this.props.container.Mounts);
_.each(mounts, m => { _.each(mounts, m => {
if (m.Destination === dockerVol) { if (m.Destination === dockerVol) {
m.Source = null; m.Source = null;
} }
}); });
var index = _.findIndex(binds, bind => bind.indexOf(`:${dockerVol}`) !== -1);
if (index >= 0) { containerActions.update(this.props.container.Name, {Mounts: mounts});
binds.splice(index, 1);
}
containerActions.update(this.props.container.Name, {HostConfig: hostConfig, Binds: binds, Mounts: mounts});
}, },
handleOpenVolumeClick: function (path) { handleOpenVolumeClick: function (path) {
metrics.track('Opened Volume Directory', { metrics.track('Opened Volume Directory', {

View File

@ -2,7 +2,6 @@ import React from 'react/addons';
import remote from 'remote'; import remote from 'remote';
import RetinaImage from 'react-retina-image'; import RetinaImage from 'react-retina-image';
import ipc from 'ipc'; import ipc from 'ipc';
var autoUpdater = remote.require('auto-updater');
import util from '../utils/Util'; import util from '../utils/Util';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
var Menu = remote.require('menu'); var Menu = remote.require('menu');
@ -32,7 +31,6 @@ var Header = React.createClass({
updateAvailable: true updateAvailable: true
}); });
}); });
autoUpdater.checkForUpdates();
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
document.removeEventListener('keyup', this.handleDocumentKeyUp, false); document.removeEventListener('keyup', this.handleDocumentKeyUp, false);

View File

@ -35,10 +35,11 @@ var Preferences = React.createClass({
}); });
}, },
render: function () { render: function () {
return ( var vmSettings;
<div className="preferences">
<div className="preferences-content"> if (process.platform !== 'linux') {
<a onClick={this.handleGoBackClick}>Go Back</a> vmSettings = (
<div>
<div className="title">VM Settings</div> <div className="title">VM Settings</div>
<div className="option"> <div className="option">
<div className="option-name"> <div className="option-name">
@ -48,6 +49,15 @@ var Preferences = React.createClass({
<input type="checkbox" checked={this.state.closeVMOnQuit} onChange={this.handleChangeCloseVMOnQuit}/> <input type="checkbox" checked={this.state.closeVMOnQuit} onChange={this.handleChangeCloseVMOnQuit}/>
</div> </div>
</div> </div>
</div>
);
}
return (
<div className="preferences">
<div className="preferences-content">
<a onClick={this.handleGoBackClick}>Go Back</a>
{vmSettings}
<div className="title">App Settings</div> <div className="title">App Settings</div>
<div className="option"> <div className="option">
<div className="option-name"> <div className="option-name">

View File

@ -3,6 +3,7 @@ import Router from 'react-router';
import Radial from './Radial.react.js'; import Radial from './Radial.react.js';
import RetinaImage from 'react-retina-image'; import RetinaImage from 'react-retina-image';
import Header from './Header.react'; import Header from './Header.react';
import Util from '../utils/Util';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
import setupStore from '../stores/SetupStore'; import setupStore from '../stores/SetupStore';
import setupActions from '../actions/SetupActions'; import setupActions from '../actions/SetupActions';
@ -43,6 +44,13 @@ var Setup = React.createClass({
shell.openExternal('https://www.docker.com/docker-toolbox'); shell.openExternal('https://www.docker.com/docker-toolbox');
}, },
handleLinuxDockerInstall: function () {
metrics.track('Opening Linux Docker installation instructions', {
from: 'setup'
});
shell.openExternal('http://docs.docker.com/linux/started/');
},
renderContents: function () { renderContents: function () {
return ( return (
<div className="contents"> <div className="contents">
@ -74,6 +82,25 @@ var Setup = React.createClass({
}, },
renderError: function () { renderError: function () {
let deleteVmAndRetry;
if (Util.isLinux()) {
if (!this.state.started) {
deleteVmAndRetry = (
<button className="btn btn-action" onClick={this.handleLinuxDockerInstall}>Install Docker</button>
);
}
} else {
if (this.state.started) {
deleteVmAndRetry = (
<button className="btn btn-action" onClick={this.handleErrorRemoveRetry}>Delete VM &amp; Retry Setup</button>
);
} else {
deleteVmAndRetry = (
<button className="btn btn-action" onClick={this.handleToolBox}>Get Toolbox</button>
);
}
}
return ( return (
<div className="setup"> <div className="setup">
<Header hideLogin={true}/> <Header hideLogin={true}/>
@ -93,7 +120,7 @@ var Setup = React.createClass({
<p className="error">{this.state.error.message || this.state.error}</p> <p className="error">{this.state.error.message || this.state.error}</p>
<p className="setup-actions"> <p className="setup-actions">
<button className="btn btn-action" onClick={this.handleErrorRetry}>Retry Setup</button> <button className="btn btn-action" onClick={this.handleErrorRetry}>Retry Setup</button>
{this.state.started ? <button className="btn btn-action" onClick={this.handleErrorRemoveRetry}>Delete VM &amp; Retry Setup</button> : <button className="btn btn-action" onClick={this.handleToolBox}>Get Toolbox</button>} {{deleteVmAndRetry}}
</p> </p>
</div> </div>
</div> </div>

View File

@ -158,7 +158,17 @@ var MenuTemplate = function () {
{ {
label: 'Bring All to Front', label: 'Bring All to Front',
selector: 'arrangeInFront:' selector: 'arrangeInFront:'
} },
{
type: 'separator'
},
{
label: 'Kitematic',
accelerator: 'Cmd+0',
click: function () {
remote.getCurrentWindow().show();
}
},
] ]
}, },
{ {

View File

@ -6,7 +6,6 @@ import AccountLogin from './components/AccountLogin.react';
import Containers from './components/Containers.react'; import Containers from './components/Containers.react';
import ContainerDetails from './components/ContainerDetails.react'; import ContainerDetails from './components/ContainerDetails.react';
import ContainerHome from './components/ContainerHome.react'; import ContainerHome from './components/ContainerHome.react';
import ContainerLogs from './components/ContainerLogs.react';
import ContainerSettings from './components/ContainerSettings.react'; import ContainerSettings from './components/ContainerSettings.react';
import ContainerSettingsGeneral from './components/ContainerSettingsGeneral.react'; import ContainerSettingsGeneral from './components/ContainerSettingsGeneral.react';
import ContainerSettingsPorts from './components/ContainerSettingsPorts.react'; import ContainerSettingsPorts from './components/ContainerSettingsPorts.react';
@ -39,7 +38,6 @@ var routes = (
<Route name="containers" path="containers" handler={Containers}> <Route name="containers" path="containers" handler={Containers}>
<Route name="container" path="details/:name" handler={ContainerDetails}> <Route name="container" path="details/:name" handler={ContainerDetails}>
<DefaultRoute name="containerHome" handler={ContainerHome} /> <DefaultRoute name="containerHome" handler={ContainerHome} />
<Route name="containerLogs" path="logs" handler={ContainerLogs}/>
<Route name="containerSettings" path="settings" handler={ContainerSettings}> <Route name="containerSettings" path="settings" handler={ContainerSettings}>
<Route name="containerSettingsGeneral" path="general" handler={ContainerSettingsGeneral}/> <Route name="containerSettingsGeneral" path="general" handler={ContainerSettingsGeneral}/>
<Route name="containerSettingsPorts" path="ports" handler={ContainerSettingsPorts}/> <Route name="containerSettingsPorts" path="ports" handler={ContainerSettingsPorts}/>

View File

@ -3,6 +3,8 @@ import alt from '../alt';
import containerServerActions from '../actions/ContainerServerActions'; import containerServerActions from '../actions/ContainerServerActions';
import containerActions from '../actions/ContainerActions'; import containerActions from '../actions/ContainerActions';
let MAX_LOG_SIZE = 3000;
class ContainerStore { class ContainerStore {
constructor () { constructor () {
this.bindActions(containerActions); this.bindActions(containerActions);
@ -98,16 +100,20 @@ class ContainerStore {
} }
updated ({container}) { updated ({container}) {
if (!container || !container.Name) {
return;
}
let containers = this.containers; let containers = this.containers;
if (containers[container.Name] && containers[container.Name].State.Updating) { if (containers[container.Name] && containers[container.Name].State.Updating) {
return; return;
} }
// Trigger log update
// TODO: fix this loading multiple times if (containers[container.Name] && containers[container.Name].Logs) {
// LogStore.fetch(container.Name); container.Logs = containers[container.Name].Logs;
}
containers[container.Name] = container; containers[container.Name] = container;
this.setState({containers}); this.setState({containers});
} }
@ -141,7 +147,7 @@ class ContainerStore {
} }
} }
waiting({name, waiting}) { waiting ({name, waiting}) {
let containers = this.containers; let containers = this.containers;
if (containers[name]) { if (containers[name]) {
containers[name].State.Waiting = waiting; containers[name].State.Waiting = waiting;
@ -158,6 +164,33 @@ class ContainerStore {
this.setState({pending: null}); this.setState({pending: null});
} }
log ({name, entry}) {
let container = this.containers[name];
if (!container) {
return;
}
if (!container.Logs) {
container.Logs = [];
}
container.Logs.push.apply(container.Logs, entry.split('\n').filter(e => e.length));
container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE);
this.emitChange();
}
logs ({name, logs}) {
let container = this.containers[name];
if (!container) {
return;
}
container.Logs = logs.split('\n');
container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE);
this.emitChange();
}
static generateName (repo) { static generateName (repo) {
const base = _.last(repo.split('/')); const base = _.last(repo.split('/'));
const names = _.keys(this.getState().containers); const names = _.keys(this.getState().containers);

View File

@ -1,85 +0,0 @@
import {EventEmitter} from 'events';
import assign from 'object-assign';
import Convert from 'ansi-to-html';
import docker from '../utils/DockerUtil';
import stream from 'stream';
var _convert = new Convert();
var _logs = {};
var _streams = {};
var MAX_LOG_SIZE = 3000;
module.exports = assign(Object.create(EventEmitter.prototype), {
SERVER_LOGS_EVENT: 'server_logs_event',
_escape: function (html) {
var text = document.createTextNode(html);
var div = document.createElement('div');
div.appendChild(text);
return div.innerHTML;
},
fetch: function (name) {
if (!name || !docker.client) {
return;
}
docker.client.getContainer(name).logs({
stdout: true,
stderr: true,
timestamps: false,
tail: MAX_LOG_SIZE,
follow: false
}, (err, logStream) => {
if (err) {
return;
}
var logs = [];
var outstream = new stream.PassThrough();
docker.client.modem.demuxStream(logStream, outstream, outstream);
outstream.on('data', (chunk) => {
logs.push(_convert.toHtml(this._escape(chunk)));
});
logStream.on('end', () => {
_logs[name] = logs;
this.emit(this.SERVER_LOGS_EVENT);
this.attach(name);
});
});
},
attach: function (name) {
if (!name || !docker.client || _streams[name]) {
return;
}
docker.client.getContainer(name).attach({
stdout: true,
stderr: true,
logs: false,
stream: true
}, (err, logStream) => {
if (err) {
return;
}
_streams[name] = logStream;
var outstream = new stream.PassThrough();
docker.client.modem.demuxStream(logStream, outstream, outstream);
outstream.on('data', (chunk) => {
_logs[name].push(_convert.toHtml(this._escape(chunk)));
if (_logs[name].length > MAX_LOG_SIZE) {
_logs[name] = _logs[name].slice(_logs[name].length - MAX_LOG_SIZE, MAX_LOG_SIZE);
}
this.emit(this.SERVER_LOGS_EVENT);
});
logStream.on('end', () => {
this.detach(name);
});
});
},
detach: function (name) {
if (_streams[name]) {
_streams[name].destroy();
delete _streams[name];
}
},
logs: function (name) {
return _logs[name] || [];
}
});

View File

@ -3,6 +3,7 @@ import path from 'path';
import Promise from 'bluebird'; import Promise from 'bluebird';
import fs from 'fs'; import fs from 'fs';
import util from './Util'; import util from './Util';
import child_process from 'child_process';
var DockerMachine = { var DockerMachine = {
command: function () { command: function () {
@ -22,7 +23,7 @@ var DockerMachine = {
return fs.existsSync(this.command()); return fs.existsSync(this.command());
}, },
version: function () { version: function () {
return util.exec([this.command(), '-v']).then(stdout => { return util.execFile([this.command(), '-v']).then(stdout => {
try { try {
var matchlist = stdout.match(/(\d+\.\d+\.\d+).*/); var matchlist = stdout.match(/(\d+\.\d+\.\d+).*/);
if (!matchlist || matchlist.length < 2) { if (!matchlist || matchlist.length < 2) {
@ -57,40 +58,46 @@ var DockerMachine = {
}); });
}, },
create: function (machineName = this.name()) { create: function (machineName = this.name()) {
return util.exec([this.command(), '-D', 'create', '-d', 'virtualbox', '--virtualbox-memory', '2048', machineName]); return util.execFile([this.command(), '-D', 'create', '-d', 'virtualbox', '--virtualbox-memory', '2048', machineName]);
}, },
start: function (machineName = this.name()) { start: function (machineName = this.name()) {
return util.exec([this.command(), '-D', 'start', machineName]); return util.execFile([this.command(), '-D', 'start', machineName]);
}, },
stop: function (machineName = this.name()) { stop: function (machineName = this.name()) {
return util.exec([this.command(), 'stop', machineName]); return util.execFile([this.command(), 'stop', machineName]);
}, },
upgrade: function (machineName = this.name()) { upgrade: function (machineName = this.name()) {
return util.exec([this.command(), 'upgrade', machineName]); return util.execFile([this.command(), 'upgrade', machineName]);
}, },
rm: function (machineName = this.name()) { rm: function (machineName = this.name()) {
return util.exec([this.command(), 'rm', '-f', machineName]); return util.execFile([this.command(), 'rm', '-f', machineName]);
}, },
ip: function (machineName = this.name()) { ip: function (machineName = this.name()) {
return util.exec([this.command(), 'ip', machineName]).then(stdout => { return util.execFile([this.command(), 'ip', machineName]).then(stdout => {
return Promise.resolve(stdout.trim().replace('\n', '')); return Promise.resolve(stdout.trim().replace('\n', ''));
}); });
}, },
url: function (machineName = this.name()) { url: function (machineName = this.name()) {
return util.exec([this.command(), 'url', machineName]).then(stdout => { return util.execFile([this.command(), 'url', machineName]).then(stdout => {
return Promise.resolve(stdout.trim().replace('\n', '')); return Promise.resolve(stdout.trim().replace('\n', ''));
}); });
}, },
regenerateCerts: function (machineName = this.name()) { regenerateCerts: function (machineName = this.name()) {
return util.exec([this.command(), 'tls-regenerate-certs', '-f', machineName]); return util.execFile([this.command(), 'tls-regenerate-certs', '-f', machineName]);
}, },
status: function (machineName = this.name()) { status: function (machineName = this.name()) {
return util.exec([this.command(), 'status', machineName]).then(stdout => { return new Promise((resolve, reject) => {
return Promise.resolve(stdout.trim().replace('\n', '')); child_process.execFile(this.command(), ['status', machineName], (error, stdout, stderr) => {
if (error) {
reject(new Error('Encountered an error: ' + error));
} else {
resolve(stdout.trim() + stderr.trim());
}
});
}); });
}, },
disk: function (machineName = this.name()) { disk: function (machineName = this.name()) {
return util.exec([this.command(), 'ssh', machineName, 'df']).then(stdout => { return util.execFile([this.command(), 'ssh', machineName, 'df']).then(stdout => {
try { try {
var lines = stdout.split('\n'); var lines = stdout.split('\n');
var dataline = _.find(lines, function (line) { var dataline = _.find(lines, function (line) {
@ -114,7 +121,7 @@ var DockerMachine = {
}); });
}, },
memory: function (machineName = this.name()) { memory: function (machineName = this.name()) {
return util.exec([this.command(), 'ssh', machineName, 'free -m']).then(stdout => { return util.execFile([this.command(), 'ssh', machineName, 'free -m']).then(stdout => {
try { try {
var lines = stdout.split('\n'); var lines = stdout.split('\n');
var dataline = _.find(lines, function (line) { var dataline = _.find(lines, function (line) {
@ -151,10 +158,15 @@ var DockerMachine = {
} }
}); });
}); });
} else if (util.isLinux()) {
cmd = cmd || process.env.SHELL;
var terminal = util.linuxTerminal();
if (terminal)
util.execFile(terminal.concat([cmd])).then(() => {});
} else { } else {
cmd = cmd || process.env.SHELL; cmd = cmd || process.env.SHELL;
this.url(machineName).then(machineUrl => { this.url(machineName).then(machineUrl => {
util.exec([path.join(process.env.RESOURCES_PATH, 'terminal'), `DOCKER_HOST=${machineUrl} DOCKER_CERT_PATH=${path.join(util.home(), '.docker/machine/machines/' + machineName)} DOCKER_TLS_VERIFY=1 ${cmd}`]).then(() => {}); util.execFile([path.join(process.env.RESOURCES_PATH, 'terminal'), `DOCKER_HOST=${machineUrl} DOCKER_CERT_PATH=${path.join(util.home(), '.docker/machine/machines/' + machineName)} DOCKER_TLS_VERIFY=1 ${cmd}`]).then(() => {});
}); });
} }
}, },

View File

@ -3,37 +3,46 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import dockerode from 'dockerode'; import dockerode from 'dockerode';
import _ from 'underscore'; import _ from 'underscore';
import child_process from 'child_process';
import util from './Util'; import util from './Util';
import hubUtil from './HubUtil'; import hubUtil from './HubUtil';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
import containerServerActions from '../actions/ContainerServerActions'; import containerServerActions from '../actions/ContainerServerActions';
import Promise from 'bluebird';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import stream from 'stream';
import JSONStream from 'JSONStream';
export default { export default {
host: null, host: null,
client: null, client: null,
placeholders: {}, placeholders: {},
streams: {},
activeContainerName: null,
setup (ip, name) { setup (ip, name) {
if (!ip || !name) { if (!ip || !name) {
throw new Error('Falsy ip or name passed to docker client setup'); throw new Error('Falsy ip or name passed to docker client setup');
} }
let certDir = path.join(util.home(), '.docker/machine/machines/', name); if (util.isLinux()) {
if (!fs.existsSync(certDir)) { this.host = 'localhost';
throw new Error('Certificate directory does not exist'); this.client = new dockerode({socketPath: '/var/run/docker.sock'});
} } else {
let certDir = path.join(util.home(), '.docker/machine/machines/', name);
if (!fs.existsSync(certDir)) {
throw new Error('Certificate directory does not exist');
}
this.host = ip; this.host = ip;
this.client = new dockerode({ this.client = new dockerode({
protocol: 'https', protocol: 'https',
host: ip, host: ip,
port: 2376, port: 2376,
ca: fs.readFileSync(path.join(certDir, 'ca.pem')), ca: fs.readFileSync(path.join(certDir, 'ca.pem')),
cert: fs.readFileSync(path.join(certDir, 'cert.pem')), cert: fs.readFileSync(path.join(certDir, 'cert.pem')),
key: fs.readFileSync(path.join(certDir, 'key.pem')) key: fs.readFileSync(path.join(certDir, 'key.pem'))
}); });
}
}, },
init () { init () {
@ -66,6 +75,14 @@ export default {
}); });
}, },
isDockerRunning () {
try {
child_process.execSync('ps ax | grep "docker daemon" | grep -v grep');
} catch (error) {
throw new Error('Cannot connect to the Docker daemon. The daemon is not running.');
}
},
startContainer (name, containerData) { startContainer (name, containerData) {
let startopts = { let startopts = {
Binds: containerData.Binds || [] Binds: containerData.Binds || []
@ -109,7 +126,7 @@ export default {
return; return;
} }
containerData.Cmd = image.Config.Cmd || 'bash'; containerData.Cmd = image.Config.Cmd || image.Config.Entrypoint || 'bash';
let existing = this.client.getContainer(name); let existing = this.client.getContainer(name);
existing.kill(() => { existing.kill(() => {
existing.remove(() => { existing.remove(() => {
@ -231,6 +248,11 @@ export default {
existingData.Tty = existingData.Config.Tty; existingData.Tty = existingData.Config.Tty;
existingData.OpenStdin = existingData.Config.OpenStdin; existingData.OpenStdin = existingData.Config.OpenStdin;
} }
data.Mounts = data.Mounts || existingData.Mounts;
data.Binds = data.Mounts.map(m => m.Source + ':' + m.Destination);
// Preserve Ports
let networking = _.extend(existingData.NetworkSettings, data.NetworkSettings); let networking = _.extend(existingData.NetworkSettings, data.NetworkSettings);
if (networking && networking.Ports) { if (networking && networking.Ports) {
let exposed = _.reduce(networking.Ports, (res, value, key) => { let exposed = _.reduce(networking.Ports, (res, value, key) => {
@ -256,8 +278,8 @@ export default {
containerServerActions.error({name, error}); containerServerActions.error({name, error});
return; return;
} }
var oldPath = path.join(util.home(), 'Kitematic', name); var oldPath = util.windowsToLinuxPath(path.join(util.home(), util.documents(), 'Kitematic', name));
var newPath = path.join(util.home(), 'Kitematic', newName); var newPath = util.windowsToLinuxPath(path.join(util.home(), util.documents(), 'Kitematic', newName));
this.client.getContainer(newName).inspect((error, container) => { this.client.getContainer(newName).inspect((error, container) => {
if (error) { if (error) {
@ -268,13 +290,12 @@ export default {
if (fs.existsSync(oldPath)) { if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, newPath); fs.renameSync(oldPath, newPath);
} }
var binds = _.pairs(container.Volumes).map(function (pair) {
return pair[1] + ':' + pair[0]; container.Mounts.forEach(m => {
m.Source = m.Source.replace(oldPath, newPath);
}); });
var newBinds = binds.map(b => {
return b.replace(path.join(util.home(), 'Kitematic', name), path.join(util.home(), 'Kitematic', newName)); this.updateContainer(newName, {Mounts: container.Mounts});
});
this.updateContainer(newName, {Binds: newBinds});
rimraf(oldPath, () => {}); rimraf(oldPath, () => {});
}); });
}); });
@ -343,6 +364,85 @@ export default {
}); });
}, },
active (name) {
this.detach();
this.activeContainerName = name;
if (name) {
this.logs();
}
},
logs () {
if (!this.activeContainerName) {
return;
}
this.client.getContainer(this.activeContainerName).logs({
stdout: true,
stderr: true,
tail: 1000,
follow: false,
timestamps: 1
}, (err, logStream) => {
if (err) {
return;
}
let logs = '';
logStream.setEncoding('utf8');
logStream.on('data', chunk => logs += chunk);
logStream.on('end', () => {
containerServerActions.logs({name: this.activeContainerName, logs});
this.attach();
});
});
},
attach () {
if (!this.activeContainerName) {
return;
}
this.client.getContainer(this.activeContainerName).logs({
stdout: true,
stderr: true,
tail: 0,
follow: true,
timestamps: 1
}, (err, logStream) => {
if (err) {
return;
}
if (this.stream) {
this.detach();
}
this.stream = logStream;
let timeout = null;
let batch = '';
logStream.setEncoding('utf8');
logStream.on('data', (chunk) => {
batch += chunk;
if (!timeout) {
timeout = setTimeout(() => {
containerServerActions.log({name: this.activeContainerName, entry: batch});
timeout = null;
batch = '';
}, 16);
}
});
});
},
detach () {
if (this.stream) {
this.stream.destroy();
this.stream = null;
}
},
listen () { listen () {
this.client.getEvents((error, stream) => { this.client.getEvents((error, stream) => {
if (error || !stream) { if (error || !stream) {
@ -351,19 +451,26 @@ export default {
} }
stream.setEncoding('utf8'); stream.setEncoding('utf8');
stream.on('data', json => { stream.pipe(JSONStream.parse()).on('data', data => {
let data = JSON.parse(json); if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') {
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') {
return; return;
} }
if (data.status === 'destroy') { if (data.status === 'destroy') {
containerServerActions.destroyed({id: data.id}); containerServerActions.destroyed({id: data.id});
this.detach(data.id);
} else if (data.status === 'kill') { } else if (data.status === 'kill') {
containerServerActions.kill({id: data.id}); containerServerActions.kill({id: data.id});
this.detach(data.id);
} else if (data.status === 'stop') { } else if (data.status === 'stop') {
containerServerActions.stopped({id: data.id}); containerServerActions.stopped({id: data.id});
this.detach(data.id);
} else if (data.status === 'create') {
this.logs();
this.fetchContainer(data.id);
} else if (data.status === 'start') {
this.attach();
this.fetchContainer(data.id);
} else if (data.id) { } else if (data.id) {
this.fetchContainer(data.id); this.fetchContainer(data.id);
} }
@ -405,9 +512,7 @@ export default {
let error = null; let error = null;
// data is associated with one layer only (can be identified with id) // data is associated with one layer only (can be identified with id)
stream.on('data', str => { stream.pipe(JSONStream.parse()).on('data', data => {
var data = JSON.parse(str);
if (data.error) { if (data.error) {
error = data.error; error = data.error;
return; return;

View File

@ -2,8 +2,8 @@ import _ from 'underscore';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import Promise from 'bluebird'; import Promise from 'bluebird';
import util from './Util';
import bugsnag from 'bugsnag-js'; import bugsnag from 'bugsnag-js';
import util from './Util';
import virtualBox from './VirtualBoxUtil'; import virtualBox from './VirtualBoxUtil';
import setupServerActions from '../actions/SetupServerActions'; import setupServerActions from '../actions/SetupServerActions';
import metrics from './MetricsUtil'; import metrics from './MetricsUtil';
@ -51,7 +51,35 @@ export default {
return _retryPromise.promise; return _retryPromise.promise;
}, },
async setup () { setup() {
return util.isLinux() ? this.nativeSetup() : this.nonNativeSetup();
},
async nativeSetup () {
while (true) {
try {
docker.setup('localhost', machine.name());
docker.isDockerRunning();
break;
} catch (error) {
router.get().transitionTo('setup');
metrics.track('Native Setup Failed');
setupServerActions.error({error});
let message = error.message.split('\n');
let lastLine = message.length > 1 ? message[message.length - 2] : 'Docker Machine encountered an error.';
bugsnag.notify('Native Setup Failed', lastLine, {
'Docker Machine Logs': error.message
}, 'info');
this.clearTimers();
await this.pause();
}
}
},
async nonNativeSetup () {
let virtualBoxVersion = null; let virtualBoxVersion = null;
let machineVersion = null; let machineVersion = null;
while (true) { while (true) {
@ -93,6 +121,7 @@ export default {
await machine.create(); await machine.create();
} else { } else {
let state = await machine.status(); let state = await machine.status();
console.log(state);
if (state !== 'Running') { if (state !== 'Running') {
if (state === 'Saved') { if (state === 'Saved') {
router.get().transitionTo('setup'); router.get().transitionTo('setup');
@ -109,9 +138,9 @@ export default {
let tries = 80, ip = null; let tries = 80, ip = null;
while (!ip && tries > 0) { while (!ip && tries > 0) {
try { try {
tries -= 1;
console.log('Trying to fetch machine IP, tries left: ' + tries); console.log('Trying to fetch machine IP, tries left: ' + tries);
ip = await machine.ip(); ip = await machine.ip();
tries -= 1;
await Promise.delay(1000); await Promise.delay(1000);
} catch (err) {} } catch (err) {}
} }
@ -125,11 +154,12 @@ export default {
break; break;
} catch (error) { } catch (error) {
router.get().transitionTo('setup'); router.get().transitionTo('setup');
metrics.track('Setup Failed', {
let novtx = error.message.indexOf('This computer doesn\'t have VT-X/AMD-v enabled') !== -1;
metrics.track(novtx ? 'Setup Halted' : 'Setup Failed', {
virtualBoxVersion, virtualBoxVersion,
machineVersion machineVersion
}); });
setupServerActions.error({error});
let message = error.message.split('\n'); let message = error.message.split('\n');
let lastLine = message.length > 1 ? message[message.length - 2] : 'Docker Machine encountered an error.'; let lastLine = message.length > 1 ? message[message.length - 2] : 'Docker Machine encountered an error.';
@ -142,6 +172,8 @@ export default {
groupingHash: machineVersion groupingHash: machineVersion
}, 'info'); }, 'info');
setupServerActions.error({error: new Error(message)});
this.clearTimers(); this.clearTimers();
await this.pause(); await this.pause();
} }

View File

@ -1,30 +1,29 @@
import exec from 'exec';
import child_process from 'child_process'; import child_process from 'child_process';
import Promise from 'bluebird'; import Promise from 'bluebird';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
import remote from 'remote'; import remote from 'remote';
var dialog = remote.require('dialog');
var app = remote.require('app'); var app = remote.require('app');
module.exports = { module.exports = {
exec: function (args, options) { execFile: function (args, options) {
options = options || {};
// Add resources dir to exec path for Windows
if (this.isWindows()) {
options.env = options.env || {};
if (!options.env.PATH) {
options.env.PATH = process.env.RESOURCES_PATH + ';' + process.env.PATH;
}
}
let fn = Array.isArray(args) ? exec : child_process.exec;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fn(args, options, (stderr, stdout, code) => { child_process.execFile(args[0], args.slice(1), options, (error, stdout) => {
if (code) { if (error) {
var cmd = Array.isArray(args) ? args.join(' ') : args; reject(new Error('Encountered an error: ' + error));
reject(new Error(cmd + ' returned non zero exit code. Stderr: ' + stderr)); } else {
resolve(stdout);
}
});
});
},
exec: function (args, options) {
return new Promise((resolve, reject) => {
child_process.exec(args, options, (error, stdout) => {
if (error) {
reject(new Error('Encountered an error: ' + error));
} else { } else {
resolve(stdout); resolve(stdout);
} }
@ -34,6 +33,9 @@ module.exports = {
isWindows: function () { isWindows: function () {
return process.platform === 'win32'; return process.platform === 'win32';
}, },
isLinux: function () {
return process.platform === 'linux';
},
binsPath: function () { binsPath: function () {
return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin'); return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin');
}, },
@ -156,5 +158,17 @@ module.exports = {
linuxToWindowsPath: function (linuxAbsPath) { linuxToWindowsPath: function (linuxAbsPath) {
return linuxAbsPath.replace('/c', 'C:').split('/').join('\\'); return linuxAbsPath.replace('/c', 'C:').split('/').join('\\');
}, },
linuxTerminal: function () {
if (fs.existsSync('/usr/bin/x-terminal-emulator')) {
return ['/usr/bin/x-terminal-emulator', '-e'];
} else {
dialog.showMessageBox({
type: 'warning',
buttons: ['OK'],
message: 'The terminal emulator symbolic link doesn\'t exists. Please read the Wiki at https://github.com/kitematic/kitematic/wiki/Common-Issues-and-Fixes#early-linux-support-from-zedtux.'
});
return;
}
},
webPorts: ['80', '8000', '8080', '8888', '3000', '5000', '2368', '9200', '8983'] webPorts: ['80', '8000', '8080', '8888', '3000', '5000', '2368', '9200', '8983']
}; };

View File

@ -16,13 +16,16 @@ var VirtualBox = {
} }
}, },
installed: function () { installed: function () {
if (util.isWindows() && !process.env.VBOX_INSTALL_PATH && !process.env.VBOX_MSI_INSTALL_PATH) {
return false;
}
return fs.existsSync(this.command()); return fs.existsSync(this.command());
}, },
active: function () { active: function () {
return fs.existsSync('/dev/vboxnetctl'); return fs.existsSync('/dev/vboxnetctl');
}, },
version: function () { version: function () {
return util.exec([this.command(), '-v']).then(stdout => { return util.execFile([this.command(), '-v']).then(stdout => {
let matchlist = stdout.match(/(\d+\.\d+\.\d+).*/); let matchlist = stdout.match(/(\d+\.\d+\.\d+).*/);
if (!matchlist || matchlist.length < 2) { if (!matchlist || matchlist.length < 2) {
Promise.reject('VBoxManage -v output format not recognized.'); Promise.reject('VBoxManage -v output format not recognized.');
@ -32,29 +35,11 @@ var VirtualBox = {
return Promise.resolve(null); return Promise.resolve(null);
}); });
}, },
poweroffall: function () {
return util.exec(this.command() + ' list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} ' + this.command() + ' controlvm {} poweroff');
},
mountSharedDir: function (vmName, pathName, hostPath) { mountSharedDir: function (vmName, pathName, hostPath) {
return util.exec([this.command(), 'sharedfolder', 'add', vmName, '--name', pathName, '--hostpath', hostPath, '--automount']); return util.execFile([this.command(), 'sharedfolder', 'add', vmName, '--name', pathName, '--hostpath', hostPath, '--automount']);
},
killall: function () {
if (util.isWindows()) {
return this.poweroffall().then(() => {
return util.exec(['powershell.exe', '\"get-process VBox* | stop-process\"']);
}).catch(() => {});
} else {
return this.poweroffall().then(() => {
return util.exec(['pkill', 'VirtualBox']);
}).then(() => {
return util.exec(['pkill', 'VBox']);
}).catch(() => {
});
}
}, },
vmExists: function (name) { vmExists: function (name) {
return util.exec([this.command(), 'showvminfo', name]).then(() => { return util.execFile([this.command(), 'showvminfo', name]).then(() => {
return true; return true;
}).catch((err) => { }).catch((err) => {
return false; return false;

View File

@ -7,14 +7,15 @@
flex-direction: row; flex-direction: row;
padding: 1rem; padding: 1rem;
.left { .left {
width: 100%; display: flex;
flex: 0.9 1 0;
flex-direction: column; flex-direction: column;
margin-right: 1rem; margin-right: 1rem;
} }
.right { .right {
display: flex;
flex: 0.1 0 300px;
width: 40%; width: 40%;
min-width: 200px;
max-width: 600px;
flex-direction: column; flex-direction: column;
} }
.full { .full {
@ -103,7 +104,6 @@
color: @gray-lightest; color: @gray-lightest;
font-family: @font-code; font-family: @font-code;
font-size: 10px; font-size: 10px;
white-space: pre-wrap;
-webkit-user-select: text; -webkit-user-select: text;
padding: 1.2rem 1.2rem 5rem 1.2rem; padding: 1.2rem 1.2rem 5rem 1.2rem;
overflow: auto; overflow: auto;

View File

@ -23,8 +23,8 @@
@color-box-button: lighten(@gray-lightest, 5%); @color-box-button: lighten(@gray-lightest, 5%);
@color-background: lighten(@gray-lightest, 4.5%); @color-background: lighten(@gray-lightest, 4.5%);
@font-regular: "Helvetica Neue", Segoe UI, Arial, "Lucida Grande", sans-serif; @font-regular: "Helvetica Neue", Segoe UI, "Ubuntu", Arial, "Lucida Grande", sans-serif;
@font-code: Menlo, Consolas; @font-code: Menlo, Consolas, "DejaVu Sans Mono";
@border-radius: 0.2rem; @border-radius: 0.2rem;