nrepl-client
Version:
node client to interact with a Clojure nREPL server.
147 lines (123 loc) • 5.06 kB
JavaScript
/*global console,require,module,process,__dirname,setTimeout,clearTimeout*/
/*
* Depending on how you start the clojure nREPL server you don't need this.
* This will start a minimal nrepl via `lein repl :headless` to which the node
* client will connect.
*
*/
var path = require("path");
var ps = require("child_process");
var util = require("util");
var merge = util._extend;
// note, the JVM will stick around when we just kill the spawning process
// so we have to do a tree kill for the process. unfortunately the "tree-kill"
// lib is currently not working on Mac OS, so we need this little hack:
var kill = (process.platform === 'darwin') ?
function(pid, signal) {
ps.exec(util.format("ps a -o pid -o ppid |"
+ "grep %s | awk '{ print $1 }' |"
+ "xargs kill -s %s", pid, signal || 'SIGTERM'));
} : require('tree-kill');
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Server start implementation. Tries to detect timeouts
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function startSentinel(options, serverState, thenDo) {
var proc = serverState.proc,
thenDoCalled = false;
if (options.verbose) {
proc.on('close', function(code) { console.log("nREPL server stopped with code %s: %s", code); });
proc.on('error', function(error) { console.log("nREPL server error %s", error); });
proc.stdout.pipe(process.stdout);
proc.stderr.pipe(process.stdout);
}
proc.on('close', function(_) { serverState.exited = true; });
checkOutputForServerStart('nREPL server started on');
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// helper
function serverStartDance(serverOutput) {
grabHostnameAndPortFromOutput(serverOutput);
serverState.started = true;
thenDoCalled = true;
options.verbose && console.log('nREPL server started');
thenDo && thenDo(null, serverState);
}
function timeout() {
if (thenDoCalled) return;
thenDoCalled = true;
thenDo && thenDo(new Error("nrepl server start timeout"), null);
}
function checkOutputForServerStart(expectedOutput) {
var timeoutProc = setTimeout(timeout, options.startTimeout),
outListener = gatherOut("stdout", check),
errListener = gatherOut("stderr", check);
proc.stdout.on('data', outListener);
proc.stderr.on('data', errListener);
function check(string) {
if (string.indexOf(expectedOutput) === -1) return;
proc.stdout.removeListener('data', outListener);
proc.stderr.removeListener('data', errListener);
clearTimeout(timeoutProc);
serverStartDance(string);
}
}
function gatherOut(type, subscriber) {
return function(data) {
serverState[type] = Buffer.concat([serverState[type], data]);
subscriber(String(serverState[type]));
}
}
function grabHostnameAndPortFromOutput(output) {
if (!output) return
var match = output.match("on port ([0-9]+) on host ([^\s]+)");
if (!match) return;
if (match[1]) serverState.port = parseInt(match[1]);
if (match[2]) serverState.hostname = match[2];
}
}
function startServer(hostname, port, projectPath, thenDo) {
try {
var procArgs = ["repl", ":headless"];
if (hostname) procArgs = procArgs.concat([':host', hostname]);
if (port) procArgs = procArgs.concat([':port', port]);
var proc = ps.spawn('lein', procArgs, {cwd: projectPath});
} catch (e) { thenDo(e, null); return; }
thenDo(null, {
proc: proc,
stdout: new Buffer(""),
stderr: new Buffer(""),
hostname: undefined, port: undefined, // set when started
started: false,
exited: false,
timedout: undefined
});
}
// -=-=-=-=-=-=-=-=-=-=-
// the actual interface
// -=-=-=-=-=-=-=-=-=-=-
var defaultOptions = {
startTimeout: 10*1000, // milliseconds
verbose: false,
projectPath: process.cwd(),
// if host / port stay undefined they are choosen by leiningen
hostname: undefined,
port: undefined
}
function start(options, thenDo) {
options = merge(merge({}, defaultOptions), options);
startServer(options.hostname, options.port,
options.projectPath, function(err, serverState) {
if (err) thenDo(err, null);
else startSentinel(options, serverState, thenDo);
});
}
function stop(serverState, thenDo) {
if (serverState.exited) { thenDo(null); return; }
// FIXME what if when kill doesn't work? At least attach to `close` and
// throw a time out error...
kill(serverState.proc.pid, 'SIGTERM');
serverState.proc.once('close', function() {
console.log("Stopped nREPL server with pid %s", serverState.proc.pid);
thenDo && thenDo(null);
});
}
module.exports = {start: start, stop: stop};