diff options
Diffstat (limited to 'unix/serial.c')
-rw-r--r-- | unix/serial.c | 596 |
1 files changed, 596 insertions, 0 deletions
diff --git a/unix/serial.c b/unix/serial.c new file mode 100644 index 00000000..1e20dd20 --- /dev/null +++ b/unix/serial.c @@ -0,0 +1,596 @@ +/* + * Serial back end (Unix-specific). + */ + +#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 Serial; +struct Serial { + Seat *seat; + LogContext *logctx; + int fd; + bool finished; + size_t inbufsize; + bufchain output_data; + Backend backend; +}; + +/* + * 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 void serial_select_result(int fd, int event); +static void serial_uxsel_setup(Serial *serial); +static void serial_try_write(Serial *serial); + +static char *serial_configure(Serial *serial, Conf *conf) +{ + struct termios options; + int bflag, bval, speed, flow, parity; + const char *str; + + if (serial->fd < 0) + return dupstr("Unable to reconfigure already-closed " + "serial connection"); + + tcgetattr(serial->fd, &options); + + /* + * Find the appropriate baud rate flag. + */ + speed = conf_get_int(conf, CONF_serspeed); +#define SETBAUD(x) (bflag = B ## x, bval = x) +#define CHECKBAUD(x) do { if (speed >= 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 B153600 + CHECKBAUD(153600); +#endif +#ifdef B230400 + CHECKBAUD(230400); +#endif +#ifdef B307200 + CHECKBAUD(307200); +#endif +#ifdef B460800 + CHECKBAUD(460800); +#endif +#ifdef B500000 + CHECKBAUD(500000); +#endif +#ifdef B576000 + CHECKBAUD(576000); +#endif +#ifdef B921600 + CHECKBAUD(921600); +#endif +#ifdef B1000000 + CHECKBAUD(1000000); +#endif +#ifdef B1152000 + CHECKBAUD(1152000); +#endif +#ifdef B1500000 + CHECKBAUD(1500000); +#endif +#ifdef B2000000 + CHECKBAUD(2000000); +#endif +#ifdef B2500000 + CHECKBAUD(2500000); +#endif +#ifdef B3000000 + CHECKBAUD(3000000); +#endif +#ifdef B3500000 + CHECKBAUD(3500000); +#endif +#ifdef B4000000 + CHECKBAUD(4000000); +#endif +#undef CHECKBAUD +#undef SETBAUD + cfsetispeed(&options, bflag); + cfsetospeed(&options, bflag); + logeventf(serial->logctx, "Configuring baud rate %d", bval); + + options.c_cflag &= ~CSIZE; + switch (conf_get_int(conf, CONF_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 dupstr("Invalid number of data bits " + "(need 5, 6, 7 or 8)"); + } + logeventf(serial->logctx, "Configuring %d data bits", + conf_get_int(conf, CONF_serdatabits)); + + if (conf_get_int(conf, CONF_serstopbits) >= 4) { + options.c_cflag |= CSTOPB; + } else { + options.c_cflag &= ~CSTOPB; + } + logeventf(serial->logctx, "Configuring %s", + (options.c_cflag & CSTOPB ? "2 stop bits" : "1 stop bit")); + + options.c_iflag &= ~(IXON|IXOFF); +#ifdef CRTSCTS + options.c_cflag &= ~CRTSCTS; +#endif +#ifdef CNEW_RTSCTS + options.c_cflag &= ~CNEW_RTSCTS; +#endif + flow = conf_get_int(conf, CONF_serflow); + if (flow == SER_FLOW_XONXOFF) { + options.c_iflag |= IXON | IXOFF; + str = "XON/XOFF"; + } else if (flow == 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"; + logeventf(serial->logctx, "Configuring %s flow control", str); + + /* Parity */ + parity = conf_get_int(conf, CONF_serparity); + if (parity == SER_PAR_ODD) { + options.c_cflag |= PARENB; + options.c_cflag |= PARODD; + str = "odd"; + } else if (parity == SER_PAR_EVEN) { + options.c_cflag |= PARENB; + options.c_cflag &= ~PARODD; + str = "even"; + } else { + options.c_cflag &= ~PARENB; + str = "no"; + } + logeventf(serial->logctx, "Configuring %s parity", str); + + options.c_cflag |= CLOCAL | CREAD; + options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); + options.c_iflag &= ~(ISTRIP | IGNCR | INLCR | ICRNL +#ifdef IUCLC + | IUCLC +#endif + ); + options.c_oflag &= ~(OPOST +#ifdef ONLCR + | ONLCR +#endif +#ifdef OCRNL + | OCRNL +#endif +#ifdef ONOCR + | ONOCR +#endif +#ifdef ONLRET + | ONLRET +#endif + ); + options.c_cc[VMIN] = 1; + options.c_cc[VTIME] = 0; + + if (tcsetattr(serial->fd, TCSANOW, &options) < 0) + return dupprintf("Configuring serial port: %s", strerror(errno)); + + 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 char *serial_init(const BackendVtable *vt, Seat *seat, + Backend **backend_handle, LogContext *logctx, + Conf *conf, const char *host, int port, + char **realhost, bool nodelay, bool keepalive) +{ + Serial *serial; + char *err; + char *line; + + /* No local authentication phase in this protocol */ + seat_set_trust_status(seat, false); + + serial = snew(Serial); + memset(serial, 0, sizeof(Serial)); + serial->backend.vt = vt; + *backend_handle = &serial->backend; + + serial->seat = seat; + serial->logctx = logctx; + serial->finished = false; + serial->inbufsize = 0; + bufchain_init(&serial->output_data); + + line = conf_get_str(conf, CONF_serline); + logeventf(serial->logctx, "Opening serial device %s", line); + + serial->fd = open(line, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK); + if (serial->fd < 0) + return dupprintf("Opening serial port '%s': %s", + line, strerror(errno)); + + cloexec(serial->fd); + + err = serial_configure(serial, conf); + if (err) + return err; + + *realhost = dupstr(line); + + if (!serial_by_fd) + serial_by_fd = newtree234(serial_compare_by_fd); + add234(serial_by_fd, serial); + + serial_uxsel_setup(serial); + + /* + * Specials are always available. + */ + seat_update_specials_menu(serial->seat); + + return NULL; +} + +static void serial_close(Serial *serial) +{ + if (serial->fd >= 0) { + uxsel_del(serial->fd); + close(serial->fd); + serial->fd = -1; + } +} + +static void serial_free(Backend *be) +{ + Serial *serial = container_of(be, Serial, backend); + + serial_close(serial); + + bufchain_clear(&serial->output_data); + + sfree(serial); +} + +static void serial_reconfig(Backend *be, Conf *conf) +{ + Serial *serial = container_of(be, Serial, backend); + + char *err = serial_configure(serial, conf); + if (err) { + /* + * FIXME: apart from freeing the dynamically allocated + * message, what should we do if this returns an error? + */ + sfree(err); + } +} + +static void serial_select_result(int fd, int event) +{ + Serial *serial; + char buf[4096]; + int ret; + bool finished = false; + + serial = find234(serial_by_fd, &fd, serial_find_by_fd); + + if (!serial) + return; /* 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) { +#ifdef EAGAIN + if (errno == EAGAIN) + return; /* spurious */ +#endif +#ifdef EWOULDBLOCK + if (errno == EWOULDBLOCK) + return; /* spurious */ +#endif + perror("read serial port"); + exit(1); + } else if (ret > 0) { + serial->inbufsize = seat_stdout(serial->seat, 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; + + seat_notify_remote_exit(serial->seat); + } +} + +static void serial_uxsel_setup(Serial *serial) +{ + int rwx = 0; + + if (serial->inbufsize <= SERIAL_MAX_BACKLOG) + rwx |= SELECT_R; + if (bufchain_size(&serial->output_data)) + rwx |= SELECT_W; /* might also want to write to it */ + uxsel_set(serial->fd, rwx, serial_select_result); +} + +static void serial_try_write(Serial *serial) +{ + ssize_t ret; + + assert(serial->fd >= 0); + + while (bufchain_size(&serial->output_data) > 0) { + ptrlen data = bufchain_prefix(&serial->output_data); + ret = write(serial->fd, data.ptr, 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); + } + + seat_sent(serial->seat, bufchain_size(&serial->output_data)); + serial_uxsel_setup(serial); +} + +/* + * Called to send data down the serial connection. + */ +static void serial_send(Backend *be, const char *buf, size_t len) +{ + Serial *serial = container_of(be, Serial, backend); + + if (serial->fd < 0) + return; + + bufchain_add(&serial->output_data, buf, len); + serial_try_write(serial); +} + +/* + * Called to query the current sendability status. + */ +static size_t serial_sendbuffer(Backend *be) +{ + Serial *serial = container_of(be, Serial, backend); + return bufchain_size(&serial->output_data); +} + +/* + * Called to set the size of the window + */ +static void serial_size(Backend *be, int width, int height) +{ + /* Do nothing! */ + return; +} + +/* + * Send serial special codes. + */ +static void serial_special(Backend *be, SessionSpecialCode code, int arg) +{ + Serial *serial = container_of(be, Serial, backend); + + if (serial->fd >= 0 && code == SS_BRK) { + tcsendbreak(serial->fd, 0); + logevent(serial->logctx, "Sending serial break at user request"); + } + + return; +} + +/* + * Return a list of the special codes that make sense in this + * protocol. + */ +static const SessionSpecial *serial_get_specials(Backend *be) +{ + static const struct SessionSpecial specials[] = { + {"Break", SS_BRK}, + {NULL, SS_EXITMENU} + }; + return specials; +} + +static bool serial_connected(Backend *be) +{ + return true; /* always connected */ +} + +static bool serial_sendok(Backend *be) +{ + return true; +} + +static void serial_unthrottle(Backend *be, size_t backlog) +{ + Serial *serial = container_of(be, Serial, backend); + serial->inbufsize = backlog; + serial_uxsel_setup(serial); +} + +static bool serial_ldisc(Backend *be, int option) +{ + /* + * Local editing and local echo are off by default. + */ + return false; +} + +static void serial_provide_ldisc(Backend *be, Ldisc *ldisc) +{ + /* This is a stub. */ +} + +static int serial_exitcode(Backend *be) +{ + Serial *serial = container_of(be, Serial, backend); + 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(Backend *be) +{ + return 0; +} + +const BackendVtable serial_backend = { + .init = serial_init, + .free = serial_free, + .reconfig = serial_reconfig, + .send = serial_send, + .sendbuffer = serial_sendbuffer, + .size = serial_size, + .special = serial_special, + .get_specials = serial_get_specials, + .connected = serial_connected, + .exitcode = serial_exitcode, + .sendok = serial_sendok, + .ldisc_option_state = serial_ldisc, + .provide_ldisc = serial_provide_ldisc, + .unthrottle = serial_unthrottle, + .cfg_info = serial_cfg_info, + .id = "serial", + .displayname_tc = "Serial", + .displayname_lc = "serial", + .protocol = PROT_SERIAL, + .serial_parity_mask = ((1 << SER_PAR_NONE) | + (1 << SER_PAR_ODD) | + (1 << SER_PAR_EVEN)), + .serial_flow_mask = ((1 << SER_FLOW_NONE) | + (1 << SER_FLOW_XONXOFF) | + (1 << SER_FLOW_RTSCTS)), +}; |