1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-10 01:48:00 +00:00
putty-source/unix/serial.c
Simon Tatham 6246ff3f0a New Seat callback, seat_sent().
This is used to notify the Seat that some data has been cleared from
the backend's outgoing data buffer. In other words, it notifies the
Seat that it might be worth calling backend_sendbuffer() again.

We've never needed this before, because until now, Seats have always
been the 'main program' part of the application, meaning they were
also in control of the event loop. So they've been able to call
backend_sendbuffer() proactively, every time they go round the event
loop, instead of having to wait for a callback.

But now, the SSH proxy is the first example of a Seat without
privileged access to the event loop, so it has no way to find out that
the backend's sendbuffer has got smaller. And without that, it can't
pass that notification on to plug_sent, to unblock in turn whatever
the proxied connection might have been waiting to send.

In fact, before this commit, sshproxy.c never called plug_sent at all.
As a result, large data uploads over an SSH jump host would hang
forever as soon as the outgoing buffer filled up for the first time:
the main backend (to which sshproxy.c was acting as a Socket) would
carefully stop filling up the buffer, and then never receive the call
to plug_sent that would cause it to start again.

The new callback is ignored everywhere except in sshproxy.c. It might
be a good idea to remove backend_sendbuffer() entirely and convert all
previous uses of it into non-empty implementations of this callback,
so that we've only got one system; but for the moment, I haven't done
that.
2021-06-27 13:52:48 +01:00

597 lines
14 KiB
C

/*
* 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);
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 size_t serial_send(Backend *be, const char *buf, size_t len)
{
Serial *serial = container_of(be, Serial, backend);
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 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 = "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)),
};