416 lines
13 KiB
JavaScript
416 lines
13 KiB
JavaScript
// -*- mode: js2; js-run: "swank-handler-tests.js" -*-
|
|
//
|
|
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions
|
|
// are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
//
|
|
// * Redistributions in binary form must reproduce the above
|
|
// copyright notice, this list of conditions and the following
|
|
// disclaimer in the documentation and/or other materials
|
|
// provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
|
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
var EventEmitter = require("events").EventEmitter;
|
|
var Script = process.binding('evals').Script;
|
|
var evalcx = Script.runInContext;
|
|
var util = require("util");
|
|
var url = require("url");
|
|
var assert = process.assert;
|
|
var lisp = require("./lisp");
|
|
var S = lisp.S, list = lisp.list, consp = lisp.consp, car = lisp.car, cdr = lisp.cdr,
|
|
repr = lisp.repr, fromLisp = lisp.fromLisp, toLisp = lisp.toLisp;
|
|
|
|
var DEFAULT_SLIME_VERSION = "2010-11-13";
|
|
|
|
function Handler (executive) {
|
|
this.executive = executive;
|
|
var self = this;
|
|
this.executive.on("output", function (str) { self.output(str); });
|
|
this.executive.on("newPackage", function (name) { self.newPackage(name); });
|
|
};
|
|
|
|
util.inherits(Handler, EventEmitter);
|
|
|
|
Handler.prototype.receive = function receive (message) {
|
|
// FIXME: error handling
|
|
console.log("Handler.prototype.receive(): %s", repr(message).replace(/\n/, "\\n"));
|
|
if (!consp(message) || car(message) != S(":emacs-rex")) {
|
|
console.log("bad message: %s", message);
|
|
return;
|
|
}
|
|
var d, expr;
|
|
try {
|
|
d = fromLisp(message, ["S:op", ">:form",
|
|
["S:name", "R*:args"],
|
|
"_:package", "_:threadId", "N:id"]);
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
console.log("failed to parse %s: %s", message, e);
|
|
return; // FIXME
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
var r = { status: ":ok", result: null };
|
|
var self = this;
|
|
var cont = function cont () {
|
|
self.sendResponse({ r: r, id: d.id },
|
|
[S(":return"), ">:r", ["S:status", "_:result"], "N:id"]);
|
|
};
|
|
|
|
switch (d.form.name) {
|
|
case "swank:connection-info":
|
|
this.executive.connectionInfo(
|
|
function (info) {
|
|
console.log("info = %j", info);
|
|
r.result = toLisp(
|
|
info,
|
|
{ "pid": "N:pid",
|
|
"encoding": { name: "encoding", spec: { "coding-system": "s:codingSystem",
|
|
"external-format": "s:externalFormat" } },
|
|
"package": { name: "packageSpec", spec: { name: "s", prompt: "s" } },
|
|
"lisp-implementation": {
|
|
name: "implementation",
|
|
spec: { type: "s", name: "s", version: "s" } },
|
|
"version": "s:version" });
|
|
cont();
|
|
});
|
|
return;
|
|
case "swank:create-repl":
|
|
r.result = toLisp(this.executive.createRepl(), ["s:packageName", "s:prompt"]);
|
|
break;
|
|
case "swank:autodoc":
|
|
r.result = S(":not-available");
|
|
break;
|
|
case "js:list-remotes":
|
|
// FIXME: support 'list of similar elements' type spec
|
|
r.result = toLisp(
|
|
this.executive.listRemotes().map(
|
|
function (item) {
|
|
return toLisp(item, ["N:index", "K:kind", "s:id", "B:isActive"]);
|
|
}, this));
|
|
break;
|
|
case "js:select-remote":
|
|
if (d.form.args.length != 2) {
|
|
console.log("bad args len for SWANK:SELECT-REMOTE -- %s", d.form.args.length);
|
|
return; // FIXME
|
|
}
|
|
// FIXME: get rid of spaghetti
|
|
var remoteIndex, sticky;
|
|
try {
|
|
// FIXME: args should be a cons / NIL
|
|
remoteIndex = fromLisp(d.form.args[0], "N");
|
|
sticky = fromLisp(d.form.args[1]);
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
console.log("can't parse arg -- %s", d.form.args[0]);
|
|
return; // FIXME
|
|
}
|
|
throw e;
|
|
}
|
|
this.executive.selectRemote(remoteIndex, sticky);
|
|
break;
|
|
case "js:set-target-url":
|
|
case "js:set-slime-version":
|
|
if (d.form.args.length != 1) {
|
|
console.log("bad args len for JS:SET-TARGET-URL -- %s", d.form.args.length);
|
|
return; // FIXME
|
|
}
|
|
try {
|
|
expr = fromLisp(d.form.args[0], "s");
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
console.log("can't parse arg -- %s", d.form.args[0]);
|
|
return; // FIXME
|
|
}
|
|
throw e;
|
|
}
|
|
this.executive[d.form.name == "js:set-target-url" ? "setTargetUrl" : "setSlimeVersion"](expr);
|
|
break;
|
|
case "swank:interactive-eval":
|
|
case "swank:listener-eval":
|
|
if (d.form.args.length != 1) {
|
|
console.log("bad args len for SWANK:LISTENER-EVAL -- %s", d.form.args.length);
|
|
return; // FIXME
|
|
}
|
|
try {
|
|
expr = fromLisp(d.form.args[0], "s");
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
console.log("can't parse arg -- %s", d.form.args[0]);
|
|
return; // FIXME
|
|
}
|
|
throw e;
|
|
}
|
|
this.executive.listenerEval(
|
|
expr, function (values) {
|
|
if (values.length)
|
|
r.result = toLisp({ values: values }, [S(":values"), "R:values"]);
|
|
cont();
|
|
});
|
|
return;
|
|
default:
|
|
// FIXME: handle unknown commands
|
|
}
|
|
cont();
|
|
};
|
|
|
|
Handler.prototype.output = function output (str) {
|
|
this.sendResponse([S(":write-string"), str]);
|
|
};
|
|
|
|
Handler.prototype.newPackage = function newPackage (name) {
|
|
this.sendResponse([S(":new-package"), name, name]);
|
|
};
|
|
|
|
Handler.prototype.sendResponse = function sendResponse(response, spec)
|
|
{
|
|
this.emit("response", repr(toLisp(response, spec || "@")));
|
|
};
|
|
|
|
function Remote () {};
|
|
|
|
util.inherits(Remote, EventEmitter);
|
|
|
|
Remote.prototype.prompt = function prompt () {
|
|
return "JS";
|
|
};
|
|
|
|
Remote.prototype.kind = function kind () {
|
|
throw new Error("must override Remote.prototype.kind()");
|
|
};
|
|
|
|
Remote.prototype.id = function id () {
|
|
throw new Error("must override Remote.prototype.id()");
|
|
};
|
|
|
|
Remote.prototype.evaluate = function evaluate (id, str) {
|
|
throw new Error("must override Remote.prototype.evaluate()");
|
|
};
|
|
|
|
Remote.prototype.fullName = function fullName () {
|
|
return "(" + this.kind() + ") " + this.id();
|
|
};
|
|
|
|
Remote.prototype.disconnect = function disconnect () {
|
|
this.emit("disconnect");
|
|
};
|
|
|
|
Remote.prototype.detachSelf = function detachSelf () {
|
|
this.removeAllListeners("output");
|
|
this.removeAllListeners("disconnect");
|
|
this.removeAllListeners("result");
|
|
};
|
|
|
|
Remote.prototype.output = function output (str) {
|
|
this.emit("output", String(str));
|
|
};
|
|
|
|
Remote.prototype.setIndex = function setIndex (n) {
|
|
this._index = n;
|
|
};
|
|
|
|
Remote.prototype.index = function index () {
|
|
return this._index;
|
|
};
|
|
|
|
Remote.prototype.sendResult = function sendResult (id, values) {
|
|
this.emit("result", id, values);
|
|
};
|
|
|
|
function DefaultRemote () {
|
|
this.context = Script.createContext();
|
|
for (var i in global) this.context[i] = global[i];
|
|
this.context.module = module;
|
|
this.context.require = require;
|
|
var self = this;
|
|
this.context._swank = {
|
|
output: function output (arg) {
|
|
self.output(arg);
|
|
}
|
|
};
|
|
}
|
|
|
|
util.inherits(DefaultRemote, Remote);
|
|
|
|
DefaultRemote.prototype.prompt = function prompt () {
|
|
return "NODE";
|
|
};
|
|
|
|
DefaultRemote.prototype.kind = function kind () {
|
|
return "direct";
|
|
};
|
|
|
|
DefaultRemote.prototype.id = function id () {
|
|
return "node.js";
|
|
};
|
|
|
|
DefaultRemote.prototype.evaluate = function evaluate (id, str) {
|
|
var r;
|
|
try {
|
|
r = evalcx(str, this.context, "repl");
|
|
} catch (e) {
|
|
r = undefined;
|
|
this.output(e.stack);
|
|
}
|
|
this.sendResult(id, r === undefined ? [] : [util.inspect(r)]);
|
|
};
|
|
|
|
// TBD: rename Executive to Dispatcher
|
|
function Executive (options) {
|
|
options = options || {};
|
|
assert(options.hasOwnProperty("config") && options.config);
|
|
this.config = options.config;
|
|
this.pid = options.hasOwnProperty("pid") ? options.pid : null;
|
|
this.remotes = [];
|
|
this.attachRemote(new DefaultRemote());
|
|
this.activeRemote = this.remotes[0];
|
|
this.pendingRequests = {};
|
|
};
|
|
|
|
util.inherits(Executive, EventEmitter);
|
|
|
|
Executive.nextId = 1; // request id counter is global in order to avoid inter-connection conflicts
|
|
|
|
Executive.nextRemoteIndex = 1;
|
|
|
|
Executive.prototype.attachRemote = function attachRemote (remote) {
|
|
assert(this.remotes.indexOf(remote) < 0);
|
|
remote.setIndex(Executive.nextRemoteIndex++);
|
|
|
|
var self = this;
|
|
remote.on(
|
|
"output", function (str) {
|
|
if (remote == self.activeRemote)
|
|
self.emit("output", str);
|
|
});
|
|
remote.on(
|
|
"disconnect", function (str) {
|
|
self.handleDisconnectRemote(remote);
|
|
});
|
|
remote.on(
|
|
"result", function (id, values) {
|
|
if (!self.pendingRequests[id]) {
|
|
self.emit("output", "WARNING: received late result from " + remote.fullName() + "\n");
|
|
return;
|
|
}
|
|
try {
|
|
self.pendingRequests[id](values);
|
|
} finally {
|
|
delete self.pendingRequests[id];
|
|
}
|
|
});
|
|
this.remotes.push(remote);
|
|
this.emit("output", "Remote attached: " + remote.fullName() + "\n");
|
|
|
|
this.config.get(
|
|
"stickyRemote",
|
|
function (stickyRemote) {
|
|
if (stickyRemote !== null &&
|
|
(!self.activeRemote || self.activeRemote.fullName() != stickyRemote) &&
|
|
remote.fullName() == stickyRemote)
|
|
self.selectRemote(remote.index(), true, true);
|
|
});
|
|
};
|
|
|
|
Executive.prototype.handleDisconnectRemote = function handleDisconnectRemote (remote) {
|
|
remote.detachSelf();
|
|
var index = this.remotes.indexOf(remote);
|
|
if (index < 0) {
|
|
this.emit("output", "WARNING: disconnectRemote() called for an unknown remote: " + remote.fullName() + "\n");
|
|
return;
|
|
}
|
|
this.remotes.splice(index, 1);
|
|
this.emit("output", "Remote detached: " + remote.fullName() + "\n");
|
|
if (remote == this.activeRemote)
|
|
this.selectRemote(this.remotes[0].index(), false, true);
|
|
};
|
|
|
|
Executive.prototype.connectionInfo = function connectionInfo (cont) {
|
|
var self = this;
|
|
var prompt = this.activeRemote.prompt();
|
|
this.config.get(
|
|
"slimeVersion",
|
|
function (slimeVersion) {
|
|
cont({ pid: self.pid === null ? process.pid : self.pid,
|
|
encoding: { codingSystem: "utf-8", externalFormat: "UTF-8" },
|
|
packageSpec: { name: prompt, prompt: prompt },
|
|
implementation: { type: "JS", name: "JS", version: "1.5" },
|
|
version: slimeVersion || DEFAULT_SLIME_VERSION });
|
|
});
|
|
};
|
|
|
|
Executive.prototype.createRepl = function createRepl () {
|
|
var prompt = this.activeRemote.prompt();
|
|
return { packageName: prompt, prompt: prompt };
|
|
};
|
|
|
|
Executive.prototype.listenerEval = function listenerEval (str, cont) {
|
|
var id = Executive.nextId++;
|
|
this.pendingRequests[id] = cont;
|
|
this.activeRemote.evaluate(id, str);
|
|
};
|
|
|
|
Executive.prototype.listRemotes = function listRemotes () {
|
|
return this.remotes.map(
|
|
function (remote) {
|
|
return { index: remote.index(), kind: remote.kind(), id: remote.id(),
|
|
isActive: remote === this.activeRemote };
|
|
}, this);
|
|
};
|
|
|
|
Executive.prototype.selectRemote = function selectRemote (index, sticky, auto) {
|
|
// TBD: sticky support (should autoselect the remote with message upon attachment)
|
|
for (var i = 0; i < this.remotes.length; ++i) {
|
|
var remote = this.remotes[i];
|
|
if (remote.index() == index) {
|
|
if (remote == this.activeRemote) {
|
|
this.emit("output", "WARNING: remote already selected: " + remote.fullName() + "\n");
|
|
return;
|
|
}
|
|
this.activeRemote = remote;
|
|
if (!auto)
|
|
this.config.set("stickyRemote", sticky ? remote.fullName() : null);
|
|
this.emit("newPackage", remote.prompt());
|
|
this.emit("output", "Remote selected" + (auto ? " (auto)" : sticky ? " (sticky)" : "") +
|
|
": " + remote.fullName() + "\n");
|
|
return;
|
|
}
|
|
}
|
|
this.emit("output", "WARNING: bad remote index\n");
|
|
};
|
|
|
|
Executive.prototype.setTargetUrl = function setTargetUrl (targetUrl) {
|
|
var parsedUrl = null;
|
|
try {
|
|
parsedUrl = url.parse(targetUrl);
|
|
} catch (e) {}
|
|
if (parsedUrl && parsedUrl.hostname)
|
|
this.config.set("targetUrl", targetUrl);
|
|
else
|
|
this.emit("output", "WARNING: the URL must contain host and port\n");
|
|
};
|
|
|
|
Executive.prototype.setSlimeVersion = function setSlimeVersion (slimeVersion) {
|
|
this.config.set("slimeVersion", slimeVersion);
|
|
};
|
|
|
|
exports.Handler = Handler;
|
|
exports.Remote = Remote;
|
|
exports.Executive = Executive;
|