From 51bb17e9d13796c83b3a6e96f71f38263f6b7b4f Mon Sep 17 00:00:00 2001 From: gretchen Date: Sun, 17 Nov 2019 19:40:36 -0800 Subject: Get a lua or sc shell on a norns. --- .gitignore | 2 + Makefile | 5 ++ README.org | 14 +++++ norns_shell.c | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.org create mode 100644 norns_shell.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0316d2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +norns_shell +*.o \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f9ad218 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +CFLAGS=-Werror -Wall -ggdb -O3 $(shell pkg-config --cflags nanomsg) +LDFLAGS=$(shell pkg-config --libs nanomsg) + +all: norns_shell + diff --git a/README.org b/README.org new file mode 100644 index 0000000..19f8786 --- /dev/null +++ b/README.org @@ -0,0 +1,14 @@ +* norns shell + +Get a lua shell or supercollider shell on a norns from the command-line, similar to the shell in the maiden interface. + +Requires nanomsg. + +#+BEGIN_SRC sh +# get a lua shell +./norns_shell +./norns_shell ws://norns.local:5555 +# get a supercollider shell +./norns_shell ws://norns.local:5556 +#+END_SRC + diff --git a/norns_shell.c b/norns_shell.c new file mode 100644 index 0000000..fbd556c --- /dev/null +++ b/norns_shell.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MAX_LINE 1024 + +int killed = 0; +void handle_signal(int sig_num) +{ + killed = 1; +} + + +int main (int argc, char **argv) +{ + char *endpoint = "ws://norns.local:5555"; + int exit = 0; + int s = 0; + + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + + /* Configure the websocket endpoint. */ + if (argc > 2 || + (argc == 2 && + (!strcmp(argv[1], "--help") + || !strcmp(argv[1], "-h")))) { + fprintf(stderr, + "Get a lua or supercollider shell on a norns.\n" + "Usage: %s [ws://norns.local:5555]\n", + argv[0]); + goto error; + } + if (argc == 2) endpoint = argv[1]; + + /* connect to the norns; use text, like it expects. */ + if ((s = nn_socket(AF_SP, NN_BUS)) < 0) { + fprintf(stderr, "Can't create socket: %m\n"); + goto error; + } + if (nn_connect(s, endpoint) < 0) { + fprintf(stderr, "Can't connect to %s: %m\n", endpoint); + goto error; + } + if (nn_setsockopt(s, + NN_WS, + NN_WS_MSG_TYPE, + &(int) {NN_WS_MSG_TYPE_TEXT}, + sizeof(int)) < 0) { + fprintf(stderr, "Couldn't switch to text mode: %m\n"); + goto error; + } + + /* since nanomsg has its own poll framework, we'll read to this + buffer in a nonblocking way until we get a newline. */ + size_t line_n = 0; + char line[MAX_LINE + 1] = {0}; + + int fl = 0; + if ((fl = fcntl(STDIN_FILENO, F_GETFL)) < 0) { + fprintf(stderr, "Couldn't read stdin fd flags: %m\n"); + goto error; + } + if (fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK)) { + fprintf(stderr, "Couldn't set stdin non-blocking: %m\n"); + goto error; + } + + /* NN_BUS doesn't guarantee delivery, unfortunately. + in practice on a LAN this means that the first couple of + messages are dropped while the connection is being set up. + this seems strange to me (it seems like you'd have to go + out of your way to drop messages in a websocket handler?) + and I'm not sure this is the right fit for norns, but that's + how it is. + + we partially solve this issue by sending an empty comment + -- as a sentinel -- repeatedly until we get the corresponding + . + */ + /* TODO: time out after three seconds or something. */ + int received = 0; + struct timespec sent_sentinel = {0}; + + struct nn_pollfd poll_s = { .fd = s, .events = NN_POLLIN | NN_POLLOUT }; + while (!killed) { + switch (nn_poll(&poll_s, 1, 1000)) { + case -1: + fprintf(stderr, "error polling: %m\n"); + goto error; + case 0: + fprintf(stderr, "# timeout...\n"); + break; + case 1: + /* If we can read, echo out the message (unless it's the + response to the sentinel, which we handle differently). */ + if (poll_s.revents & NN_POLLIN) { + char *buf = NULL; + size_t n = 0; + if ((n = nn_recv(s, &buf, NN_MSG, 0)) < 0) { + fprintf(stderr, "Couldn't receive: %m\n"); + goto error; + } + /* swallow the first */ + if (!received && !strncmp(buf, "\n\n", n)) { + fprintf(stderr, "# it lives!\n"); + } else { + /* strip final newline, if there are two... */ + if (n >= 2 && buf[n - 1] == '\n' && buf[n - 2] == '\n') + n -= 1; + if (write(STDOUT_FILENO, buf, n) < n) { + fprintf(stderr, "Couldn't write: %m\n"); + goto error; + } + } + nn_freemsg(buf); + received = 1; + } + /* If we can write, see if there's a full line. */ + if (poll_s.revents & NN_POLLOUT) { + if (!received) { + /* only do this five times a second... */ + struct timespec now = {0}; + if (clock_gettime(CLOCK_MONOTONIC, &now)) { + fprintf(stderr, "Can't use the clock? %m\n"); + goto error; + } + /* first bit syncs with the clock... */ + if ((now.tv_sec - sent_sentinel.tv_sec <= 1) + && (now.tv_nsec - sent_sentinel.tv_nsec + <= (1000000000 / 5))) + break; + memcpy(&sent_sentinel, &now, sizeof(struct timespec)); + + /* send the sentinel comment */ + if (nn_send(s, "--\n", 3, 0) != 3) { + fprintf(stderr, "Couldn't send: %m\n"); + goto error; + } + } else { + int n = 0; + if ((n = read(STDIN_FILENO, + &line[line_n], + MAX_LINE - line_n)) < 0) { + if (errno != EAGAIN) { + fprintf(stderr, "Can't read: %m\n"); + goto error; + } + /* treat EAGAIN as "read 0" */ + n = 0; + } + line_n += n; + if (line_n >= 1 && line[line_n - 1] == '\n') { + if ((nn_send(s, line, line_n, 0)) != line_n) { + fprintf(stderr, "Couldn't send: %m\n"); + goto error; + } + line_n = 0; + } + if (line_n == MAX_LINE) { + fprintf(stderr, "Line too long :(.\n"); + goto error; + } + } + } + break; + } + } + goto finish; +error: + exit = 1; +finish: + if (s > 0) nn_close(s); + return exit; +} + -- cgit v1.2.1