Skip to content

Commit

Permalink
Add server-side socket support
Browse files Browse the repository at this point in the history
-Refactor into REST (node.js) and socket (node_app_socket.js)
-File node_apps.js now only deals with app management
  • Loading branch information
mamacdon committed Dec 11, 2012
1 parent 2e964c7 commit 5ef6b76
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 178 deletions.
165 changes: 23 additions & 142 deletions lib/node.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,181 +6,62 @@ var url = require('url');
var api = require('./api'), write = api.write, writeError = api.writeError; var api = require('./api'), write = api.write, writeError = api.writeError;
var fileUtil = require('./fileUtil'); var fileUtil = require('./fileUtil');
var resource = require('./resource'); var resource = require('./resource');
var node_apps = require('./node_apps'), App = node_apps.App, AppTable = node_apps.AppTable; var node_apps = require('./node_apps');

var PATH_TO_NODE = process.execPath;
var INSPECT_PORT = 8900;


function printError(e) { function printError(e) {
console.log('err' + e); console.log('err' + e);
} }


function spawnNode(modulePath, args) { function getDecoratedAppJson(req, nodeRoot, appContext, app) {
var child = child_process.spawn(PATH_TO_NODE, [modulePath].concat(Array.isArray(args) ? args : []), { var json = app.toJson();
cwd: path.dirname(modulePath) // weird/maybe wrong: cwd is module dir, not the Orion Shell's cwd var requestUrl = url.parse(req.url);
json.Location = url.format({
host: req.headers.host,
pathname: requestUrl.pathname + '/' + app.pid
}); });
child.on('error', printError); return json;
return child;
} }


