// -*- 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;