[Snek] [PATCH] snekserver: allow network connections to a snigle Snek instance
Mikhail Gusarov
dottedmag at dottedmag.net
Sun Apr 12 07:21:09 PDT 2020
This functionality ought to be implemented in inetd, systemd or socat, but none
of them provide "replace old client with new one" option.
---
ports/ev3/.gitignore | 1 +
ports/ev3/Makefile | 8 +-
ports/ev3/snek-main.c | 7 +
ports/ev3/snekserver.c | 510 +++++++++++++++++++++++++++++++++++++++++
ports/ev3/utils.c | 4 +-
ports/ev3/utils.h | 12 +
6 files changed, 538 insertions(+), 4 deletions(-)
create mode 100644 ports/ev3/snekserver.c
diff --git a/ports/ev3/.gitignore b/ports/ev3/.gitignore
index ea3551e..65fbcc8 100644
--- a/ports/ev3/.gitignore
+++ b/ports/ev3/.gitignore
@@ -1 +1,2 @@
snek-ev3-*
+snekserver-*
diff --git a/ports/ev3/Makefile b/ports/ev3/Makefile
index 8b08faa..74c27f9 100644
--- a/ports/ev3/Makefile
+++ b/ports/ev3/Makefile
@@ -19,6 +19,7 @@ SNEK_ROOT=../..
CC=arm-linux-gnueabi-gcc
PROGNAME=snek-ev3
PROG=$(PROGNAME)-$(SNEK_VERSION)
+SERVER=snekserver-$(SNEK_VERSION)
SNEK_LOCAL_SRC = \
snek-main.c \
@@ -46,14 +47,17 @@ CFLAGS+=-DSNEK_MEM_INCLUDE_NAME $(OPT) -g -I. $(SNEK_CFLAGS) -Werror $(CPPFLAGS)
LIBS=-lm
-all: $(PROG)
+all: $(PROG) $(SERVER)
$(PROG): $(SNEK_OBJ)
$(call quiet,CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SNEK_OBJ) $(LIBS)
+$(SERVER): snekserver.o utils.o
+ $(call quiet,CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
+
install: $(PROG)
install -d $(DESTDIR)$(SHAREDIR)
install $(PROG) $(DESTDIR)$(SHAREDIR)
clean::
- rm -f $(PROG)
+ rm -f $(PROG) $(SERVER) snekserver.o
diff --git a/ports/ev3/snek-main.c b/ports/ev3/snek-main.c
index ffa48b2..16fee1a 100644
--- a/ports/ev3/snek-main.c
+++ b/ports/ev3/snek-main.c
@@ -45,6 +45,7 @@ snek_getc_interactive(void)
if (snek_parse_middle)
prompt = "+ ";
fputs(prompt, stdout);
+ fflush(stdout);
line = fgets(line_base, 4096, stdin);
if (!line)
return EOF;
@@ -111,6 +112,12 @@ main(int argc, char **argv)
exit(1);
}
} else {
+ /* If Snek is started from snekserver, then its stdout is not
+ * connected to a tty, but nevertheless the server expects the
+ * line buffering
+ */
+ setlinebuf(stdout);
+
snek_file = "<stdin>";
snek_posix_input = stdin;
snek_interactive = true;
diff --git a/ports/ev3/snekserver.c b/ports/ev3/snekserver.c
new file mode 100644
index 0000000..6b2dff9
--- /dev/null
+++ b/ports/ev3/snekserver.c
@@ -0,0 +1,510 @@
+/*
+ * Copyright 2020 Mikhail Gusarov <dottedmag at dottedmag.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ */
+
+#define _GNU_SOURCE
+
+#include "utils.h"
+#include <errno.h>
+#include <getopt.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/signalfd.h>
+#include <sys/socket.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define DEFAULT_PORT 9553
+#define DEFAULT_SNEK_PATH ("snek-ev3-" SNEK_VERSION)
+
+static int
+poll_noeintr(struct pollfd fds[], nfds_t nfds, int timeout)
+{
+ for (;;) {
+ int ret = poll(fds, nfds, timeout);
+ if (ret == -1 && (errno == EINTR || errno == EAGAIN))
+ continue;
+ return ret;
+ }
+}
+
+static int
+waitpid_noeintr(pid_t pid, int *stat_loc, int options)
+{
+ for (;;) {
+ int ret = waitpid(pid, stat_loc, options);
+ if (ret == -1 && errno == EINTR)
+ continue;
+ return ret;
+ }
+}
+
+static int
+accept4_noeintr(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags)
+{
+ for (;;) {
+ int ret = accept4(sockfd, addr, addrlen, flags);
+ if (ret == -1 && errno == EINTR)
+ continue;
+ return ret;
+ }
+}
+
+static int
+close_noeintr(int fd)
+{
+ for (;;) {
+ int ret = close(fd);
+ if (ret == -1 && errno == EINTR)
+ continue;
+ return ret;
+ }
+}
+
+static int
+dup2_noeintr(int fd, int fd2)
+{
+ for (;;) {
+ int ret = dup2(fd, fd2);
+ if (ret == -1 && errno == EINTR)
+ continue;
+ return ret;
+ }
+}
+
+static int
+setup_listen(int port)
+{
+ int sock = socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock == -1) {
+ perror("socket");
+ exit(1);
+ }
+
+ int val = 1;
+ if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) == -1) {
+ perror("setsockopt(SO_REUSEADDR)");
+ exit(1);
+ }
+
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(port),
+ .sin_addr = {INADDR_ANY},
+ };
+
+ if (bind(sock, (const struct sockaddr *) &addr, sizeof(addr)) == -1) {
+ perror("bind");
+ exit(1);
+ }
+
+ if (listen(sock, 5) == -1) {
+ perror("listen");
+ exit(1);
+ }
+
+ return sock;
+}
+
+static void
+validate(struct pollfd *fd, int allowed_events, const char *name)
+{
+ if (fd->revents & ~allowed_events) {
+ fprintf(stderr, "Unexpected event on %s: 0x%x\n", name, fd->revents);
+ exit(1);
+ }
+}
+
+#define FD_LISTEN 0
+#define FD_CONNECTED 1
+#define FD_STDIN 2
+#define FD_STDOUT 3
+#define FD_STDERR 4
+
+#ifdef DEBUG
+static const char *
+strevs(char *buf, int i)
+{
+ char *p = buf;
+ if (i == 0) {
+ *p++ = '-';
+ }
+ if (i & POLLIN) {
+ *p++ = 'I';
+ }
+ if (i & POLLOUT) {
+ *p++ = 'O';
+ }
+ if (i & POLLERR) {
+ *p++ = 'E';
+ }
+ if (i & POLLHUP) {
+ *p++ = 'H';
+ }
+ *p++ = 0;
+ return buf;
+}
+
+static void
+print_pre_poll_state(int stdin_buf, int stdout_buf, struct pollfd *fds)
+{
+ if (stdin_buf == EOF)
+ fprintf(stderr, "[]");
+ else
+ fprintf(stderr, "[0x%02x]", stdin_buf);
+ fprintf(stderr, " -> (snek) -> ");
+ if (stdout_buf == EOF)
+ fprintf(stderr, "[]");
+ else
+ fprintf(stderr, "[0x%02x]", stdout_buf);
+ fprintf(stderr, "\n");
+
+ char r[10];
+ fprintf(stderr, "FD_LISTEN fd=%2d events=%4s\n", fds[0].fd, strevs(e, fds[0].events));
+ fprintf(stderr, "FD_CONN fd=%2d events=%4s\n", fds[1].fd, strevs(e, fds[1].events));
+ fprintf(stderr, "FD_STDIN fd=%2d events=%4s\n", fds[2].fd, strevs(e, fds[2].events));
+ fprintf(stderr, "FD_STDOUT fd=%2d events=%4s\n", fds[3].fd, strevs(e, fds[3].events));
+ fprintf(stderr, "FD_STDERR fd=%2d events=%4s\n", fds[4].fd, strevs(e, fds[4].events));
+}
+
+static void
+print_post_poll_state()
+{
+ char e[10], r[10];
+ fprintf(stderr, "FD_LISTEN fd=%2d events=%4s revents=%4s\n", fds[0].fd, strevs(e, fds[0].events),
+ strevs(r, fds[0].revents));
+ fprintf(stderr, "FD_CONN fd=%2d events=%4s revents=%4s\n", fds[1].fd, strevs(e, fds[1].events),
+ strevs(r, fds[1].revents));
+ fprintf(stderr, "FD_STDIN fd=%2d events=%4s revents=%4s\n", fds[2].fd, strevs(e, fds[2].events),
+ strevs(r, fds[2].revents));
+ fprintf(stderr, "FD_STDOUT fd=%2d events=%4s revents=%4s\n", fds[3].fd, strevs(e, fds[3].events),
+ strevs(r, fds[3].revents));
+ fprintf(stderr, "FD_STDERR fd=%2d events=%4s revents=%4s\n", fds[4].fd, strevs(e, fds[4].events),
+ strevs(r, fds[4].revents));
+}
+#endif
+
+static pid_t
+run_snek(const char *cmd, int *subproc_stdin, int *subproc_stdout, int *subproc_stderr)
+{
+ int stdin_fd[2], stdout_fd[2], stderr_fd[2];
+ if (pipe(stdin_fd) == -1) {
+ perror("pipe");
+ exit(1);
+ }
+ if (pipe(stdout_fd) == -1) {
+ perror("pipe");
+ exit(1);
+ }
+ if (pipe(stderr_fd) == -1) {
+ perror("pipe");
+ exit(1);
+ }
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ perror("fork");
+ exit(1);
+ }
+
+ if (pid == 0) {
+ close_noeintr(0);
+ dup2_noeintr(stdin_fd[0], 0);
+
+ close_noeintr(stdin_fd[0]);
+ close_noeintr(stdin_fd[1]);
+
+ close_noeintr(1);
+ dup2_noeintr(stdout_fd[1], 1);
+
+ close_noeintr(stdout_fd[0]);
+ close_noeintr(stdout_fd[1]);
+
+ close_noeintr(2);
+ dup2_noeintr(stderr_fd[1], 2);
+
+ close_noeintr(stderr_fd[0]);
+ close_noeintr(stderr_fd[1]);
+
+ if (execl(cmd, cmd, NULL) == -1) {
+ perror("execl");
+ exit(1);
+ }
+ }
+
+ *subproc_stdin = stdin_fd[1];
+ close_noeintr(stdin_fd[0]);
+ *subproc_stdout = stdout_fd[0];
+ close_noeintr(stdout_fd[1]);
+ *subproc_stderr = stderr_fd[0];
+ close_noeintr(stderr_fd[1]);
+
+ return pid;
+}
+
+static void
+usage(char *program, int val)
+{
+ fprintf(stderr, "usage: %s [--version] [--help] [--port <port>] [--snek-bin <path>]\n", program);
+ exit(val);
+}
+
+static const struct option options[] = {
+ {.name = "version", .has_arg = 0, .val = 'v'}, {.name = "snek-bin", .has_arg = 1, .val = 'e'},
+ {.name = "port", .has_arg = 1, .val = 'p'}, {.name = "help", .has_arg = 0, .val = '?'},
+ {.name = NULL, .has_arg = 0, .val = 0},
+};
+
+static void
+args(int argc, char **argv, int *port, const char **snek_path)
+{
+ int c;
+ while ((c = getopt_long(argc, argv, "p:e:", options, NULL)) != -1) {
+ switch (c) {
+ case 'v':
+ printf("%s version %s\n", argv[0], SNEK_VERSION);
+ exit(0);
+ case 'p':
+ *port = atoi(optarg);
+ if (*port <= 0 || *port > 65535) {
+ usage(argv[0], 1);
+ }
+ break;
+ case 'e':
+ *snek_path = optarg;
+ break;
+ case '?':
+ usage(argv[0], 0);
+ break;
+ default:
+ usage(argv[0], 1);
+ }
+ }
+}
+
+int
+main(int argc, char **argv)
+{
+ int port = DEFAULT_PORT;
+ const char *snek_path = DEFAULT_SNEK_PATH;
+
+ args(argc, argv, &port, &snek_path);
+
+ int listen_socket = setup_listen(port);
+ int connected_socket = -1;
+
+ int subproc_stdin, subproc_stdout, subproc_stderr;
+ int subproc_pid = run_snek(snek_path, &subproc_stdin, &subproc_stdout, &subproc_stderr);
+
+ int stdin_buf = EOF, stdout_buf = EOF;
+
+ for (;;) {
+ struct pollfd fds[] = {
+ [FD_LISTEN] =
+ {
+ .fd = listen_socket,
+ .events = POLLIN,
+ },
+ [FD_CONNECTED] =
+ {
+ .fd = connected_socket,
+ .events = POLLIN | POLLOUT,
+ },
+ [FD_STDIN] =
+ {
+ .fd = subproc_stdin,
+ .events = 0,
+ },
+ [FD_STDOUT] =
+ {
+ .fd = subproc_stdout,
+ .events = POLLIN,
+ },
+ [FD_STDERR] =
+ {
+ .fd = subproc_stderr,
+ .events = POLLIN,
+ },
+ };
+
+ // Do not read from socket if previous byte is not yet read by Snek
+ if (stdin_buf != EOF) {
+ fds[FD_CONNECTED].events &= ~POLLIN;
+ }
+
+ // Do not expect to write to socket if there is no output from Snek
+ if (stdout_buf == EOF) {
+ fds[FD_CONNECTED].events &= ~POLLOUT;
+ }
+
+ // Try to sink buffered data to Snek
+ if (stdin_buf != EOF) {
+ fds[FD_STDIN].events |= POLLOUT;
+ }
+
+ // Do not read from Snek if a client is connected and hasn't
+ // read the buffered data yet
+ if (stdout_buf != EOF && connected_socket != -1) {
+ fds[FD_STDOUT].events &= ~POLLIN;
+ fds[FD_STDERR].events &= ~POLLIN;
+ }
+
+#ifdef DEBUG
+ print_pre_poll_state(stdin_buf, stdout_buf, fds);
+#endif
+ int ret = poll_noeintr(fds, sizeof(fds) / sizeof(fds[0]), -1);
+ if (ret == -1) {
+ perror("poll");
+ exit(1);
+ }
+
+#ifdef DEBUG
+ print_post_poll_state(fds);
+#endif
+
+ // errors
+
+ validate(&fds[FD_LISTEN], POLLIN, "listening socket");
+ validate(&fds[FD_CONNECTED], POLLIN | POLLOUT | POLLHUP, "connected socket");
+ validate(&fds[FD_STDIN], POLLOUT | POLLHUP | POLLERR, "stdin pipe");
+ validate(&fds[FD_STDOUT], POLLIN | POLLHUP | POLLERR, "stdout pipe");
+ validate(&fds[FD_STDERR], POLLIN | POLLHUP | POLLERR, "stderr pipe");
+
+ // Snek exited
+
+ if ((fds[FD_STDIN].revents & (POLLHUP | POLLERR)) || (fds[FD_STDOUT].revents & (POLLHUP | POLLERR)) ||
+ (fds[FD_STDERR].revents & (POLLHUP | POLLERR))) {
+ close_noeintr(subproc_stdin);
+ close_noeintr(subproc_stdout);
+ close_noeintr(subproc_stderr);
+
+ int status;
+ pid_t pid = waitpid_noeintr(subproc_pid, &status, 0);
+ if (pid == -1) {
+ perror("waitpid");
+ exit(1);
+ }
+
+ if (pid != subproc_pid) {
+ fprintf(stderr, "unexpected pid: %d instead of %d\n", pid, subproc_pid);
+ exit(1);
+ }
+
+ if (WIFEXITED(status)) {
+ fprintf(stderr, "snek exited with code %d\n", WEXITSTATUS(status));
+ }
+
+ if (WIFSIGNALED(status)) {
+ fprintf(stderr, "snek terminated due to signal %d%s\n", WTERMSIG(status),
+ WCOREDUMP(status) ? " (core dumped)" : "");
+ }
+ exit(1);
+ }
+
+ // connected socket is closed
+
+ if (fds[FD_CONNECTED].revents & POLLHUP) {
+ close_noeintr(connected_socket);
+ connected_socket = -1;
+ stdout_buf = stdin_buf = EOF;
+ continue;
+ }
+
+ // new connection, replace existing
+
+ if (fds[FD_LISTEN].revents & POLLIN) {
+ struct sockaddr_in addr;
+ socklen_t addr_len = sizeof(addr);
+
+ int sock = accept4_noeintr(listen_socket, (struct sockaddr *) &addr, &addr_len, SOCK_CLOEXEC);
+ if (sock == -1) {
+ fprintf(stderr, "Warning: unable to accept connection, dropped: %s\n", strerror(errno));
+ continue;
+ }
+
+ close_noeintr(connected_socket);
+ connected_socket = sock;
+ stdout_buf = stdin_buf = EOF;
+ continue;
+ }
+
+ // "write" fds to dispose of buffered data
+
+ if (fds[FD_STDIN].revents & POLLOUT) {
+ char c = stdin_buf;
+ int written = write_noeintr(subproc_stdin, &c, 1);
+ if (written == 1) {
+ stdin_buf = EOF;
+ }
+ // write errors will be handled on the next poll() iteration
+ continue;
+ }
+
+ if (fds[FD_CONNECTED].revents & POLLOUT) {
+ char c = stdout_buf;
+ if (write_noeintr(connected_socket, &c, 1) == 1) {
+ stdout_buf = EOF;
+ }
+ // write errors will be handled on the next poll() iteration
+ continue;
+ }
+
+ // "read" fds to fill buffers
+
+ if (fds[FD_STDERR].revents & POLLIN) {
+ char c;
+ if (read_noeintr(subproc_stderr, &c, 1) == 1) {
+ if (connected_socket != -1) {
+ stdout_buf = c;
+ }
+ // else discard, a-la serial port
+ }
+ // read errors will be handled on the next poll() iteration
+ continue;
+ }
+
+ if (fds[FD_STDOUT].revents & POLLIN) {
+ char c;
+ if (read_noeintr(subproc_stdout, &c, 1) == 1) {
+ if (connected_socket != -1) {
+ stdout_buf = c;
+ }
+ // else discard, a-la serial port
+ }
+ // read errors will be handled on the next poll() iteration
+ continue;
+ }
+
+ if (fds[FD_CONNECTED].revents & POLLIN) {
+ char c;
+ int read_ = read_noeintr(connected_socket, &c, 1);
+ if (read_ == 0) {
+ close_noeintr(connected_socket);
+ connected_socket = -1;
+ stdout_buf = stdin_buf = EOF;
+ } else if (read_ == 1) {
+ stdin_buf = c;
+ }
+ // read errors will be handled on the next poll() iteration
+ continue;
+ }
+
+ // unreachable
+ exit(255);
+ }
+}
diff --git a/ports/ev3/utils.c b/ports/ev3/utils.c
index b4d7626..fbdf737 100644
--- a/ports/ev3/utils.c
+++ b/ports/ev3/utils.c
@@ -34,7 +34,7 @@ close_noerrno(int fd)
errno = saved_errno;
}
-static ssize_t
+ssize_t
read_noeintr(int fd, char *buf, size_t bufsize)
{
for (;;) {
@@ -45,7 +45,7 @@ read_noeintr(int fd, char *buf, size_t bufsize)
}
}
-static ssize_t
+ssize_t
write_noeintr(int fd, const char *buf, size_t bufsize)
{
for (;;) {
diff --git a/ports/ev3/utils.h b/ports/ev3/utils.h
index 77c465b..77db1ea 100644
--- a/ports/ev3/utils.h
+++ b/ports/ev3/utils.h
@@ -24,6 +24,18 @@
const char *
cutprefix(const char *s, const char *prefix, int prefixlen);
+/*
+ * read(2) that retries in case of EINTR
+ */
+ssize_t
+read_noeintr(int fd, char *buf, size_t bufsize);
+
+/*
+ * write(2) that retries in case of EINTR
+ */
+ssize_t
+write_noeintr(int fd, const char *buf, size_t bufsize);
+
/*
* Reads up to bufsize bytes of data from sysfs file filename in directory dirfd
* and places it into bufffer buf.
--
2.24.0
More information about the Snek
mailing list