module.exports = function(options) { module.exports = function(options) {
var nodeRoot = options.root; var nodeRoot = options.root;
var fileRoot = options.fileRoot; var appContext = options.appContext;
var workspaceDir = options.workspaceDir; if (!nodeRoot || !appContext) { throw 'Missing "nodeRoot" or "appContext" parameter'; }
if (!nodeRoot) { throw 'options.root path required'; }
var appTable = new AppTable();
var inspector = null;

function safeModulePath(p) {
var filePath = api.rest(fileRoot, p);
return fileUtil.safeFilePath(workspaceDir, filePath);
}

function startInspectorApp(req, modulePath, args, restartOnExit){
var child, app;
try {
child = spawnNode(modulePath, args);
var location = url.resolve(url.format({
protocol: req.protocol, // TODO this may be bad
host: req.headers.host,
pathname: 'node-inspector' + '/'
}), './' + child.pid);
app = new App(child.pid, location, child);
appTable.put(child.pid, app);
child.on('exit', function() {
console.log('exit # ' + child.pid);
appTable.remove(child.pid);
if(restartOnExit){
startInspectorApp(req, modulePath, args, restartOnExit);
}
});
return app;
} catch (e) {
console.log(e.stack || e);
appTable.remove(child.pid);
app.stop();
return null;
}
}

function startApp(req, res, modulePath, args, debugInfo) {
var child, app;
try {
child = spawnNode(modulePath, args);
var location = url.resolve(url.format({
protocol: req.protocol, // TODO this may be bad
host: req.headers.host,
pathname: nodeRoot + '/'
}), './' + child.pid);

app = new App(child.pid, location, child, debugInfo);
appTable.put(child.pid, app);
child.on('exit', function() {
console.log('exit # ' + child.pid);
appTable.remove(child.pid);
});
write(201, res, { Location: location }, app.toJson());
return app;
} catch (e) {
console.log(e.stack || e);
appTable.remove(child.pid);
app.stop();
return null;
}
}


/**
* @param {HttpRequest} req
* @param {HttpResponse} res
* @param {Function} next
* @param {String} rest
*/
return resource(nodeRoot, { return resource(nodeRoot, {
/**
* @param {HttpRequest} req
* @param {HttpResponse} res
* @param {Function} next
* @param {String} rest
*/
GET: function(req, res, next, rest) { GET: function(req, res, next, rest) {
var pid = rest; var pid = rest;
if (pid === '') { if (pid === '') {
write(200, res, null, { write(200, res, null, {
Apps: appTable.apps().map(function(app) { Apps: appContext.appTable.apps().map(function(app) {
return app.toJson(); return getDecoratedAppJson(req, nodeRoot, appContext, app);
}) })
}); });
} else { } else {
var app = appTable.get(pid); var app = appContext.appTable.get(pid);
if (!app) { if (!app) {
writeError(404, res); writeError(404, res);
return; return;
} }
write(200, res, null, app.toJson()); write(200, res, null, getDecoratedAppJson(req, nodeRoot, appContext, app));
}
},
POST: function(req, res, next, rest) {
function checkPath(modulePath) {
if (typeof modulePath !== 'string') {
writeError(400, res, 'Missing parameter "modulePath"');
return false;
}
return true;
}
function checkArgs(args) {
if (args && !Array.isArray(args)) {
writeError(400, res, 'Parameter "args" must be an array, or omitted');
return false;
}
return true;
}
function checkPort(port) {
if (typeof port !== 'number') {
writeError(400, res, 'Parameter "port" must be a number');
return false;
}
return true;
}
var params = req.body, modulePath = params.modulePath, args = params.args, port;
if (rest === 'start') {
if (checkPath(modulePath) && checkArgs(args)) {
startApp(req, res, safeModulePath(modulePath), args);
}
} else if (rest === 'debug') {
port = params.port;
if (checkPath(modulePath) && checkPort(port)) {
modulePath = safeModulePath(modulePath);
var parsedOrigin = url.parse(req.headers.origin);
var debugInfo = url.resolve(url.format({
protocol: parsedOrigin.protocol,
hostname: parsedOrigin.hostname,
port: INSPECT_PORT + '/'
}), './' + "debug?port=" + port);

var app = startApp(req, res, modulePath, ["--debug-brk=" + port].concat(modulePath), debugInfo);
//Lazy spawn the node inspector procees for the first time when user wants to debug an app.
if(app && !inspector){
modulePath = require.resolve('node-inspector/bin/inspector');
var inspectorArg = [modulePath].concat("--web-port=" + INSPECT_PORT);
inspector = startInspectorApp(req, modulePath, inspectorArg, true);
}
}
} else if (rest === 'debug_inspect') {
port = params.port;
if (checkPort(port) && !inspector) {
modulePath = require.resolve('node-inspector/bin/inspector');
var inspectorArg = [modulePath].concat("--web-port=" + port);
inspector = startApp(req, res, modulePath, inspectorArg, true);
}
} else {
write(400, res);
} }
}, },
// POST: No POST for apps -- starting apps is handled by a Web Socket connection
DELETE: function(req, res, next, rest) { DELETE: function(req, res, next, rest) {
if (rest === '') { if (rest === '') {
writeError(400, res); writeError(400, res);
return; return;
} }
var pid = rest; var pid = rest, app = appContext.appTable.get(pid);
var app = appTable.remove(pid);
if (!app) { if (!app) {
writeError(404, res); writeError(404, res);
} else { } else {
app.stop(); appContext.stopApp(app);
write(204, res); write(204, res);
} }
} }
Expand Down
89 changes: 89 additions & 0 deletions lib/node_app_socket.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,89 @@
/*global Buffer exports module require*/
/*jslint devel:true*/
var api = require('./api');

function emitError(socket, error) {
socket.emit('error', error && error.stack);
}

/**
* Forwards events:
* app 'stdout' -> socket 'stdout'
* app 'stderr' -> socket 'stderr'
*/
function pipeStreams(app, socket) {
var proc = app.process;
// TODO: This forces proc output to be interpreted as UTF-8. Should send binary data to client and let them deal with it
proc.stdout.setEncoding('utf8');
proc.stderr.setEncoding('utf8');
var streamHandler = function(type, data) {
data = Buffer.isBuffer(data) ? data.toString('base64') : data;
socket.emit(type, data);
};
var stdoutListener = streamHandler.bind(null, 'stdout');
var stderrListener = streamHandler.bind(null, 'stderr');
app.on('stdout', stdoutListener);
app.on('stderr', stderrListener);
app.on('exit', function() {
app.removeListener('stdout', stdoutListener);
app.removeListener('stderr', stderrListener);
});
}

function checkParamType(args, name, type) {
if (typeof args[name] !== type) {
throw new Error('Missing parameter "' + name + '"');
}
}

function checkPort(port) {
if (typeof port !== 'number') {
throw new Error('Parameter "port" must be a number');
}
return true;
}

exports.install = function(options) {
var io = options.io, appContext = options.appContext;
if (!io || !appContext) {
throw new Error('Missing "io" or "appContext"');
}
io.sockets.on('connection', function(socket) {
var handshakeData = socket.handshake;
socket.on('start', function(data) {
try {
checkParamType(data, 'modulePath', 'string');
var app = appContext.startApp(data.modulePath, data.args);
pipeStreams(app, socket);
app.on('exit', function(c) {
socket.emit('stopped', app.toJson());
});
socket.emit('started', app.toJson());
} catch (error) {
console.log(error && error.stack);
emitError(socket, error);
}
});
socket.on('debug', function(data) {
try {
checkParamType(data, 'modulePath', 'string');
checkParamType(data, 'port', 'number');
var app = appContext.debugApp(data.modulePath, data.port, handshakeData.headers, handshakeData.url);
pipeStreams(app, socket);
app.on('exit', function(c) {
socket.emit('stopped', app.toJson());
});
socket.emit('started', app.toJson());
} catch (error) {
console.log(error && error.stack);
emitError(socket, error);
}
});
socket.on('disconnect', function() {
// stop piping?
});
});
};

exports.uninstall = function() {
};
Loading

0 comments on commit 5ef6b76

Please sign in to comment.