Serial back end for Unix. Due to hardware limitations (no Linux box
[u/mdw/putty] / unix / uxser.c
diff --git a/unix/uxser.c b/unix/uxser.c
new file mode 100644 (file)
index 0000000..85e9618
--- /dev/null
@@ -0,0 +1,510 @@
+/*
+ * Serial back end (Unix-specific).
+ */
+
+/*
+ * TODO:
+ * 
+ *  - send break.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <limits.h>
+
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <termios.h>
+
+#include "putty.h"
+#include "tree234.h"
+
+#define SERIAL_MAX_BACKLOG 4096
+
+typedef struct serial_backend_data {
+    void *frontend;
+    int fd;
+    int finished;
+    int inbufsize;
+    bufchain output_data;
+} *Serial;
+
+/*
+ * We store our serial backends in a tree sorted by fd, so that
+ * when we get an uxsel notification we know which backend instance
+ * is the owner of the serial port that caused it.
+ */
+static int serial_compare_by_fd(void *av, void *bv)
+{
+    Serial a = (Serial)av;
+    Serial b = (Serial)bv;
+
+    if (a->fd < b->fd)
+       return -1;
+    else if (a->fd > b->fd)
+       return +1;
+    return 0;
+}
+
+static int serial_find_by_fd(void *av, void *bv)
+{
+    int a = *(int *)av;
+    Serial b = (Serial)bv;
+
+    if (a < b->fd)
+       return -1;
+    else if (a > b->fd)
+       return +1;
+    return 0;
+}
+
+static tree234 *serial_by_fd = NULL;
+
+static int serial_select_result(int fd, int event);
+static void serial_uxsel_setup(Serial serial);
+static void serial_try_write(Serial serial);
+
+static const char *serial_configure(Serial serial, Config *cfg)
+{
+    struct termios options;
+    int bflag, bval;
+    const char *str;
+    char *msg;
+
+    if (serial->fd < 0)
+       return "Unable to reconfigure already-closed serial connection";
+
+    tcgetattr(serial->fd, &options);
+
+    /*
+     * Find the appropriate baud rate flag.
+     */
+#define SETBAUD(x) (bflag = B ## x, bval = x)
+#define CHECKBAUD(x) do { if (cfg->serspeed >= x) SETBAUD(x); } while (0)
+    SETBAUD(50);
+#ifdef B75
+    CHECKBAUD(75);
+#endif
+#ifdef B110
+    CHECKBAUD(110);
+#endif
+#ifdef B134
+    CHECKBAUD(134);
+#endif
+#ifdef B150
+    CHECKBAUD(150);
+#endif
+#ifdef B200
+    CHECKBAUD(200);
+#endif
+#ifdef B300
+    CHECKBAUD(300);
+#endif
+#ifdef B600
+    CHECKBAUD(600);
+#endif
+#ifdef B1200
+    CHECKBAUD(1200);
+#endif
+#ifdef B1800
+    CHECKBAUD(1800);
+#endif
+#ifdef B2400
+    CHECKBAUD(2400);
+#endif
+#ifdef B4800
+    CHECKBAUD(4800);
+#endif
+#ifdef B9600
+    CHECKBAUD(9600);
+#endif
+#ifdef B19200
+    CHECKBAUD(19200);
+#endif
+#ifdef B38400
+    CHECKBAUD(38400);
+#endif
+#ifdef B57600
+    CHECKBAUD(57600);
+#endif
+#ifdef B76800
+    CHECKBAUD(76800);
+#endif
+#ifdef B115200
+    CHECKBAUD(115200);
+#endif
+#ifdef B230400
+    CHECKBAUD(230400);
+#endif
+#undef CHECKBAUD
+#undef SETBAUD
+    cfsetispeed(&options, bflag);
+    cfsetospeed(&options, bflag);
+    msg = dupprintf("Configuring baud rate %d", bval);
+    logevent(serial->frontend, msg);
+    sfree(msg);
+
+    options.c_cflag &= ~CSIZE;
+    switch (cfg->serdatabits) {
+      case 5: options.c_cflag |= CS5; break;
+      case 6: options.c_cflag |= CS6; break;
+      case 7: options.c_cflag |= CS7; break;
+      case 8: options.c_cflag |= CS8; break;
+      default: return "Invalid number of data bits (need 5, 6, 7 or 8)";
+    }
+    msg = dupprintf("Configuring %d data bits", cfg->serdatabits);
+    logevent(serial->frontend, msg);
+    sfree(msg);
+
+    if (cfg->serstopbits >= 4) {
+       options.c_cflag |= CSTOPB;
+    } else {
+       options.c_cflag &= ~CSTOPB;
+    }
+    msg = dupprintf("Configuring %d stop bits",
+                   (options.c_cflag & CSTOPB ? 2 : 1));
+    logevent(serial->frontend, msg);
+    sfree(msg);
+
+    options.c_cflag &= ~(IXON|IXOFF);
+    if (cfg->serflow == SER_FLOW_XONXOFF) {
+       options.c_cflag |= IXON | IXOFF;
+       str = "XON/XOFF";
+    } else if (cfg->serflow == SER_FLOW_RTSCTS) {
+#ifdef CRTSCTS
+       options.c_cflag |= CRTSCTS;
+#endif
+#ifdef CNEW_RTSCTS
+       options.c_cflag |= CNEW_RTSCTS;
+#endif
+       str = "RTS/CTS";
+    } else
+       str = "no";
+    msg = dupprintf("Configuring %s flow control", str);
+    logevent(serial->frontend, msg);
+    sfree(msg);
+
+    /* Parity */
+    if (cfg->serparity == SER_PAR_ODD) {
+       options.c_cflag |= PARENB;
+       options.c_cflag |= PARODD;
+       str = "odd";
+    } else if (cfg->serparity == SER_PAR_EVEN) {
+       options.c_cflag |= PARENB;
+       options.c_cflag &= ~PARODD;
+       str = "even";
+    } else {
+       options.c_cflag &= ~PARENB;
+       str = "no";
+    }
+    msg = dupprintf("Configuring %s parity", str);
+    logevent(serial->frontend, msg);
+    sfree(msg);
+
+    options.c_cflag |= CLOCAL | CREAD;
+    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
+    options.c_oflag &= ~OPOST;
+    options.c_cc[VMIN] = 1;
+    options.c_cc[VTIME] = 0;
+
+    if (tcsetattr(serial->fd, TCSANOW, &options) < 0)
+       return "Unable to configure serial port";
+
+    return NULL;
+}
+
+/*
+ * Called to set up the serial connection.
+ * 
+ * Returns an error message, or NULL on success.
+ *
+ * Also places the canonical host name into `realhost'. It must be
+ * freed by the caller.
+ */
+static const char *serial_init(void *frontend_handle, void **backend_handle,
+                              Config *cfg,
+                              char *host, int port, char **realhost, int nodelay,
+                              int keepalive)
+{
+    Serial serial;
+    const char *err;
+
+    serial = snew(struct serial_backend_data);
+    *backend_handle = serial;
+
+    serial->frontend = frontend_handle;
+    serial->finished = FALSE;
+    serial->inbufsize = 0;
+    bufchain_init(&serial->output_data);
+
+    {
+       char *msg = dupprintf("Opening serial device %s", cfg->serline);
+       logevent(serial->frontend, msg);
+    }
+
+    serial->fd = open(cfg->serline, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK);
+    if (serial->fd < 0)
+       return "Unable to open serial port";
+
+    err = serial_configure(serial, cfg);
+    if (err)
+       return err;
+
+    *realhost = dupstr(cfg->serline);
+
+    if (!serial_by_fd)
+       serial_by_fd = newtree234(serial_compare_by_fd);
+    add234(serial_by_fd, serial);
+
+    serial_uxsel_setup(serial);
+
+    return NULL;
+}
+
+static void serial_close(Serial serial)
+{
+    if (serial->fd >= 0) {
+       close(serial->fd);
+       serial->fd = -1;
+    }
+}
+
+static void serial_free(void *handle)
+{
+    Serial serial = (Serial) handle;
+
+    serial_close(serial);
+
+    bufchain_clear(&serial->output_data);
+
+    sfree(serial);
+}
+
+static void serial_reconfig(void *handle, Config *cfg)
+{
+    Serial serial = (Serial) handle;
+    const char *err;
+
+    err = serial_configure(serial, cfg);
+
+    /*
+     * FIXME: what should we do if err returns something?
+     */
+}
+
+static int serial_select_result(int fd, int event)
+{
+    Serial serial;
+    char buf[4096];
+    int ret;
+    int finished = FALSE;
+
+    serial = find234(serial_by_fd, &fd, serial_find_by_fd);
+
+    if (!serial)
+       return 1;                      /* spurious event; keep going */
+
+    if (event == 1) {
+       ret = read(serial->fd, buf, sizeof(buf));
+
+       if (ret == 0) {
+           /*
+            * Shouldn't happen on a real serial port, but I'm open
+            * to the idea that there might be two-way devices we
+            * can treat _like_ serial ports which can return EOF.
+            */
+           finished = TRUE;
+       } else if (ret < 0) {
+           perror("read serial port");
+           exit(1);
+       } else if (ret > 0) {
+           serial->inbufsize = from_backend(serial->frontend, 0, buf, ret);
+           serial_uxsel_setup(serial); /* might acquire backlog and freeze */
+       }
+    } else if (event == 2) {
+       /*
+        * Attempt to send data down the pty.
+        */
+       serial_try_write(serial);
+    }
+
+    if (finished) {
+       serial_close(serial);
+
+       serial->finished = TRUE;
+
+       notify_remote_exit(serial->frontend);
+    }
+
+    return !finished;
+}
+
+static void serial_uxsel_setup(Serial serial)
+{
+    int rwx = 0;
+
+    if (serial->inbufsize <= SERIAL_MAX_BACKLOG)
+       rwx |= 1;
+    if (bufchain_size(&serial->output_data))
+        rwx |= 2;                      /* might also want to write to it */
+    uxsel_set(serial->fd, rwx, serial_select_result);
+}
+
+static void serial_try_write(Serial serial)
+{
+    void *data;
+    int len, ret;
+
+    assert(serial->fd >= 0);
+
+    while (bufchain_size(&serial->output_data) > 0) {
+        bufchain_prefix(&serial->output_data, &data, &len);
+       ret = write(serial->fd, data, len);
+
+        if (ret < 0 && (errno == EWOULDBLOCK)) {
+            /*
+             * We've sent all we can for the moment.
+             */
+            break;
+        }
+       if (ret < 0) {
+           perror("write serial port");
+           exit(1);
+       }
+       bufchain_consume(&serial->output_data, ret);
+    }
+
+    serial_uxsel_setup(serial);
+}
+
+/*
+ * Called to send data down the serial connection.
+ */
+static int serial_send(void *handle, char *buf, int len)
+{
+    Serial serial = (Serial) handle;
+
+    if (serial->fd < 0)
+       return 0;
+
+    bufchain_add(&serial->output_data, buf, len);
+    serial_try_write(serial);
+
+    return bufchain_size(&serial->output_data);
+}
+
+/*
+ * Called to query the current sendability status.
+ */
+static int serial_sendbuffer(void *handle)
+{
+    Serial serial = (Serial) handle;
+    return bufchain_size(&serial->output_data);
+}
+
+/*
+ * Called to set the size of the window
+ */
+static void serial_size(void *handle, int width, int height)
+{
+    /* Do nothing! */
+    return;
+}
+
+/*
+ * Send serial special codes.
+ */
+static void serial_special(void *handle, Telnet_Special code)
+{
+    /*
+     * FIXME: serial break? XON? XOFF?
+     */
+    return;
+}
+
+/*
+ * Return a list of the special codes that make sense in this
+ * protocol.
+ */
+static const struct telnet_special *serial_get_specials(void *handle)
+{
+    /*
+     * FIXME: serial break? XON? XOFF?
+     */
+    return NULL;
+}
+
+static int serial_connected(void *handle)
+{
+    return 1;                         /* always connected */
+}
+
+static int serial_sendok(void *handle)
+{
+    return 1;
+}
+
+static void serial_unthrottle(void *handle, int backlog)
+{
+    Serial serial = (Serial) handle;
+    serial->inbufsize = backlog;
+    serial_uxsel_setup(serial);
+}
+
+static int serial_ldisc(void *handle, int option)
+{
+    /*
+     * Local editing and local echo are off by default.
+     */
+    return 0;
+}
+
+static void serial_provide_ldisc(void *handle, void *ldisc)
+{
+    /* This is a stub. */
+}
+
+static void serial_provide_logctx(void *handle, void *logctx)
+{
+    /* This is a stub. */
+}
+
+static int serial_exitcode(void *handle)
+{
+    Serial serial = (Serial) handle;
+    if (serial->fd >= 0)
+        return -1;                     /* still connected */
+    else
+        /* Exit codes are a meaningless concept with serial ports */
+        return INT_MAX;
+}
+
+/*
+ * cfg_info for Serial does nothing at all.
+ */
+static int serial_cfg_info(void *handle)
+{
+    return 0;
+}
+
+Backend serial_backend = {
+    serial_init,
+    serial_free,
+    serial_reconfig,
+    serial_send,
+    serial_sendbuffer,
+    serial_size,
+    serial_special,
+    serial_get_specials,
+    serial_connected,
+    serial_exitcode,
+    serial_sendok,
+    serial_ldisc,
+    serial_provide_ldisc,
+    serial_provide_logctx,
+    serial_unthrottle,
+    serial_cfg_info,
+    1
+};