console: added websocket support, PoC web console.
authorKrisztian Litkey <kli@iki.fi>
Fri, 8 Feb 2013 21:17:05 +0000 (23:17 +0200)
committerKrisztian Litkey <kli@iki.fi>
Fri, 8 Feb 2013 21:17:05 +0000 (23:17 +0200)
src/Makefile.am
src/plugins/console/console.html [new file with mode: 0644]
src/plugins/console/console.js [new file with mode: 0644]
src/plugins/console/plugin-console.c [new file with mode: 0644]

index 3951b68..5a3488c 100644 (file)
@@ -1062,7 +1062,7 @@ endif
 endif
 
 # console plugin
-CONSOLE_PLUGIN_REGULAR_SOURCES = plugins/plugin-console.c
+CONSOLE_PLUGIN_REGULAR_SOURCES = plugins/console/plugin-console.c
 CONSOLE_PLUGIN_SOURCES         = $(CONSOLE_PLUGIN_REGULAR_SOURCES) \
                                 plugin-console-func-info.c
 CONSOLE_PLUGIN_CFLAGS                 =
diff --git a/src/plugins/console/console.html b/src/plugins/console/console.html
new file mode 100644 (file)
index 0000000..f81fac2
--- /dev/null
@@ -0,0 +1,88 @@
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15">
+
+<style type="text/css">
+textarea.console_input {
+    font-size: 12pt;
+    font-family: monospace;
+    background-color: black;
+    color: white;
+    resize: none;
+    overflow: hidden;
+}
+
+textarea.console_output {
+    font-size: 12pt;
+    font-family: monospace;
+    background-color: black;
+    color: white;
+    resize: none;
+}
+</style>
+
+<script type="text/javascript" src="console.js"></script>
+
+<title>Murphy Console (disconnected)</title>
+</head>
+
+
+<body onLoad="setupConsole();">
+
+<script type="text/javascript">
+
+var mrpc = null;
+
+function setupConsole () {
+    var cmds = {
+        connect:    cmd_connect,
+        disconnect: cmd_disconnect,
+        resize:     cmd_resize
+    };
+
+    mrpc = new MrpConsole("console_div", cmds);
+    var addr = mrpc.socketUri(document.URL);
+
+    console.log("Trying to connect to Murphy @ " + addr);
+
+    mrpc.connect(addr, "murphy");
+    mrpc.focus();
+
+    mrpc.onconnected = function () {
+        document.title = "Murphy Console @ " + addr;
+        mrpc.append("Connection to Murphy console established.\n");
+    };
+
+    mrpc.onclosed = function () {
+        document.title = "Murphy Console (disconnected)";
+        mrpc.append("Murphy console connection closed.\n");
+        mrpc.append("Use 'connect' to try to reconnect.\n");
+    };
+}
+
+
+function cmd_connect(args) {
+    var addr;
+
+    addr = mrpc.socketUri(document.URL);
+    console.log("Trying to reconnect...");
+    mrpc.connect(addr, "murphy");
+}
+
+
+function cmd_disconnect() {
+
+    console.log("Disconnecting...");
+    addr = mrpc.disconnect();
+}
+
+
+function cmd_resize(args) {
+    mrpc.resize(args[0], args[1]);
+}
+
+
+</script>
+
+<div id="console_div"></div>
+</body></html>
diff --git a/src/plugins/console/console.js b/src/plugins/console/console.js
new file mode 100644 (file)
index 0000000..9c38338
--- /dev/null
@@ -0,0 +1,444 @@
+/*
+ * An Ode to My Suckage in Javascript...
+ *
+ * It'd be nice if someone wrote a relatively simple readline-like + output
+ * javascript package (ie. not a full VT100 terminal emulator like termlib).
+ */
+
+
+/*
+ * custom console error type
+ */
+
+function MrpConsoleError(message) {
+    this.name    = "Murphy Console Error";
+    this.message = message;
+}
+
+
+/** Create a Murphy console, put it after next_elem_id. */
+function MrpConsole(next_elem_id, user_commands) {
+    var sck, input, output, div, next_elem;
+
+    this.reset();
+    this.commands = user_commands;
+
+    /* create output text area */
+    output = document.createElement("textarea");
+    output.console = this;
+    this.output    = output;
+
+    output.setAttribute("class"     , "console_output");
+    output.setAttribute("cols"      , 80);
+    output.setAttribute("rows"      , 25);
+    output.setAttribute("readonly"  , true);
+    output.setAttribute("disabled"  , true);
+    output.setAttribute("spellcheck", false);
+    output.nline   = 0;
+    output.onfocus = function () { return false; }
+
+    /* create input text area, hook up input handler */
+    input = document.createElement("textarea");
+    input.console = this;
+    this.input    = input;
+
+    input.setAttribute("class"     , "console_input");
+    input.setAttribute("cols"      , 81);
+    input.setAttribute("rows"      ,  1);
+    input.setAttribute("spellcheck", false);
+    input.setAttribute("autofocus" , "autofocus");
+
+    input.onkeyup    = this.onkeyup;
+    input.onkeypress = this.onkeypress;
+
+    next_elem = document.getElementById(next_elem_id);
+
+    if (!next_elem)
+        throw new MrpConsoleError("element " + next_elem_id + " not found");
+
+    div = document.createElement("div");
+    div.appendChild(output);
+    div.appendChild(input);
+
+    /* insert console div to document */
+    document.body.insertBefore(div, next_elem);
+
+    this.setInput("");
+}
+
+
+/** Reset/initialize internal state to the disconnected defaults. */
+MrpConsole.prototype.reset = function () {
+    this.connected = false;
+    this.server    = null;
+    this.sck       = null;
+
+    this.history    = new Array ();
+    this.histidx    = 0;
+    if (!this.input)
+        this.prompt     = "disconnected";
+    else
+        this.setPrompt("disconnected");
+}
+
+
+/** Resize the console. */
+MrpConsole.prototype.resize = function (width, height) {
+    if (width && width > 0) {
+        this.output.cols = width;
+        this.input.cols  = width;
+    }
+    if (height && height > 0)
+        this.output.rows = height;
+}
+
+
+/** Get the focus to the console. */
+MrpConsole.prototype.focus = function () {
+    if (this.input)
+        this.input.focus();
+}
+
+
+/** Write output to the console, replacing its current contents. */
+MrpConsole.prototype.write = function (text, noscroll) {
+    var out = this.output;
+
+    out.value = text;
+    out.nline = text.split("\n").length;
+
+    if (!noscroll)
+        this.scrollBottom();
+}
+
+
+/** Append output to the console. */
+MrpConsole.prototype.append = function (text, noscroll) {
+    var out = this.output;
+
+    out.value += text;
+    out.nline += text.split("\n").length;
+
+    if (!noscroll)
+        this.scrollBottom();
+}
+
+
+/** Set the content of the input field to 'prompt> text'. */
+MrpConsole.prototype.setInput = function (text) {
+    this.input.value = this.prompt + "> " + text;
+}
+
+
+/** Get the current input value (without the prompt). */
+MrpConsole.prototype.getInput = function () {
+    if (this.input)
+        return this.input.value.slice(this.prompt.length + 2).split("\n")[0];
+    else
+        return "";
+}
+
+
+/** Set the input prompt to the given value. */
+MrpConsole.prototype.setPrompt = function (prompt) {
+    var value = this.getInput();
+
+    if (!this.input)
+        this.prompt = prompt;
+    else {
+        this.prompt = prompt;
+        this.input.value = this.prompt + "> " + value;
+    }
+}
+
+
+/** Scroll the output window up or down the given amount of lines. */
+MrpConsole.prototype.scroll = function (amount) {
+    var out = this.output;
+    var pxl = (out.nline ? (out.scrollHeight / out.nline) : 0);
+    var top = out.scrollTop + (amount * pxl);
+
+    if (top < 0)
+        top = 0;
+    if (top > out.scrollHeight)
+        top = out.scrollHeight;
+
+    out.scrollTop = top;
+}
+
+
+/** Scroll the output window up or down by a 'page'. */
+MrpConsole.prototype.scrollPage = function (dir) {
+    var out   = this.output;
+    var nline = 2 * 25 / 3;
+
+    if (dir < 0)
+        dir = -1;
+    else
+        dir = +1;
+
+    this.scroll(dir * nline);
+}
+
+
+/** Scroll to the bottom. */
+MrpConsole.prototype.scrollBottom = function () {
+    this.output.scrollTop = this.output.scrollHeight;
+}
+
+
+/** Add a new entry to the history. */
+MrpConsole.prototype.historyAppend = function (entry) {
+    if (entry.length > 0) {
+        this.history.push(entry);
+        this.histidx = this.history.length;
+
+        this.setInput("");
+    }
+}
+
+
+/** Go to the previous history entry. */
+MrpConsole.prototype.historyShow = function (dir) {
+    var idx = this.histidx + dir;
+
+    if (0 <= idx && idx < this.history.length) {
+        if (this.histidx == this.history.length &&
+            this.input.value.length > 0) {
+            /* Hmm... autoinsert to history, not the Right Thing To Do... */
+            this.historyAppend(this.getInput());
+        }
+
+        this.histidx = idx;
+        this.setInput(this.history[this.histidx]);
+    }
+    else if (idx >= this.history.length) {
+        this.histidx = this.history.length;
+        this.setInput("");
+    }
+}
+
+
+/** Make sure the input position never enters the prompt. */
+MrpConsole.prototype.checkInputPosition = function () {
+    var pos = this.input.selectionStart;
+
+    if (pos <= this.prompt.length + 2) {
+        this.input.selectionStart = this.prompt.length + 2;
+        this.input.selectionEnd   = this.prompt.length + 2;
+        return false;
+    }
+    else
+        return true;
+}
+
+
+/** Key up handler. */
+MrpConsole.prototype.onkeyup = function (e) {
+    var c = this.console;
+    var l;
+
+    /*console.log("got key " + e.which);*/
+
+    switch (e.keyCode) {
+    case e.DOM_VK_RETURN:
+        if (c.input.value.length > c.prompt.length + 2) {
+            l = c.getInput();
+            c.historyAppend(l);
+            c.processCmd(l);
+            c.setInput("");
+        }
+        break;
+    case e.DOM_VK_PAGE_UP:
+        if (e.shiftKey)
+            c.scrollPage(-1);
+        break;
+    case e.DOM_VK_PAGE_DOWN:
+        if (e.shiftKey)
+            c.scrollPage(+1);
+        break;
+
+    case e.DOM_VK_UP:
+        if (!e.shiftKey)
+            c.historyShow(-1);
+        else
+            c.scroll(-1);
+        break;
+    case e.DOM_VK_DOWN:
+        if (!e.shiftKey)
+            c.historyShow(+1);
+        else
+            c.scroll(+1);
+        break;
+    case e.DOM_VK_LEFT:
+    case e.DOM_VK_BACK_SPACE:
+        if (!c.checkInputPosition())
+            return false;
+        break;
+    }
+
+    return true;
+}
+
+
+/** Key-press handler. */
+MrpConsole.prototype.onkeypress = function (e) {
+    var c = this.console;
+    var l;
+
+    switch (e.which) {
+    case e.DOM_VK_LEFT:
+    case e.DOM_VK_BACK_SPACE:
+        if (!c.checkInputPosition())
+            return false;
+    }
+
+    return true;
+}
+
+
+/** Connect to the Murphy daemon running at the given address. */
+MrpConsole.prototype.connect = function (address) {
+    var c = this.console;
+
+    if (this.connected)
+        throw new MrpConsoleError("already connected to " + this.address);
+    else {
+        this.server    = address;
+        this.connected = false;
+
+        this.setPrompt("connecting");
+
+        if (typeof MozWebSocket != "undefined")
+            sck = new MozWebSocket(this.server, "murphy");
+        else
+            sck = new WebSocket(this.server, "murphy");
+
+        this.sck      = sck;
+        sck.console   = this;
+        sck.onopen    = this.sckconnect;
+        sck.onclose   = this.sckclosed;
+        sck.onerror   = this.sckerror;
+        sck.onmessage = this.sckmessage;
+    }
+}
+
+
+/** Close the console connection. */
+MrpConsole.prototype.disconnect = function () {
+    if (this.connected) {
+        this.sck.close();
+    }
+}
+
+
+/** Connection established event handler. */
+MrpConsole.prototype.sckconnect = function () {
+    var c = this.console;
+
+    c.connected = true;
+    c.setPrompt("connected");
+
+    if (c.onconnected)
+        c.onconnected();
+}
+
+
+/** Connection shutdown event handler. */
+MrpConsole.prototype.sckclosed = function () {
+    var c = this.console;
+
+    console.log("socket closed");
+
+    c.reset();
+
+    if (c.onclosed)
+        c.onclosed();
+}
+
+
+/** Socket error event handler. */
+MrpConsole.prototype.sckerror = function (e) {
+    var c = this.console;
+
+    c.reset();
+
+    if (c.onerror)
+        c.onerror();
+
+    if (c.onclosed)
+        c.onclosed();
+}
+
+
+/** Socket message event handler. */
+MrpConsole.prototype.sckmessage = function (message) {
+    var c   = this.console;
+    var msg = JSON.parse(message.data);
+
+    if (msg.prompt)
+        c.setPrompt(msg.prompt);
+    else {
+        if (msg.output) {
+            c.append(msg.output);
+        }
+    }
+}
+
+
+/** Send a request to the server. */
+MrpConsole.prototype.send_request = function (req) {
+    var sck = this.sck;
+
+    sck.send(JSON.stringify(req));
+}
+
+
+/** Process a command entered by the user. */
+MrpConsole.prototype.processCmd = function (cmd) {
+    var c, l, cb, args;
+
+    for (c in this.commands) {
+        l  = c.length;
+        cb = this.commands[c];
+        if (cmd.substring(0, l) == c && (cmd.length == l ||
+                                         cmd.substring(l, l + 1) == ' ')) {
+            args = cmd.split(' ').splice(1);
+
+            this.commands[c](args);
+
+            return;
+        }
+    }
+
+    this.append(this.prompt + "> " + cmd + "\n");
+    this.send_request({ input: cmd });
+
+    if (cmd == 'help') {
+        if (this.commands && Object.keys(this.commands).length > 0) {
+            this.append("Web console commands:\n");
+            for (c in this.commands) {
+                this.append("    " + c + "\n");
+            }
+        }
+        else
+            this.append("No Web console commands.");
+    }
+}
+
+
+/** Determine a WebSocket URI based on an HTTP URI. */
+MrpConsole.prototype.socketUri = function (http_uri) {
+    var proto, colon, rest;
+
+    colon = http_uri.indexOf(':');           /* get first colon */
+    proto = http_uri.substring(0, colon);    /* get protocol */
+    rest  = http_uri.substring(colon + 3);   /* get URI sans protocol:// */
+    addr  = rest.split("/")[0];              /* strip URI path from address */
+
+    switch (proto) {
+    case "http":  return "ws://"  + addr;
+    case "https": return "wss://" + addr;
+    default:      return null;
+    }
+}
diff --git a/src/plugins/console/plugin-console.c b/src/plugins/console/plugin-console.c
new file mode 100644 (file)
index 0000000..f3e0474
--- /dev/null
@@ -0,0 +1,727 @@
+/*
+ * Copyright (c) 2012, Intel Corporation
+ *
+ * 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.
+ *  * Neither the name of Intel Corporation nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS 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 COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <netdb.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <murphy/common.h>
+#include <murphy/core.h>
+
+#include "config.h"
+
+#ifdef WEBSOCKETS_ENABLED
+#    include <murphy/common/wsck-transport.h>
+#    include <murphy/common/json.h>
+#endif
+
+#include <murphy/plugins/console-protocol.h>
+
+#define DEFAULT_ADDRESS "unxs:@murphy-console"    /* default console address */
+#define DEFAULT_HTTPDIR "/etc/murphy/html"        /* default content dir */
+
+
+/*
+ * an active console instance
+ */
+
+typedef struct {
+    mrp_console_t   *mc;                 /* associated murphy console */
+    mrp_transport_t *t;                  /* associated transport */
+    mrp_sockaddr_t   addr;               /* for temp. datagram 'connection' */
+    socklen_t        alen;               /* address length if any */
+    int              id;                 /* console ID for log redirection */
+} console_t;
+
+
+/*
+ * console plugin data
+ */
+
+typedef struct {
+    const char      *address;            /* console address */
+    mrp_transport_t *t;                  /* transport we're listening on */
+    int              sock;               /* main socket for new connections */
+    mrp_io_watch_t  *iow;                /* main socket I/O watch */
+    mrp_context_t   *ctx;                /* murphy context */
+    mrp_list_hook_t  clients;            /* active console clients */
+    mrp_sockaddr_t   addr;               /* resolved transport address */
+    socklen_t        alen;               /* address length */
+    console_t       *c;                  /* datagram console being served */
+    const char      *httpdir;            /* WRT console agent directory */
+} data_t;
+
+
+
+static int next_id = 1;
+
+static ssize_t write_req(mrp_console_t *mc, void *buf, size_t size)
+{
+    console_t *c = (console_t *)mc->backend_data;
+    mrp_msg_t *msg;
+    uint16_t   tag, type;
+    uint32_t   len;
+
+    tag  = MRP_CONSOLE_OUTPUT;
+    type = MRP_MSG_FIELD_BLOB;
+    len  = size;
+    msg  = mrp_msg_create(tag, type, len, buf, NULL);
+
+    if (msg != NULL) {
+        mrp_transport_send(c->t, msg);
+        mrp_msg_unref(msg);
+
+        return size;
+    }
+    else
+        return -1;
+}
+
+
+static void logger(void *data, mrp_log_level_t level, const char *file,
+                   int line, const char *func, const char *format, va_list ap)
+{
+    console_t  *c = (console_t *)data;
+    va_list     cp;
+    const char *prefix;
+
+    MRP_UNUSED(file);
+    MRP_UNUSED(line);
+    MRP_UNUSED(func);
+
+    switch (level) {
+    case MRP_LOG_ERROR:   prefix = "[log] E: "; break;
+    case MRP_LOG_WARNING: prefix = "[log] W: "; break;
+    case MRP_LOG_INFO:    prefix = "[log] I: "; break;
+    case MRP_LOG_DEBUG:   prefix = "[log] D: "; break;
+    default:              prefix = "[log] ?: ";
+    }
+
+    va_copy(cp, ap);
+    mrp_console_printf(c->mc, "%s", prefix);
+    mrp_console_vprintf(c->mc, format, cp);
+    mrp_console_printf(c->mc, "\n");
+    va_end(cp);
+}
+
+
+static void register_logger(console_t *c)
+{
+    char name[32];
+
+    if (!c->id)
+        return;
+
+    snprintf(name, sizeof(name), "console/%d", c->id);
+    mrp_log_register_target(name, logger, c);
+}
+
+
+static void unregister_logger(console_t *c)
+{
+    char name[32];
+
+    if (!c->id)
+        return;
+
+    snprintf(name, sizeof(name), "console/%d", c->id);
+    mrp_log_unregister_target(name);
+}
+
+
+static void set_prompt_req(mrp_console_t *mc, const char *prompt)
+{
+    console_t *c = (console_t *)mc->backend_data;
+    mrp_msg_t *msg;
+    uint16_t   tag, type;
+
+    tag  = MRP_CONSOLE_PROMPT;
+    type = MRP_MSG_FIELD_STRING;
+    msg  = mrp_msg_create(tag, type, prompt, NULL);
+
+    if (msg != NULL) {
+        mrp_transport_send(c->t, msg);
+        mrp_msg_unref(msg);
+    }
+}
+
+
+static void free_req(void *backend_data)
+{
+    mrp_free(backend_data);
+}
+
+
+static void recv_cb(mrp_transport_t *t, mrp_msg_t *msg, void *user_data)
+{
+    console_t       *c = (console_t *)user_data;
+    mrp_msg_field_t *f;
+    char            *input;
+    size_t           size;
+
+    MRP_UNUSED(t);
+
+    if ((f = mrp_msg_find(msg, MRP_CONSOLE_INPUT)) != NULL) {
+        if (f->type == MRP_MSG_FIELD_BLOB) {
+            input = f->str;
+            size  = f->size[0];
+
+            if (size > 0) {
+                MRP_CONSOLE_BUSY(c->mc, {
+                        c->mc->evt.input(c->mc, input, size);
+                    });
+
+                c->mc->check_destroy(c->mc);
+            }
+
+            return;
+        }
+    }
+
+    mrp_log_warning("Ignoring malformed message from console/%d...", c->id);
+}
+
+
+static void recvfrom_cb(mrp_transport_t *t, mrp_msg_t *msg,
+                        mrp_sockaddr_t *addr, socklen_t alen, void *user_data)
+{
+    console_t       *c = (console_t *)user_data;
+    mrp_sockaddr_t   obuf;
+    socklen_t        olen;
+    mrp_msg_field_t *f;
+    char            *input;
+    size_t           size;
+
+    MRP_UNUSED(t);
+
+    if ((f = mrp_msg_find(msg, MRP_CONSOLE_INPUT)) != NULL) {
+        if (f->type == MRP_MSG_FIELD_BLOB) {
+            input = f->str;
+            size  = f->size[0];
+
+            if (size > 0) {
+                mrp_sockaddr_cpy(&obuf, &c->addr, olen = c->alen);
+                mrp_sockaddr_cpy(&c->addr, addr, c->alen = alen);
+                mrp_transport_connect(t, addr, alen);
+
+                MRP_CONSOLE_BUSY(c->mc, {
+                        c->mc->evt.input(c->mc, input, size);
+                    });
+
+                c->mc->check_destroy(c->mc);
+
+
+                mrp_transport_disconnect(t);
+
+                if (olen) {
+                    mrp_transport_connect(t, &obuf, olen);
+                    mrp_sockaddr_cpy(&c->addr, &obuf, c->alen = olen);
+                }
+
+                return;
+            }
+        }
+    }
+
+    mrp_log_warning("Ignoring malformed message from console/%d...", c->id);
+}
+
+
+/*
+ * generic stream transport
+ */
+
+#define stream_write_req      write_req
+#define stream_set_prompt_req set_prompt_req
+#define stream_free_req       free_req
+#define stream_recv_cb        recv_cb
+
+static void stream_close_req(mrp_console_t *mc)
+{
+    console_t *c = (console_t *)mc->backend_data;
+
+    if (c->t != NULL) {
+        mrp_transport_disconnect(c->t);
+        mrp_transport_destroy(c->t);
+        unregister_logger(c);
+
+        c->t = NULL;
+    }
+}
+
+
+static void stream_connection_cb(mrp_transport_t *lt, void *user_data)
+{
+    static mrp_console_req_t req;
+
+    data_t    *data = (data_t *)user_data;
+    console_t *c;
+    int        flags;
+
+    if ((c = mrp_allocz(sizeof(*c))) != NULL) {
+        flags = MRP_TRANSPORT_REUSEADDR | MRP_TRANSPORT_NONBLOCK;
+        c->t  = mrp_transport_accept(lt, c, flags);
+
+        if (c->t != NULL) {
+            req.write      = stream_write_req;
+            req.close      = stream_close_req;
+            req.free       = stream_free_req;
+            req.set_prompt = stream_set_prompt_req;
+
+            c->mc = mrp_create_console(data->ctx, &req, c);
+
+            if (c->mc != NULL) {
+                c->id = next_id++;
+                register_logger(c);
+
+                return;
+            }
+            else {
+                mrp_transport_destroy(c->t);
+                c->t = NULL;
+            }
+        }
+    }
+}
+
+
+static void stream_closed_cb(mrp_transport_t *t, int error, void *user_data)
+{
+    console_t *c = (console_t *)user_data;
+
+    if (error)
+        mrp_log_error("Connection to console/%d closed with error %d (%s).",
+                      c->id, error, strerror(error));
+    else {
+        mrp_log_info("console/%d has closed the connection.", c->id);
+
+        mrp_transport_disconnect(t);
+        mrp_transport_destroy(t);
+        unregister_logger(c);
+        c->t = NULL;
+    }
+}
+
+
+static int stream_setup(data_t *data)
+{
+    static mrp_transport_evt_t evt;
+
+    mrp_mainloop_t  *ml = data->ctx->ml;
+    mrp_transport_t *t;
+    const char      *type;
+    mrp_sockaddr_t   addr;
+    socklen_t        alen;
+    int              flags;
+
+    t    = NULL;
+    alen = sizeof(addr);
+    alen = mrp_transport_resolve(NULL, data->address, &addr, alen, &type);
+
+    if (alen <= 0) {
+        mrp_log_error("Failed to resolve console transport address '%s'.",
+                      data->address);
+
+        return FALSE;
+    }
+
+    evt.connection  = stream_connection_cb;
+    evt.closed      = stream_closed_cb;
+    evt.recvmsg     = stream_recv_cb;
+    evt.recvmsgfrom = NULL;
+
+    flags = MRP_TRANSPORT_REUSEADDR;
+    t     = mrp_transport_create(ml, type, &evt, data, flags);
+
+    if (t != NULL) {
+        if (mrp_transport_bind(t, &addr, alen) && mrp_transport_listen(t, 1)) {
+            data->t = t;
+
+            return TRUE;
+        }
+        else {
+            mrp_log_error("Failed to bind console to '%s'.", data->address);
+            mrp_transport_destroy(t);
+        }
+    }
+    else
+        mrp_log_error("Failed to create console transport.");
+
+    return FALSE;
+}
+
+
+/*
+ * datagram transports
+ */
+
+#define dgram_write_req       write_req
+#define dgram_free_req        free_req
+#define dgram_set_prompt_req  set_prompt_req
+
+#define dgram_recv_cb         recv_cb
+#define dgram_recvfrom_cb     recvfrom_cb
+
+
+static void dgram_close_req(mrp_console_t *mc)
+{
+    console_t *c = (console_t *)mc->backend_data;
+    mrp_msg_t *msg;
+    uint16_t   tag, type;
+
+    tag  = MRP_CONSOLE_BYE;
+    type = MRP_MSG_FIELD_BOOL;
+    msg  = mrp_msg_create(tag, type, TRUE, NULL);
+
+    if (msg != NULL) {
+        mrp_transport_send(c->t, msg);
+        mrp_msg_unref(msg);
+    }
+
+    mrp_transport_disconnect(c->t);
+}
+
+
+static int dgram_setup(data_t *data)
+{
+    static mrp_transport_evt_t evt;
+    static mrp_console_req_t   req;
+
+    mrp_mainloop_t  *ml = data->ctx->ml;
+    mrp_transport_t *t;
+    const char      *type;
+    mrp_sockaddr_t   addr;
+    socklen_t        alen;
+    int              flags;
+    console_t       *c;
+
+    t    = NULL;
+    alen = sizeof(addr);
+    alen = mrp_transport_resolve(NULL, data->address, &addr, alen, &type);
+
+    if (alen <= 0) {
+        mrp_log_error("Failed to resolve console transport address '%s'.",
+                      data->address);
+
+        return FALSE;
+    }
+
+    c = mrp_allocz(sizeof(*c));
+
+    if (c != NULL) {
+        evt.recvmsg     = dgram_recv_cb;
+        evt.recvmsgfrom = dgram_recvfrom_cb;
+        evt.connection  = NULL;
+        evt.closed      = NULL;
+
+        flags = MRP_TRANSPORT_REUSEADDR;
+        t     = mrp_transport_create(ml, type, &evt, c, flags);
+
+        if (t != NULL) {
+            if (mrp_transport_bind(t, &addr, alen)) {
+                req.write      = dgram_write_req;
+                req.close      = dgram_close_req;
+                req.free       = dgram_free_req;
+                req.set_prompt = dgram_set_prompt_req;
+
+                c->t  = t;
+                c->mc = mrp_create_console(data->ctx, &req, c);
+
+                if (c->mc != NULL) {
+                    data->c         = c;
+                    c->mc->preserve = TRUE;
+
+                    return TRUE;
+                }
+                else
+                    mrp_log_error("Failed to create console.");
+            }
+            else
+                mrp_log_error("Failed to bind console to '%s'.", data->address);
+
+            c->t = NULL;
+            mrp_transport_destroy(t);
+        }
+        else
+            mrp_log_error("Failed to create console transport.");
+
+        mrp_free(c);
+    }
+
+    return FALSE;
+}
+
+
+#ifdef WEBSOCKETS_ENABLED
+
+/*
+ * websocket transport
+ */
+
+#define wsock_close_req stream_close_req
+#define wsock_free_req  free_req
+#define wsock_closed_cb stream_closed_cb
+
+static ssize_t wsock_write_req(mrp_console_t *mc, void *buf, size_t size)
+{
+    console_t  *c = (console_t *)mc->backend_data;
+    mrp_json_t *msg;
+
+    msg = mrp_json_create(MRP_JSON_OBJECT);
+
+    if (msg != NULL) {
+        if (mrp_json_add_string_slice(msg, "output", buf, size))
+            mrp_transport_sendcustom(c->t, msg);
+
+        mrp_json_unref(msg);
+
+        return size;
+    }
+    else
+        return -1;
+}
+
+
+static void wsock_set_prompt_req(mrp_console_t *mc, const char *prompt)
+{
+    console_t  *c = (console_t *)mc->backend_data;
+    mrp_json_t *msg;
+
+    msg = mrp_json_create(MRP_JSON_OBJECT);
+
+    if (msg != NULL) {
+        if (mrp_json_add_string(msg, "prompt", prompt))
+            mrp_transport_sendcustom(c->t, msg);
+
+        mrp_json_unref(msg);
+    }
+}
+
+
+static void wsock_recv_cb(mrp_transport_t *t, void *data, void *user_data)
+{
+    console_t  *c   = (console_t *)user_data;
+    mrp_json_t *msg = (mrp_json_t *)data;
+    const char *s;
+    char       *input;
+    size_t      size;
+
+    MRP_UNUSED(t);
+
+    s = mrp_json_object_to_string((mrp_json_t *)data);
+
+    mrp_debug("recived WRT console message:");
+    mrp_debug("  %s", s);
+
+    if (mrp_json_get_string(msg, "input", &input)) {
+        size = strlen(input);
+
+        if (size > 0) {
+            MRP_CONSOLE_BUSY(c->mc, {
+                    c->mc->evt.input(c->mc, input, size);
+                });
+
+            c->mc->check_destroy(c->mc);
+        }
+    }
+}
+
+
+static void wsock_connection_cb(mrp_transport_t *lt, void *user_data)
+{
+    static mrp_console_req_t req;
+    data_t    *data = (data_t *)user_data;
+    console_t *c;
+
+    mrp_debug("incoming web console connection...");
+
+    if ((c = mrp_allocz(sizeof(*c))) != NULL) {
+        c->t = mrp_transport_accept(lt, c, 0);
+
+        if (c->t != NULL) {
+            req.write      = wsock_write_req;
+            req.close      = wsock_close_req;
+            req.free       = wsock_free_req;
+            req.set_prompt = wsock_set_prompt_req;
+
+            c->mc = mrp_create_console(data->ctx, &req, c);
+
+            if (c->mc != NULL) {
+                c->id = next_id++;
+                register_logger(c);
+
+                return;
+            }
+            else {
+                mrp_transport_destroy(c->t);
+                c->t = NULL;
+            }
+        }
+    }
+}
+
+
+static int wsock_setup(data_t *data)
+{
+    static mrp_transport_evt_t evt;
+
+    mrp_mainloop_t  *ml = data->ctx->ml;
+    mrp_transport_t *t;
+    const char      *type;
+    mrp_sockaddr_t   addr;
+    socklen_t        alen;
+    int              flags;
+
+    t    = NULL;
+    alen = sizeof(addr);
+    alen = mrp_transport_resolve(NULL, data->address, &addr, alen, &type);
+
+    if (alen <= 0) {
+        mrp_log_error("Failed to resolve console transport address '%s'.",
+                      data->address);
+
+        return FALSE;
+    }
+
+    evt.connection  = wsock_connection_cb;
+    evt.closed      = wsock_closed_cb;
+    evt.recvcustom  = wsock_recv_cb;
+    evt.recvmsgfrom = NULL;
+
+    flags = MRP_TRANSPORT_MODE_CUSTOM;
+    t     = mrp_transport_create(ml, type, &evt, data, flags);
+
+    if (t != NULL) {
+        if (mrp_transport_bind(t, &addr, alen) && mrp_transport_listen(t, 1)) {
+            mrp_transport_setopt(t, MRP_WSCK_OPT_HTTPDIR, data->httpdir);
+            data->t = t;
+
+            return TRUE;
+        }
+        else {
+            mrp_log_error("Failed to bind console to '%s'.", data->address);
+            mrp_transport_destroy(t);
+        }
+    }
+    else
+        mrp_log_error("Failed to create console transport.");
+
+    return FALSE;
+}
+
+#endif /* WEBSOCKETS_ENABLED */
+
+
+enum {
+    ARG_ADDRESS,                          /* console transport address */
+    ARG_HTTPDIR                           /* content directory for HTTP */
+};
+
+
+
+static int console_init(mrp_plugin_t *plugin)
+{
+    data_t *data;
+    int     ok;
+
+    if ((data = mrp_allocz(sizeof(*data))) != NULL) {
+        mrp_list_init(&data->clients);
+
+        data->ctx     = plugin->ctx;
+        data->address = plugin->args[ARG_ADDRESS].str;
+        data->httpdir = plugin->args[ARG_HTTPDIR].str;
+
+        mrp_log_info("Using console address '%s'...", data->address);
+
+        if (!strncmp(data->address, "wsck:", 5)) {
+            if (data->httpdir != NULL)
+                mrp_log_info("Using '%s' for serving console Web agent...",
+                             data->httpdir);
+            else
+                mrp_log_info("Not serving console Web agent...");
+        }
+
+        if (!strncmp(data->address, "tcp4:", 5) ||
+            !strncmp(data->address, "tcp6:", 5) ||
+            !strncmp(data->address, "unxs:", 5))
+            ok = stream_setup(data);
+#ifdef WEBSOCKETS_ENABLED
+        else if (!strncmp(data->address, "wsck:", 5))
+            ok = wsock_setup(data);
+#endif
+        else
+            ok = dgram_setup(data);
+
+        if (ok) {
+            plugin->data = data;
+
+            return TRUE;
+        }
+    }
+
+    mrp_free(data);
+
+    return FALSE;
+}
+
+
+static void console_exit(mrp_plugin_t *plugin)
+{
+    mrp_log_info("Cleaning up %s...", plugin->instance);
+}
+
+
+#define CONSOLE_DESCRIPTION "A debug console for Murphy."
+#define CONSOLE_HELP \
+    "The debug console provides a telnet-like remote session and a\n"     \
+    "simple shell-like command interpreter with commands to help\n"       \
+    "development, debugging, and trouble-shooting. The set of commands\n" \
+    "can be dynamically extended by registering new commands from\n"      \
+    "other plugins."
+
+#define CONSOLE_VERSION MRP_VERSION_INT(0, 0, 1)
+#define CONSOLE_AUTHORS "Krisztian Litkey <kli@iki.fi>"
+
+
+static mrp_plugin_arg_t console_args[] = {
+    MRP_PLUGIN_ARGIDX(ARG_ADDRESS, STRING, "address", DEFAULT_ADDRESS),
+    MRP_PLUGIN_ARGIDX(ARG_HTTPDIR, STRING, "httpdir", DEFAULT_HTTPDIR)
+};
+
+MURPHY_REGISTER_CORE_PLUGIN("console",
+                            CONSOLE_VERSION, CONSOLE_DESCRIPTION,
+                            CONSOLE_AUTHORS, CONSOLE_HELP, MRP_SINGLETON,
+                            console_init, console_exit,
+                            console_args, MRP_ARRAY_SIZE(console_args),
+                            NULL, 0, NULL, 0, NULL);