Skip to content

Commit

Permalink
WIP: Create node app to act as proxy in front of kernel gateway.
Browse files Browse the repository at this point in the history
Ref jupyter#135

(c) Copyright IBM Corp. 2015
  • Loading branch information
jhpedemonte committed Nov 23, 2015
1 parent 0b05e9e commit ba7f6ca
Show file tree
Hide file tree
Showing 22 changed files with 799 additions and 0 deletions.
4 changes: 4 additions & 0 deletions node-deployed-dashboard/.bowerrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"directory": "public/components",
"json": "bower.json"
}
10 changes: 10 additions & 0 deletions node-deployed-dashboard/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
3 changes: 3 additions & 0 deletions node-deployed-dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
public/components
.sass-cache
82 changes: 82 additions & 0 deletions node-deployed-dashboard/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@


var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var exphbs = require('express-handlebars');
var config = require('./config/config');

var routes = require('./routes/index');
var apiRoutes = require('./routes/api');
// var users = require('./routes/user');

var kernelProxy = require('./app/kernel-proxy');

var app = express();

var env = process.env.NODE_ENV || 'development';
app.locals.ENV = env;
app.locals.ENV_DEVELOPMENT = env == 'development';

// view engine setup

app.engine('handlebars', exphbs({
defaultLayout: 'main',
partialsDir: ['views/partials/']
}));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'handlebars');

// app.use(favicon(__dirname + '/public/img/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
app.use('/api', apiRoutes);
// app.use('/users', users);

/// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

/// error handlers

// development error handler
// will print stacktrace

if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err,
title: 'error'
});
});
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {},
title: 'error'
});
});

// kernelProxy.init(config);

module.exports = app;
128 changes: 128 additions & 0 deletions node-deployed-dashboard/app/kernel-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
var Services = require('jupyter-js-services');
var request = require('request');
var Promise = require('es6-promise').Promise;
var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
var WebSocket = require('ws');

global.XMLHttpRequest = XMLHttpRequest;
global.WebSocket = WebSocket;

function init(config) {
var baseUrl = config.kernelGatewayUrl;
baseUrl += (baseUrl[baseUrl.length - 1] === '/' ? '' : '/'); // ensure it ends in '/'

// check if we need to spawn a new container or use existing one
// check for a previous container, which we can reuse
// var containerUrl = localStorage.getItem(CONTAINER_URL);
// TODO get CONTAINER_URL from request
var containerUrl = null;

// TODO return CONTAINER_URL to the client
return checkExistingContainer(containerUrl)
.then(function(containerExists) {
if (!containerExists) {
console.log('spawning! [', baseUrl, ']');
return callSpawn(baseUrl);
} else {
return { url: containerUrl };
}
})
.then(function(data) {
var kernelOptions = {
baseUrl: data.url,
wsUrl: data.url.replace(/^http/, 'ws'),
name: 'python3'
};
console.log('starting new kernel at', data.url);
Services.startNewKernel(kernelOptions)
.then(function(kernel) {
setupKernel(kernel);
return true;
}).catch(function(e) {
console.error('Failed to create session/kernel:', e);
});
});
}

// generates a session URL containing a unique id
function generateSessionUrl() {
// TODO get path from request
return window.location.pathname + '#' + generateId();
}

// adapted from http://guid.us/GUID/JavaScript
function generateId() {
return (((1+Math.random())*0x100000000)|0).toString(16).substring(1);
}

// register event handlers on a new kernel
function setupKernel(_kernel) {
console.log('> setupKernel');
kernel = _kernel;

kernel.commOpened.connect(function(_kernel, commMsg) {
console.log('> commOpened');
var comm = kernel.connectToComm(commMsg.target_name, commMsg.comm_id);
});
}

// Spawn a new kernel container. Returns promise to container URL.
function callSpawn(baseUrl) {
return new Promise(function(resolve, reject) {
request.post({
url: baseUrl + 'api/spawn/',
json: true
// body: { image_name: 'jupyter/notebook' }, // TODO remove if not necessary
}, function(error, response, body) {
// console.log(response);
if (!error && response.statusCode >= 200 && response.statusCode < 300) {
console.log(body);
if (body.status === 'full') {
reject('tmpnb server is full');
// throw new Error('tmpnb server is full');
} else {
// test if we have a full URL (new tmpnb) or partial (old tmpnb)
if (!/^http/.test(body.url)) {
body.url = baseUrl + body.url;
}
resolve(body);
}
} else if (error) {
reject('error spawning tmpnb container' + error);
// throw new Error('error spawning tmpnb container', error);
}
});
});
}

// Check if container at URL is valid. Returns promise to boolean value.
function checkExistingContainer(url) {
return new Promise(function(resolve, reject) {
if (!url) {
return resolve(false); // There is no existing container
}

url += (url[url.length - 1] === '/' ? '' : '/'); // ensure it ends in '/'

request.get(url + 'api/kernels', function(error, response, body) {
if (!error && response.statusCode == 200) {
try {
var data = JSON.parse(body);
resolve(true);
} catch(e) {
// JSON conversion failed, most likely because response is HTML text telling us
// that the container doesn't exist. Therefore, fail.
resolve(false);
// TODO let client know to clear local storage for CONTAINER_URL
}
} else if (error) {
resolve(false);
// TODO let client know to clear local storage for CONTAINER_URL
}
});
});
}

module.exports = {
init: init
};
8 changes: 8 additions & 0 deletions node-deployed-dashboard/bin/www
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env node
var app = require('../app');

app.set('port', process.env.PORT || 3000);

var server = app.listen(app.get('port'), function() {
console.log('Express server listening on port ' + server.address().port);
});
9 changes: 9 additions & 0 deletions node-deployed-dashboard/bower.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "node-deployed-dashboard",
"version": "0.0.1",
"ignore": [
"**/.*",
"node_modules",
"components"
]
}
7 changes: 7 additions & 0 deletions node-deployed-dashboard/config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Copy to `config.js` and enter correct values.
*/

module.exports = {
kernelGatewayUrl: 'https://tmpnb-urth-demo.cloudet.xyz/'
};
7 changes: 7 additions & 0 deletions node-deployed-dashboard/config/config.js.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Copy to `config.js` and enter correct values.
*/

module.exports = {
kernelGatewayUrl: ''
};
Loading

0 comments on commit ba7f6ca

Please sign in to comment.