Commit 0593d07c for guacamole.apache.org
commit 0593d07c406d1b1aea9c3658271024ae89355207
Author: Ciro Iriarte <ciro.iriarte@gmail.com>
Date: Wed Jul 15 17:01:00 2026 -0300
GUACAMOLE-2301: Add serial reconnect, line-ending, echo, control-line, and diagnostics support
Implements the reviewed serial console feature gaps on top of the initial
serial protocol plugin:
- Auto-reconnect on drop (new "auto-reconnect", default true): the terminal and
scrollback are kept alive across reconnects; a single stream object persists
for the session and its fd/telnet context is swapped under write_lock by
guac_serial_stream_reopen(), so the input thread never touches a stale fd.
- Line-ending translation (new "line-ending": cr|lf|crlf, default cr) applied to
typed Enter and pasted text via a small cross-buffer state machine.
- Distinct contention/error logging: EBUSY/EACCES/EPERM and TIOCEXCL-busy on
local devices, ECONNREFUSED/ECONNRESET on network transports.
- Local echo (new "local-echo", default false).
- Structured connect/disconnect reason logging (EOF, EIO, reset, hangup).
- Hangup-on-close (new "hangup-on-close" -> HUPCL) and DTR/RTS control-line
toggles via the serial-control pipe (dtr-on/off, rts-on/off), using
TIOCMBIS/TIOCMBIC locally and RFC2217 SET-CONTROL (DTR 8/9, RTS 11/12).
- Connect status banner with an "8N1"-style summary and press-Enter hint.
- Interruptible paste-delay pacing so a long paste cannot stall teardown.
The device allowlist is retained in settings and re-validated (re-realpath) on
every local (re)open.
diff --git a/src/protocols/serial/client.c b/src/protocols/serial/client.c
index 4969bb89..d441fc60 100644
--- a/src/protocols/serial/client.c
+++ b/src/protocols/serial/client.c
@@ -101,13 +101,13 @@ int guac_serial_client_free_handler(guac_client* client) {
guac_serial_client* serial_client = (guac_serial_client*) client->data;
- /* Close the serial stream's file descriptor, unblocking the client
- * thread's read loop. The descriptor is cleared so it is not closed a
- * second time when the stream is later freed. */
- if (serial_client->stream != NULL && serial_client->stream->fd != -1) {
- close(serial_client->stream->fd);
- serial_client->stream->fd = -1;
- }
+ /* Signal shutdown so the client thread's read loop, reconnect waits, and
+ * auto-reconnect loop all observe the stopped state and exit promptly. The
+ * stream's own file descriptor is not closed here (it is swapped under a
+ * lock during reconnects); the worker unblocks via its bounded poll timeout
+ * and the descriptor is closed when the stream is freed below, after the
+ * worker has been joined. */
+ guac_client_stop(client);
/* Stop the terminal — closing its input pipe to unblock the input thread —
* WITHOUT freeing it yet. The client thread may still be writing serial
diff --git a/src/protocols/serial/local.c b/src/protocols/serial/local.c
index d4b01a9b..3eb9b2c4 100644
--- a/src/protocols/serial/local.c
+++ b/src/protocols/serial/local.c
@@ -36,30 +36,67 @@
int guac_serial_local_open(guac_serial_stream* stream, guac_client* client,
guac_serial_settings* settings) {
+ /* Re-validate the device against the allowlist on every (re)open, guarding
+ * against a symlink being repointed at a disallowed device between
+ * reconnects */
+ if (settings->allowed_devices != NULL && settings->allowed_devices[0] != '\0'
+ && !guac_serial_device_permitted(settings->device,
+ settings->allowed_devices)) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" is not "
+ "permitted by the allowed-devices list, or is not currently "
+ "available.", settings->device);
+ stream->open_status = GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN;
+ return -1;
+ }
+
/* Open device non-blocking so open() does not wait for carrier detect */
int fd = open(settings->device, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
- int status = (errno == ENOENT)
- ? GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND
- : GUAC_PROTOCOL_STATUS_SERVER_ERROR;
- guac_client_abort(client, status,
- "Unable to open serial device \"%s\": %s",
- settings->device, strerror(errno));
+
+ /* Distinguish contention from other failures */
+ if (errno == EBUSY || errno == EACCES || errno == EPERM) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" is "
+ "busy or exclusively locked by another process: %s",
+ settings->device, strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else if (errno == ENOENT) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" not "
+ "found: %s", settings->device, strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
+ }
+ else {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to open serial "
+ "device \"%s\": %s", settings->device, strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+ }
+
return -1;
}
- /* Request exclusive access; warn but continue if unsupported */
- if (ioctl(fd, TIOCEXCL) != 0)
+ /* Request exclusive access. A busy result means another process holds the
+ * device exclusively; treat that as a distinct contention failure. Other
+ * failures are non-fatal (the device may simply not support it). */
+ if (ioctl(fd, TIOCEXCL) != 0) {
+ if (errno == EBUSY) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" is "
+ "busy or exclusively locked by another process.",
+ settings->device);
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ close(fd);
+ return -1;
+ }
guac_client_log(client, GUAC_LOG_WARNING, "Unable to obtain exclusive "
"access to serial device \"%s\": %s", settings->device,
strerror(errno));
+ }
/* Read current line settings */
struct termios tio;
if (tcgetattr(fd, &tio) != 0) {
- guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
- "Unable to read attributes of serial device \"%s\": %s",
- settings->device, strerror(errno));
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to read attributes of "
+ "serial device \"%s\": %s", settings->device, strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
close(fd);
return -1;
}
@@ -159,15 +196,22 @@ int guac_serial_local_open(guac_serial_stream* stream, guac_client* client,
}
+ /* Lower the modem control lines on close if requested (resets many
+ * attached devices), otherwise leave them raised */
+ if (settings->hangup_on_close)
+ tio.c_cflag |= HUPCL;
+ else
+ tio.c_cflag &= ~HUPCL;
+
/* Block for at least one byte per read, with no inter-byte timeout */
tio.c_cc[VMIN] = 1;
tio.c_cc[VTIME] = 0;
/* Apply configuration immediately and discard any pending I/O */
if (tcsetattr(fd, TCSANOW, &tio) != 0) {
- guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
- "Unable to configure serial device \"%s\": %s",
- settings->device, strerror(errno));
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to configure serial "
+ "device \"%s\": %s", settings->device, strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
close(fd);
return -1;
}
diff --git a/src/protocols/serial/pipe.c b/src/protocols/serial/pipe.c
index 8d8171d7..0fd59587 100644
--- a/src/protocols/serial/pipe.c
+++ b/src/protocols/serial/pipe.c
@@ -67,12 +67,27 @@ static void guac_serial_control_dispatch(guac_user* user, const char* token) {
guac_client* client = user->client;
guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_serial_stream* stream = serial_client->stream;
+
+ /* All commands operate on an established stream */
+ if (stream == NULL)
+ return;
/* Send a serial break */
- if (strcmp(token, "break") == 0) {
- if (serial_client->stream != NULL)
- guac_serial_stream_send_break(serial_client->stream);
- }
+ if (strcmp(token, "break") == 0)
+ guac_serial_stream_send_break(stream);
+
+ /* Toggle the DTR modem control line */
+ else if (strcmp(token, "dtr-on") == 0)
+ guac_serial_stream_set_line(stream, GUAC_SERIAL_CONTROL_LINE_DTR, true);
+ else if (strcmp(token, "dtr-off") == 0)
+ guac_serial_stream_set_line(stream, GUAC_SERIAL_CONTROL_LINE_DTR, false);
+
+ /* Toggle the RTS modem control line */
+ else if (strcmp(token, "rts-on") == 0)
+ guac_serial_stream_set_line(stream, GUAC_SERIAL_CONTROL_LINE_RTS, true);
+ else if (strcmp(token, "rts-off") == 0)
+ guac_serial_stream_set_line(stream, GUAC_SERIAL_CONTROL_LINE_RTS, false);
/* Ignore unrecognized commands */
else
diff --git a/src/protocols/serial/rfc2217.c b/src/protocols/serial/rfc2217.c
index 48502df5..e3ba18d9 100644
--- a/src/protocols/serial/rfc2217.c
+++ b/src/protocols/serial/rfc2217.c
@@ -35,9 +35,11 @@
#include <guacamole/tcp.h>
#include <libtelnet.h>
+#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdint.h>
+#include <string.h>
#include <unistd.h>
/**
@@ -241,9 +243,30 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
int fd = guac_tcp_connect(settings->hostname, settings->port,
settings->timeout);
if (fd < 0) {
- guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
- "Unable to connect to serial server \"%s\" port \"%s\".",
- settings->hostname, settings->port);
+
+ int err = errno;
+
+ /* Distinguish an actively-refused or reset endpoint from other
+ * failures. The client is not aborted here so the caller may retry. */
+ if (err == ECONNREFUSED) {
+ guac_client_log(client, GUAC_LOG_ERROR, "ser2net endpoint "
+ "\"%s:%s\" refused the connection: %s", settings->hostname,
+ settings->port, strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else if (err == ECONNRESET) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Connection to ser2net "
+ "endpoint \"%s:%s\" was reset: %s", settings->hostname,
+ settings->port, strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to connect to "
+ "serial server \"%s\" port \"%s\".", settings->hostname,
+ settings->port);
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
+ }
+
return -1;
}
@@ -260,8 +283,9 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
telnet_t* telnet = telnet_init(rfc2217_options,
guac_serial_rfc2217_event_handler, 0, stream);
if (telnet == NULL) {
- guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ guac_client_log(client, GUAC_LOG_ERROR,
"RFC2217 client allocation failed.");
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
close(fd);
stream->fd = -1;
return -1;
@@ -309,6 +333,11 @@ int guac_serial_rfc2217_send_break(guac_serial_stream* stream) {
}
+void guac_serial_rfc2217_set_control(guac_serial_stream* stream, int value) {
+ guac_serial_rfc2217_send_byte((telnet_t*) stream->telnet,
+ GUAC_SERIAL_RFC2217_SET_CONTROL, (unsigned char) value);
+}
+
void guac_serial_rfc2217_free(guac_serial_stream* stream) {
if (stream->telnet != NULL) {
telnet_free((telnet_t*) stream->telnet);
@@ -321,12 +350,11 @@ void guac_serial_rfc2217_free(guac_serial_stream* stream) {
int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
guac_serial_settings* settings) {
- (void) stream;
(void) settings;
- guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
- "RFC2217 support was not compiled in "
- "(rebuild guacamole-server with libtelnet).");
+ guac_client_log(client, GUAC_LOG_ERROR, "RFC2217 support was not compiled "
+ "in (rebuild guacamole-server with libtelnet).");
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
return -1;
}
@@ -353,6 +381,12 @@ int guac_serial_rfc2217_send_break(guac_serial_stream* stream) {
return 0;
}
+void guac_serial_rfc2217_set_control(guac_serial_stream* stream, int value) {
+ /* RFC2217 unavailable; a stream with this backend is never created */
+ (void) stream;
+ (void) value;
+}
+
void guac_serial_rfc2217_free(guac_serial_stream* stream) {
/* No libtelnet session can exist when RFC2217 is not compiled in */
(void) stream;
diff --git a/src/protocols/serial/rfc2217.h b/src/protocols/serial/rfc2217.h
index 971f6a22..adb44546 100644
--- a/src/protocols/serial/rfc2217.h
+++ b/src/protocols/serial/rfc2217.h
@@ -107,6 +107,21 @@ void guac_serial_rfc2217_recv(guac_serial_stream* stream, const char* buffer,
*/
int guac_serial_rfc2217_send_break(guac_serial_stream* stream);
+/**
+ * Sends a COM-PORT-OPTION SET-CONTROL subnegotiation with the given control
+ * value to the remote end of an RFC2217 connection. Used to toggle the modem
+ * control lines (e.g. DTR ON=8, DTR OFF=9, RTS ON=11, RTS OFF=12 per RFC 2217).
+ * This must only be called on a stream whose backend is
+ * GUAC_SERIAL_BACKEND_RFC2217.
+ *
+ * @param stream
+ * The RFC2217 serial stream on which to send the SET-CONTROL command.
+ *
+ * @param value
+ * The RFC 2217 SET-CONTROL value to send.
+ */
+void guac_serial_rfc2217_set_control(guac_serial_stream* stream, int value);
+
/**
* Frees the libtelnet session associated with the given stream, if any. Safe
* to call regardless of backend; has no effect if no session is present.
diff --git a/src/protocols/serial/serial.c b/src/protocols/serial/serial.c
index 93c9c38c..ff8f1b93 100644
--- a/src/protocols/serial/serial.c
+++ b/src/protocols/serial/serial.c
@@ -33,16 +33,209 @@
#include <guacamole/protocol.h>
#include <guacamole/recording.h>
+#include <errno.h>
#include <poll.h>
#include <pthread.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
+/**
+ * The reason a serial read loop exited, used to produce a concise, accurate
+ * disconnect message.
+ */
+typedef enum guac_serial_disconnect_reason {
+
+ /**
+ * The remote/device closed the connection cleanly (read() returned 0).
+ */
+ GUAC_SERIAL_DISCONNECT_EOF,
+
+ /**
+ * A device I/O error occurred (errno EIO), typically an unplugged device.
+ */
+ GUAC_SERIAL_DISCONNECT_IO_ERROR,
+
+ /**
+ * The connection was reset by the peer (errno ECONNRESET).
+ */
+ GUAC_SERIAL_DISCONNECT_RESET,
+
+ /**
+ * The descriptor hung up or reported an error condition (POLLHUP/POLLERR).
+ */
+ GUAC_SERIAL_DISCONNECT_HANGUP,
+
+ /**
+ * An otherwise-unclassified read or poll error occurred.
+ */
+ GUAC_SERIAL_DISCONNECT_ERROR,
+
+ /**
+ * The client is shutting down; the loop exited for teardown, not a fault.
+ */
+ GUAC_SERIAL_DISCONNECT_STOPPING
+
+} guac_serial_disconnect_reason;
+
+/**
+ * Returns the letter (N/E/O/M/S) representing the given parity scheme, as used
+ * in the "8N1"-style line settings summary.
+ *
+ * @param parity
+ * The parity scheme.
+ *
+ * @return
+ * The single-character parity letter.
+ */
+static char guac_serial_parity_letter(guac_serial_parity parity) {
+ switch (parity) {
+ case GUAC_SERIAL_PARITY_ODD: return 'O';
+ case GUAC_SERIAL_PARITY_EVEN: return 'E';
+ case GUAC_SERIAL_PARITY_MARK: return 'M';
+ case GUAC_SERIAL_PARITY_SPACE: return 'S';
+ default: return 'N';
+ }
+}
+
+/**
+ * Writes a human-readable description of the connection's endpoint (the local
+ * device path, or "host:port" for network connections) into the given buffer.
+ *
+ * @param settings
+ * The connection settings.
+ *
+ * @param buffer
+ * The buffer to write the endpoint description into.
+ *
+ * @param size
+ * The size of the buffer, in bytes.
+ */
+static void guac_serial_endpoint(guac_serial_settings* settings, char* buffer,
+ int size) {
+ if (settings->type == GUAC_SERIAL_TYPE_LOCAL)
+ snprintf(buffer, size, "%s", settings->device);
+ else
+ snprintf(buffer, size, "%s:%s", settings->hostname, settings->port);
+}
+
+/**
+ * Writes a concise, dim, bracketed status banner to the terminal on its own
+ * line, so a blank screen is diagnosable.
+ *
+ * @param term
+ * The terminal to write the banner to.
+ *
+ * @param text
+ * The banner text (without surrounding brackets or styling).
+ */
+static void guac_serial_write_banner(guac_terminal* term, const char* text) {
+ char buffer[512];
+ int length = snprintf(buffer, sizeof(buffer),
+ "\r\n\x1b[2m[serial: %s]\x1b[0m\r\n", text);
+ if (length > 0) {
+ if (length >= (int) sizeof(buffer))
+ length = sizeof(buffer) - 1;
+ guac_terminal_write(term, buffer, length);
+ }
+}
+
+/**
+ * Returns the wire terminator bytes and length for the given line ending.
+ *
+ * @param line_ending
+ * The configured line ending.
+ *
+ * @param terminator
+ * Set to the terminator byte sequence.
+ *
+ * @param length
+ * Set to the length of the terminator, in bytes.
+ */
+static void guac_serial_line_terminator(guac_serial_line_ending line_ending,
+ const char** terminator, int* length) {
+ switch (line_ending) {
+ case GUAC_SERIAL_LINE_ENDING_LF: *terminator = "\n"; *length = 1; break;
+ case GUAC_SERIAL_LINE_ENDING_CRLF: *terminator = "\r\n"; *length = 2; break;
+ default: *terminator = "\r"; *length = 1; break;
+ }
+}
+
+/**
+ * Translates outgoing line endings in the given input buffer to the configured
+ * terminator, collapsing any of "\r", "\n", or "\r\n" to a single terminator.
+ * A small cross-call state machine (saw_cr) ensures a "\r\n" pair split across
+ * buffers, or within one buffer, produces exactly one terminator. Non-newline
+ * bytes are passed through unchanged.
+ *
+ * @param line_ending
+ * The configured line ending.
+ *
+ * @param in
+ * The input buffer.
+ *
+ * @param in_length
+ * The number of bytes in the input buffer.
+ *
+ * @param out
+ * The output buffer, which must have capacity for at least twice
+ * in_length bytes.
+ *
+ * @param saw_cr
+ * Pointer to the carry state: non-zero if the previous byte processed was
+ * a carriage return whose terminator was already emitted. Must be
+ * initialized to zero before the first call and preserved across calls.
+ *
+ * @return
+ * The number of bytes written to the output buffer.
+ */
+static int guac_serial_translate_line_ending(guac_serial_line_ending line_ending,
+ const char* in, int in_length, char* out, int* saw_cr) {
+
+ const char* terminator;
+ int terminator_length;
+ guac_serial_line_terminator(line_ending, &terminator, &terminator_length);
+
+ int out_length = 0;
+ for (int i = 0; i < in_length; i++) {
+
+ char c = in[i];
+
+ /* A carriage return always emits one terminator */
+ if (c == '\r') {
+ memcpy(out + out_length, terminator, terminator_length);
+ out_length += terminator_length;
+ *saw_cr = 1;
+ }
+
+ /* A line feed emits a terminator unless it completes a "\r\n" pair */
+ else if (c == '\n') {
+ if (!*saw_cr) {
+ memcpy(out + out_length, terminator, terminator_length);
+ out_length += terminator_length;
+ }
+ *saw_cr = 0;
+ }
+
+ /* All other bytes pass through unchanged */
+ else {
+ out[out_length++] = c;
+ *saw_cr = 0;
+ }
+
+ }
+
+ return out_length;
+
+}
+
/**
* Input thread, started by the main serial client thread. This thread
- * continuously reads from the terminal's STDIN and transfers all read data to
- * the serial line.
+ * continuously reads from the terminal's STDIN, translates outgoing line
+ * endings, optionally echoes input locally, and transfers the data to the
+ * serial stream. It survives reconnects (the stream drops input while the
+ * transport is down) and exits only when STDIN closes at teardown.
*
* @param data
* The current guac_client instance.
@@ -58,15 +251,27 @@ static void* guac_serial_input_thread(void* data) {
guac_client* client = (guac_client*) data;
guac_serial_client* serial_client = (guac_serial_client*) client->data;
+ guac_serial_settings* settings = serial_client->settings;
char buffer[8192];
+ char translated[2 * sizeof(buffer)];
+ int saw_cr = 0;
int bytes_read;
/* Write all data read from STDIN to the serial stream */
while ((bytes_read = guac_terminal_read_stdin(serial_client->term, buffer,
sizeof(buffer))) > 0) {
- if (guac_serial_stream_write(serial_client->stream, buffer, bytes_read) < 0)
- break;
+
+ /* Translate outgoing line endings before pacing/writing */
+ int length = guac_serial_translate_line_ending(settings->line_ending,
+ buffer, bytes_read, translated, &saw_cr);
+
+ guac_serial_stream_write(serial_client->stream, translated, length);
+
+ /* Echo the user's typed bytes locally, if enabled */
+ if (settings->local_echo)
+ guac_terminal_write(serial_client->term, buffer, bytes_read);
+
}
return NULL;
@@ -74,30 +279,142 @@ static void* guac_serial_input_thread(void* data) {
}
/**
- * Waits for data on the given file descriptor for up to one second. The return
- * value is identical to that of poll(): 0 on timeout, < 0 on error, and > 0
- * when the descriptor is readable or has hung up.
+ * Runs the read loop for the currently-open serial stream, writing received
+ * data to the terminal until the connection drops or the client stops.
*
- * @param fd
- * The file descriptor to wait for.
+ * @param client
+ * The guac_client associated with the connection.
+ *
+ * @param serial_client
+ * The serial client data, whose stream must currently be open.
*
* @return
- * A value greater than zero when data is available or the descriptor has
- * hung up, zero on timeout, and less than zero on error.
+ * The reason the loop exited.
*/
-static int guac_serial_wait(int fd) {
+static guac_serial_disconnect_reason guac_serial_run_read_loop(
+ guac_client* client, guac_serial_client* serial_client) {
+
+ char buffer[8192];
+
+ while (client->state == GUAC_CLIENT_RUNNING) {
- /* Watch for readable data as well as hangup/error conditions */
- struct pollfd fds[] = {{
- .fd = fd,
- .events = POLLIN | POLLHUP | POLLERR,
- .revents = 0,
- }};
+ int fd = serial_client->stream->fd;
- int wait_result;
- GUAC_RETRY_EINTR(wait_result, poll(fds, 1, 1000));
+ /* Wait up to one second for data, hangup, or error */
+ struct pollfd pfd = { .fd = fd, .events = POLLIN, .revents = 0 };
+ int wait_result;
+ GUAC_RETRY_EINTR(wait_result, poll(&pfd, 1, 1000));
- return wait_result;
+ if (wait_result < 0)
+ return GUAC_SERIAL_DISCONNECT_ERROR;
+
+ /* Nothing to read yet */
+ if (wait_result == 0)
+ continue;
+
+ /* Hangup/error with no pending data ends the connection */
+ if ((pfd.revents & (POLLHUP | POLLERR | POLLNVAL))
+ && !(pfd.revents & POLLIN))
+ return GUAC_SERIAL_DISCONNECT_HANGUP;
+
+ int bytes_read;
+ GUAC_RETRY_EINTR(bytes_read, read(fd, buffer, sizeof(buffer)));
+
+ /* Classify the disconnect */
+ if (bytes_read == 0)
+ return GUAC_SERIAL_DISCONNECT_EOF;
+ if (bytes_read < 0) {
+ if (errno == EIO)
+ return GUAC_SERIAL_DISCONNECT_IO_ERROR;
+ if (errno == ECONNRESET)
+ return GUAC_SERIAL_DISCONNECT_RESET;
+ return GUAC_SERIAL_DISCONNECT_ERROR;
+ }
+
+ /* RFC2217 output must be de-framed; all other transports are raw */
+ if (serial_client->stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+ guac_serial_rfc2217_recv(serial_client->stream, buffer, bytes_read);
+ else
+ guac_terminal_write(serial_client->term, buffer, bytes_read);
+
+ }
+
+ return GUAC_SERIAL_DISCONNECT_STOPPING;
+
+}
+
+/**
+ * Logs the reason a serial connection dropped, at a level appropriate to
+ * whether it represents a normal end-of-connection or a fault.
+ *
+ * @param client
+ * The guac_client associated with the connection.
+ *
+ * @param reason
+ * The reason the connection dropped.
+ */
+static void guac_serial_log_disconnect(guac_client* client,
+ guac_serial_disconnect_reason reason) {
+
+ switch (reason) {
+
+ case GUAC_SERIAL_DISCONNECT_EOF:
+ guac_client_log(client, GUAC_LOG_INFO,
+ "Serial connection dropped: remote/device closed the "
+ "connection (EOF).");
+ break;
+
+ case GUAC_SERIAL_DISCONNECT_IO_ERROR:
+ guac_client_log(client, GUAC_LOG_WARNING,
+ "Serial connection dropped: device I/O error (unplugged?).");
+ break;
+
+ case GUAC_SERIAL_DISCONNECT_RESET:
+ guac_client_log(client, GUAC_LOG_WARNING,
+ "Serial connection dropped: connection reset.");
+ break;
+
+ case GUAC_SERIAL_DISCONNECT_HANGUP:
+ guac_client_log(client, GUAC_LOG_WARNING,
+ "Serial connection dropped: hangup.");
+ break;
+
+ case GUAC_SERIAL_DISCONNECT_ERROR:
+ guac_client_log(client, GUAC_LOG_WARNING,
+ "Serial connection dropped: read error.");
+ break;
+
+ case GUAC_SERIAL_DISCONNECT_STOPPING:
+ /* Normal teardown; nothing to report as a fault */
+ break;
+
+ }
+
+}
+
+/**
+ * Sleeps for the given number of seconds, returning early if the client stops
+ * running. The wait is broken into short slices so that teardown is prompt.
+ *
+ * @param client
+ * The client whose shutdown state is checked.
+ *
+ * @param seconds
+ * The number of seconds to wait.
+ *
+ * @return
+ * Non-zero if the client stopped running during the wait, zero otherwise.
+ */
+static int guac_serial_reconnect_wait(guac_client* client, int seconds) {
+
+ int milliseconds = seconds * 1000;
+ while (milliseconds > 0 && client->state == GUAC_CLIENT_RUNNING) {
+ int slice = milliseconds < 100 ? milliseconds : 100;
+ usleep((useconds_t) slice * 1000);
+ milliseconds -= slice;
+ }
+
+ return client->state != GUAC_CLIENT_RUNNING;
}
@@ -112,14 +429,21 @@ void* guac_serial_client_thread(void* data) {
guac_serial_settings* settings = serial_client->settings;
pthread_t input_thread;
- char buffer[8192];
- int wait_result;
+
+ /* Human-readable endpoint and "9600 8N1"-style line settings summary */
+ char endpoint[256];
+ guac_serial_endpoint(settings, endpoint, sizeof(endpoint));
+
+ char line_settings[64];
+ snprintf(line_settings, sizeof(line_settings), "%d %d%c%d",
+ settings->baud_rate, settings->data_bits,
+ guac_serial_parity_letter(settings->parity), settings->stop_bits);
/* Set process title to the device or hostname reached by this connection */
- const char* endpoint = settings->type == GUAC_SERIAL_TYPE_LOCAL
+ const char* title_host = settings->type == GUAC_SERIAL_TYPE_LOCAL
? settings->device : settings->hostname;
guac_process_title_set_endpoint(GUAC_SERIAL_PROCESS_TITLE_NAME,
- NULL, endpoint, NULL);
+ NULL, title_host, NULL);
/* Set up screen recording, if requested */
if (settings->recording_path != NULL) {
@@ -171,23 +495,18 @@ void* guac_serial_client_thread(void* data) {
settings->typescript_write_existing);
}
- /* Open the serial stream via the configured transport */
- serial_client->stream = guac_serial_stream_open(client, settings);
- if (serial_client->stream == NULL) {
- /* Already aborted within guac_serial_stream_open() */
- return NULL;
- }
-
- /* Connection established */
- guac_client_log(client, GUAC_LOG_INFO, "Serial connection established.");
+ /* Allocate the persistent stream (not yet connected) so it survives
+ * reconnects for the session lifetime */
+ serial_client->stream = guac_serial_stream_alloc(client, settings);
- /* Allow the terminal to render; there is no login detection */
+ /* Allow the terminal to render; there is no login detection. Started before
+ * the first connect so that (re)connection banners are visible. */
guac_terminal_start(serial_client->term);
/* Send current values of exposed arguments to owner only */
guac_client_for_owner(client, guac_serial_send_current_argv, serial_client);
- /* Start input thread */
+ /* Start input thread (persists across reconnects) */
if (pthread_create(&input_thread, NULL, guac_serial_input_thread,
(void*) client)) {
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
@@ -195,26 +514,90 @@ void* guac_serial_client_thread(void* data) {
return NULL;
}
- /* While data available, write to terminal */
- while ((wait_result = guac_serial_wait(serial_client->stream->fd)) >= 0) {
-
- /* Resume waiting if no data available */
- if (wait_result == 0)
- continue;
+ int connected = 0;
+ int first_attempt = 1;
+ int ever_connected = 0;
+
+ while (client->state == GUAC_CLIENT_RUNNING) {
+
+ /* (Re)connect if not currently connected */
+ if (!connected) {
+
+ /* Pace reconnect attempts, but do not wait before the very first
+ * connect attempt */
+ if (!first_attempt) {
+ if (guac_serial_reconnect_wait(client,
+ GUAC_SERIAL_RECONNECT_INTERVAL))
+ break;
+ }
+
+ int result = guac_serial_stream_reopen(serial_client->stream,
+ client, settings);
+
+ if (result != 0) {
+
+ /* A failed very-first connect with reconnect disabled aborts,
+ * exactly as before */
+ if (first_attempt && !settings->auto_reconnect) {
+ guac_client_abort(client, serial_client->stream->open_status,
+ "Unable to establish serial connection to %s.",
+ endpoint);
+ break;
+ }
+
+ /* Announce the wait so a blank screen is diagnosable */
+ if (first_attempt)
+ guac_serial_write_banner(serial_client->term,
+ "connecting…");
+
+ first_attempt = 0;
+ continue;
+
+ }
+
+ /* Connected */
+ connected = 1;
+ first_attempt = 0;
+
+ if (ever_connected) {
+ guac_client_log(client, GUAC_LOG_INFO, "Serial connection "
+ "re-established to %s (%s).", endpoint, line_settings);
+ guac_serial_write_banner(serial_client->term, "reconnected");
+ }
+ else
+ guac_client_log(client, GUAC_LOG_INFO, "Serial connection "
+ "established to %s (%s).", endpoint, line_settings);
+
+ ever_connected = 1;
+
+ /* Status banner with press-Enter hint (§7) */
+ char banner[384];
+ snprintf(banner, sizeof(banner),
+ "connected to %s @ %s — press Enter if blank",
+ endpoint, line_settings);
+ guac_serial_write_banner(serial_client->term, banner);
+
+ }
+
+ /* Run the read loop until the connection drops */
+ guac_serial_disconnect_reason reason =
+ guac_serial_run_read_loop(client, serial_client);
+ connected = 0;
+
+ /* Stop quietly if the client is shutting down */
+ if (reason == GUAC_SERIAL_DISCONNECT_STOPPING
+ || client->state != GUAC_CLIENT_RUNNING)
+ break;
- int bytes_read;
- GUAC_RETRY_EINTR(bytes_read,
- read(serial_client->stream->fd, buffer, sizeof(buffer)));
+ guac_serial_log_disconnect(client, reason);
- /* EOF, EIO, or hangup indicates the serial line has closed */
- if (bytes_read <= 0)
+ /* Without auto-reconnect, a drop ends the session */
+ if (!settings->auto_reconnect)
break;
- /* RFC2217 output must be de-framed; all other transports are raw */
- if (serial_client->stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
- guac_serial_rfc2217_recv(serial_client->stream, buffer, bytes_read);
- else
- guac_terminal_write(serial_client->term, buffer, bytes_read);
+ /* Announce reconnection and loop to retry */
+ guac_serial_write_banner(serial_client->term,
+ "connection lost — reconnecting…");
}
diff --git a/src/protocols/serial/settings.c b/src/protocols/serial/settings.c
index 770ff906..ddb68cc8 100644
--- a/src/protocols/serial/settings.c
+++ b/src/protocols/serial/settings.c
@@ -74,6 +74,10 @@ const char* GUAC_SERIAL_CLIENT_ARGS[] = {
"recording-include-clipboard",
"create-recording-path",
"recording-write-existing",
+ "auto-reconnect",
+ "line-ending",
+ "local-echo",
+ "hangup-on-close",
NULL
};
@@ -282,6 +286,30 @@ enum SERIAL_ARGS_IDX {
*/
IDX_RECORDING_WRITE_EXISTING,
+ /**
+ * Whether the connection should automatically reconnect if the serial line
+ * is dropped. Defaults to true.
+ */
+ IDX_AUTO_RECONNECT,
+
+ /**
+ * The line ending written to the serial line for the operator's outgoing
+ * newlines ("cr", "lf", or "crlf"). Defaults to "cr".
+ */
+ IDX_LINE_ENDING,
+
+ /**
+ * Whether user input should be echoed to the terminal locally. Defaults to
+ * false.
+ */
+ IDX_LOCAL_ECHO,
+
+ /**
+ * Whether the modem control lines should be lowered (HUPCL) when the local
+ * device is closed. Defaults to false.
+ */
+ IDX_HANGUP_ON_CLOSE,
+
SERIAL_ARGS_COUNT
};
@@ -383,33 +411,30 @@ static int guac_serial_parse_flow(const char* str, guac_serial_flow_control* flo
}
/**
- * Returns whether the given local device is permitted by the given ":"-
- * separated allowlist of device paths or path prefixes. Both the device and
- * each allowlist entry are canonicalized with realpath() before comparison; a
- * device is permitted if its canonical path equals a canonicalized entry or
- * falls beneath one of the canonicalized entries.
- *
- * @param user
- * The user on whose behalf error messages should be logged.
+ * Parses the given line-ending string into a guac_serial_line_ending value.
*
- * @param device
- * The local device path to test.
+ * @param str
+ * The line-ending string ("cr", "lf", or "crlf").
*
- * @param allowed
- * The ":"-separated allowlist of permitted device paths or prefixes.
+ * @param line_ending
+ * A pointer to the value that should receive the parsed result.
*
* @return
- * true if the device is permitted, false otherwise.
+ * Zero if the string was successfully parsed, non-zero otherwise.
*/
-static bool guac_serial_device_allowed(guac_user* user, const char* device,
- const char* allowed) {
+static int guac_serial_parse_line_ending(const char* str,
+ guac_serial_line_ending* line_ending) {
+ if (strcmp(str, "cr") == 0) { *line_ending = GUAC_SERIAL_LINE_ENDING_CR; return 0; }
+ if (strcmp(str, "lf") == 0) { *line_ending = GUAC_SERIAL_LINE_ENDING_LF; return 0; }
+ if (strcmp(str, "crlf") == 0) { *line_ending = GUAC_SERIAL_LINE_ENDING_CRLF; return 0; }
+ return 1;
+}
+
+bool guac_serial_device_permitted(const char* device, const char* allowed) {
char real_device[PATH_MAX];
- if (realpath(device, real_device) == NULL) {
- guac_user_log(user, GUAC_LOG_ERROR, "Unable to resolve serial device "
- "\"%s\": %s", device, strerror(errno));
+ if (realpath(device, real_device) == NULL)
return false;
- }
/* Duplicate the list so it may be tokenized in place */
char* list = guac_strdup(allowed);
@@ -446,11 +471,6 @@ static bool guac_serial_device_allowed(guac_user* user, const char* device,
}
guac_mem_free(list);
-
- if (!permitted)
- guac_user_log(user, GUAC_LOG_ERROR, "Serial device \"%s\" is not "
- "permitted by the configured allowed-devices list.", device);
-
return permitted;
}
@@ -618,18 +638,51 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
if (settings->paste_delay < 0)
settings->paste_delay = 0;
- /* Enforce local device allowlist, if configured */
- char* allowed = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
- argv, IDX_ALLOWED_DEVICES, NULL);
+ /* Parse auto-reconnect flag */
+ settings->auto_reconnect =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_AUTO_RECONNECT, true);
+
+ /* Parse line ending */
+ char* line_ending_str = guac_user_parse_args_string(user,
+ GUAC_SERIAL_CLIENT_ARGS, argv, IDX_LINE_ENDING, "cr");
+ if (guac_serial_parse_line_ending(line_ending_str, &settings->line_ending)) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Invalid line-ending \"%s\": must "
+ "be \"cr\", \"lf\", or \"crlf\".", line_ending_str);
+ guac_mem_free(line_ending_str);
+ guac_serial_settings_free(settings);
+ return NULL;
+ }
+ guac_mem_free(line_ending_str);
+
+ /* Parse local echo flag */
+ settings->local_echo =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_LOCAL_ECHO, false);
+
+ /* Parse hangup-on-close flag */
+ settings->hangup_on_close =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_HANGUP_ON_CLOSE, false);
+
+ /* Retain the device allowlist so it can be re-checked on every (re)open */
+ settings->allowed_devices =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_ALLOWED_DEVICES, NULL);
+
+ /* Enforce local device allowlist at parse time, if configured */
if (settings->type == GUAC_SERIAL_TYPE_LOCAL
- && allowed != NULL && allowed[0] != '\0') {
- if (!guac_serial_device_allowed(user, settings->device, allowed)) {
- guac_mem_free(allowed);
+ && settings->allowed_devices != NULL
+ && settings->allowed_devices[0] != '\0') {
+ if (!guac_serial_device_permitted(settings->device,
+ settings->allowed_devices)) {
+ guac_user_log(user, GUAC_LOG_ERROR, "Serial device \"%s\" is not "
+ "permitted by the configured allowed-devices list, or "
+ "could not be resolved.", settings->device);
guac_serial_settings_free(settings);
return NULL;
}
}
- guac_mem_free(allowed);
/* Read-only mode */
settings->read_only =
@@ -771,6 +824,7 @@ void guac_serial_settings_free(guac_serial_settings* settings) {
/* Free transport connection information */
guac_mem_free(settings->device);
+ guac_mem_free(settings->allowed_devices);
guac_mem_free(settings->hostname);
guac_mem_free(settings->port);
diff --git a/src/protocols/serial/settings.h b/src/protocols/serial/settings.h
index 728c8196..65dab32e 100644
--- a/src/protocols/serial/settings.h
+++ b/src/protocols/serial/settings.h
@@ -50,6 +50,12 @@
*/
#define GUAC_SERIAL_DEFAULT_BREAK_DURATION 500
+/**
+ * The interval, in seconds, between automatic reconnection attempts after the
+ * serial connection is dropped.
+ */
+#define GUAC_SERIAL_RECONNECT_INTERVAL 2
+
/**
* The filename to use for the typescript, if not specified.
*/
@@ -118,6 +124,30 @@ typedef enum guac_serial_flow_control {
GUAC_SERIAL_FLOW_XON_XOFF
} guac_serial_flow_control;
+/**
+ * The line ending written to the serial line in place of the operator's
+ * outgoing carriage returns/newlines.
+ */
+typedef enum guac_serial_line_ending {
+
+ /**
+ * A single carriage return ("\r"). This is what most network device
+ * consoles (e.g. Cisco) expect, and is the default.
+ */
+ GUAC_SERIAL_LINE_ENDING_CR,
+
+ /**
+ * A single line feed ("\n").
+ */
+ GUAC_SERIAL_LINE_ENDING_LF,
+
+ /**
+ * A carriage return followed by a line feed ("\r\n").
+ */
+ GUAC_SERIAL_LINE_ENDING_CRLF
+
+} guac_serial_line_ending;
+
/**
* Settings for the serial connection. The values for this structure are parsed
* from the arguments given during the Guacamole protocol handshake using the
@@ -142,6 +172,14 @@ typedef struct guac_serial_settings {
*/
char* device;
+ /**
+ * An optional ":"-separated allowlist of permitted local device paths or
+ * path prefixes, or NULL if no allowlist was configured. Retained so the
+ * device can be re-validated on every (re)open, guarding against a symlink
+ * being repointed at a disallowed device between reconnects.
+ */
+ char* allowed_devices;
+
/**
* The hostname of the network serial server to connect to when type is
* GUAC_SERIAL_TYPE_NETWORK. NULL if not a network connection.
@@ -197,6 +235,30 @@ typedef struct guac_serial_settings {
*/
int paste_delay;
+ /**
+ * Whether the connection should automatically attempt to reconnect if the
+ * serial line is dropped, keeping the terminal and its scrollback intact.
+ */
+ bool auto_reconnect;
+
+ /**
+ * The line ending written to the serial line in place of the operator's
+ * outgoing carriage returns/newlines.
+ */
+ guac_serial_line_ending line_ending;
+
+ /**
+ * Whether user input should be echoed to the terminal locally. Useful for
+ * devices or bootloaders which do not echo typed characters themselves.
+ */
+ bool local_echo;
+
+ /**
+ * Whether the modem control lines should be lowered (HUPCL) when the local
+ * device is closed, which resets many attached devices.
+ */
+ bool hangup_on_close;
+
/**
* Whether this connection is read-only, and user input should be dropped.
*/
@@ -387,6 +449,28 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
*/
speed_t guac_serial_baud_to_speed(int baud_rate);
+/**
+ * Returns whether the given local device is permitted by the given ":"-
+ * separated allowlist of device paths or path prefixes. Both the device and
+ * each allowlist entry are canonicalized with realpath() before comparison; a
+ * device is permitted if its canonical path equals a canonicalized entry or
+ * falls beneath one of the canonicalized entries. If the device cannot be
+ * resolved (e.g. it does not currently exist), it is not permitted.
+ *
+ * This performs no logging so that it may be reused both at parse time and on
+ * every (re)open.
+ *
+ * @param device
+ * The local device path to test.
+ *
+ * @param allowed
+ * The ":"-separated allowlist of permitted device paths or prefixes.
+ *
+ * @return
+ * true if the device is permitted, false otherwise.
+ */
+bool guac_serial_device_permitted(const char* device, const char* allowed);
+
/**
* Frees the given guac_serial_settings object, having been previously
* allocated via guac_serial_parse_args().
diff --git a/src/protocols/serial/stream.c b/src/protocols/serial/stream.c
index 1d189413..cd1c3c0e 100644
--- a/src/protocols/serial/stream.c
+++ b/src/protocols/serial/stream.c
@@ -28,6 +28,7 @@
#include <guacamole/client.h>
#include <guacamole/error.h>
#include <guacamole/mem.h>
+#include <guacamole/protocol.h>
#include <errno.h>
#include <string.h>
@@ -35,6 +36,13 @@
#include <termios.h>
#include <unistd.h>
+/**
+ * The granularity, in milliseconds, of the interruptible sleep used to pace
+ * byte-by-byte writes. Shorter slices make teardown more responsive during a
+ * long paced paste; longer slices reduce wakeups.
+ */
+#define GUAC_SERIAL_PACING_SLICE 20
+
/**
* Writes the entire buffer given to the specified file descriptor, retrying
* the write automatically if necessary.
@@ -71,7 +79,30 @@ static int guac_serial_stream_write_all(int fd, const char* buffer, int size) {
}
-guac_serial_stream* guac_serial_stream_open(guac_client* client,
+/**
+ * Sleeps for the given number of milliseconds, returning early if the client
+ * stops running. The wait is broken into short slices so that teardown is not
+ * delayed by a long paced write.
+ *
+ * @param stream
+ * The serial stream whose client is checked for shutdown.
+ *
+ * @param milliseconds
+ * The number of milliseconds to sleep.
+ */
+static void guac_serial_stream_interruptible_sleep(guac_serial_stream* stream,
+ int milliseconds) {
+
+ while (milliseconds > 0 && stream->client->state == GUAC_CLIENT_RUNNING) {
+ int slice = milliseconds < GUAC_SERIAL_PACING_SLICE
+ ? milliseconds : GUAC_SERIAL_PACING_SLICE;
+ usleep((useconds_t) slice * 1000);
+ milliseconds -= slice;
+ }
+
+}
+
+guac_serial_stream* guac_serial_stream_alloc(guac_client* client,
guac_serial_settings* settings) {
guac_serial_stream* stream = guac_mem_zalloc(sizeof(guac_serial_stream));
@@ -80,9 +111,28 @@ guac_serial_stream* guac_serial_stream_open(guac_client* client,
stream->telnet = NULL;
stream->paste_delay = settings->paste_delay;
stream->break_duration = settings->break_duration;
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
pthread_mutex_init(&stream->write_lock, NULL);
- /* Dispatch to the appropriate backend */
+ return stream;
+
+}
+
+int guac_serial_stream_reopen(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ pthread_mutex_lock(&stream->write_lock);
+
+ /* Tear down any existing transport so the fd/telnet are never stale */
+ guac_serial_rfc2217_free(stream);
+ if (stream->fd != -1) {
+ close(stream->fd);
+ stream->fd = -1;
+ }
+
+ /* Dispatch to the appropriate backend. Backends set stream->fd on success,
+ * or set stream->open_status and return non-zero on failure without
+ * aborting the client (so the caller may retry). */
int result;
if (settings->type == GUAC_SERIAL_TYPE_LOCAL)
result = guac_serial_local_open(stream, client, settings);
@@ -91,37 +141,45 @@ guac_serial_stream* guac_serial_stream_open(guac_client* client,
else
result = guac_serial_tcp_open(stream, client, settings);
- /* Clean up and return NULL on failure (backend has already aborted) */
- if (result != 0) {
- pthread_mutex_destroy(&stream->write_lock);
- guac_mem_free(stream);
- return NULL;
- }
+ /* Ensure the fd is left invalid on failure */
+ if (result != 0)
+ stream->fd = -1;
- return stream;
+ pthread_mutex_unlock(&stream->write_lock);
+ return result;
}
int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
int length) {
- int result = length;
-
pthread_mutex_lock(&stream->write_lock);
+ /* Drop input while the transport is down (e.g. mid-reconnect) rather than
+ * blocking or writing to a stale descriptor */
+ if (stream->fd < 0) {
+ pthread_mutex_unlock(&stream->write_lock);
+ return length;
+ }
+
/* Pace output byte-by-byte when a paste delay is configured */
if (stream->paste_delay > 0) {
for (int i = 0; i < length; i++) {
+ /* Bail promptly if the session is tearing down, so a long paste
+ * cannot hold up teardown */
+ if (stream->client->state != GUAC_CLIENT_RUNNING)
+ break;
+
if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
guac_serial_rfc2217_send(stream, buffer + i, 1);
- else if (guac_serial_stream_write_all(stream->fd, buffer + i, 1) != 1) {
- result = -1;
+ else if (guac_serial_stream_write_all(stream->fd, buffer + i, 1) != 1)
+ /* The descriptor went bad; the read loop will detect the drop
+ * and reconnect, so simply stop writing */
break;
- }
- usleep((useconds_t) stream->paste_delay * 1000);
+ guac_serial_stream_interruptible_sleep(stream, stream->paste_delay);
}
@@ -132,13 +190,13 @@ int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
guac_serial_rfc2217_send(stream, buffer, length);
- else if (guac_serial_stream_write_all(stream->fd, buffer, length) != length)
- result = -1;
+ else
+ guac_serial_stream_write_all(stream->fd, buffer, length);
}
pthread_mutex_unlock(&stream->write_lock);
- return result;
+ return length;
}
@@ -148,6 +206,12 @@ int guac_serial_stream_send_break(guac_serial_stream* stream) {
pthread_mutex_lock(&stream->write_lock);
+ /* Ignore if the transport is currently down */
+ if (stream->fd < 0) {
+ pthread_mutex_unlock(&stream->write_lock);
+ return 0;
+ }
+
switch (stream->backend) {
case GUAC_SERIAL_BACKEND_LOCAL:
@@ -184,6 +248,60 @@ int guac_serial_stream_send_break(guac_serial_stream* stream) {
}
+int guac_serial_stream_set_line(guac_serial_stream* stream,
+ guac_serial_control_line line, bool state) {
+
+ int result = 0;
+ const char* line_name = (line == GUAC_SERIAL_CONTROL_LINE_DTR)
+ ? "DTR" : "RTS";
+
+ pthread_mutex_lock(&stream->write_lock);
+
+ /* Ignore if the transport is currently down */
+ if (stream->fd < 0) {
+ pthread_mutex_unlock(&stream->write_lock);
+ return 0;
+ }
+
+ switch (stream->backend) {
+
+ case GUAC_SERIAL_BACKEND_LOCAL: {
+ int flag = (line == GUAC_SERIAL_CONTROL_LINE_DTR)
+ ? TIOCM_DTR : TIOCM_RTS;
+ if (ioctl(stream->fd, state ? TIOCMBIS : TIOCMBIC, &flag) != 0) {
+ guac_client_log(stream->client, GUAC_LOG_WARNING, "Unable to "
+ "set %s %s: %s", line_name, state ? "on" : "off",
+ strerror(errno));
+ result = -1;
+ }
+ break;
+ }
+
+ case GUAC_SERIAL_BACKEND_TCP:
+ guac_client_log(stream->client, GUAC_LOG_WARNING, "Control-line "
+ "toggles (DTR/RTS) require the rfc2217 transport; ignoring "
+ "on raw TCP transport.");
+ break;
+
+ case GUAC_SERIAL_BACKEND_RFC2217: {
+ /* RFC2217 SET-CONTROL values: DTR ON=8, DTR OFF=9, RTS ON=11,
+ * RTS OFF=12 */
+ int value;
+ if (line == GUAC_SERIAL_CONTROL_LINE_DTR)
+ value = state ? 8 : 9;
+ else
+ value = state ? 11 : 12;
+ guac_serial_rfc2217_set_control(stream, value);
+ break;
+ }
+
+ }
+
+ pthread_mutex_unlock(&stream->write_lock);
+ return result;
+
+}
+
void guac_serial_stream_close(guac_serial_stream* stream) {
/* Nothing to do if never opened */
diff --git a/src/protocols/serial/stream.h b/src/protocols/serial/stream.h
index b3c7c542..339119bb 100644
--- a/src/protocols/serial/stream.h
+++ b/src/protocols/serial/stream.h
@@ -49,6 +49,24 @@ typedef enum guac_serial_backend {
} guac_serial_backend;
+/**
+ * A modem control line which may be asserted or de-asserted on the serial
+ * stream.
+ */
+typedef enum guac_serial_control_line {
+
+ /**
+ * The Data Terminal Ready (DTR) line.
+ */
+ GUAC_SERIAL_CONTROL_LINE_DTR,
+
+ /**
+ * The Request To Send (RTS) line.
+ */
+ GUAC_SERIAL_CONTROL_LINE_RTS
+
+} guac_serial_control_line;
+
/**
* A transport-neutral, bidirectional serial byte stream. All reads are
* performed directly on the file descriptor by the main worker thread; writes
@@ -99,25 +117,54 @@ typedef struct guac_serial_stream {
*/
guac_client* client;
+ /**
+ * The guac_protocol_status describing the most recent failed open attempt.
+ * Used by the initial connect path to abort with an appropriate status.
+ */
+ int open_status;
+
} guac_serial_stream;
/**
- * Opens a serial byte stream using the transport described by the given
- * settings, dispatching to the local, TCP, or RFC2217 backend as appropriate.
- * On failure, the client is aborted and NULL is returned.
+ * Allocates a serial byte stream for the given client, without yet opening any
+ * transport. The returned stream has no open file descriptor (fd is -1) until
+ * guac_serial_stream_reopen() succeeds. This allows a single stream object to
+ * persist across reconnects for the lifetime of the session.
*
* @param client
- * The client for which the stream is being opened.
+ * The client for which the stream is being allocated.
*
* @param settings
* The parsed connection settings describing the transport to use.
*
* @return
* A newly-allocated serial stream which must be freed with
- * guac_serial_stream_close(), or NULL if the connection could not be
- * established.
+ * guac_serial_stream_close().
+ */
+guac_serial_stream* guac_serial_stream_alloc(guac_client* client,
+ guac_serial_settings* settings);
+
+/**
+ * (Re)opens the transport for the given serial stream, closing any currently-
+ * open file descriptor and libtelnet session first, then dispatching to the
+ * local, TCP, or RFC2217 backend as appropriate. The stream's fd is swapped
+ * under its write lock so that a concurrent writer never touches a stale
+ * descriptor. On failure, the stream's open_status is set and the fd is left
+ * at -1; the client is NOT aborted, so the caller may retry.
+ *
+ * @param stream
+ * The serial stream to (re)open.
+ *
+ * @param client
+ * The client for which the transport is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the transport to use.
+ *
+ * @return
+ * Zero on success, non-zero on failure.
*/
-guac_serial_stream* guac_serial_stream_open(guac_client* client,
+int guac_serial_stream_reopen(guac_serial_stream* stream, guac_client* client,
guac_serial_settings* settings);
/**
@@ -156,6 +203,28 @@ int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
*/
int guac_serial_stream_send_break(guac_serial_stream* stream);
+/**
+ * Asserts or de-asserts a modem control line (DTR or RTS) on the serial stream.
+ * For local transports the line is toggled via TIOCMBIS/TIOCMBIC; for RFC2217
+ * transports the corresponding COM-PORT-OPTION SET-CONTROL subnegotiation is
+ * sent. Raw TCP transports do not support control-line signalling; a warning
+ * is logged and the call is a no-op.
+ *
+ * @param stream
+ * The serial stream on which to toggle the control line.
+ *
+ * @param line
+ * The control line to toggle (DTR or RTS).
+ *
+ * @param state
+ * true to assert (raise) the line, false to de-assert (lower) it.
+ *
+ * @return
+ * Zero on success, non-zero on error.
+ */
+int guac_serial_stream_set_line(guac_serial_stream* stream,
+ guac_serial_control_line line, bool state);
+
/**
* Closes the given serial stream, releasing all associated resources. This
* function is idempotent and safe to call with a NULL stream.
diff --git a/src/protocols/serial/tcp.c b/src/protocols/serial/tcp.c
index 9f825456..a45594a0 100644
--- a/src/protocols/serial/tcp.c
+++ b/src/protocols/serial/tcp.c
@@ -27,8 +27,10 @@
#include <guacamole/protocol.h>
#include <guacamole/tcp.h>
+#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
+#include <string.h>
int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
guac_serial_settings* settings) {
@@ -37,9 +39,30 @@ int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
int fd = guac_tcp_connect(settings->hostname, settings->port,
settings->timeout);
if (fd < 0) {
- guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
- "Unable to connect to serial server \"%s\" port \"%s\".",
- settings->hostname, settings->port);
+
+ int err = errno;
+
+ /* Distinguish an actively-refused or reset endpoint from other
+ * failures (e.g. name resolution, timeout) */
+ if (err == ECONNREFUSED) {
+ guac_client_log(client, GUAC_LOG_ERROR, "ser2net endpoint "
+ "\"%s:%s\" refused the connection: %s", settings->hostname,
+ settings->port, strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else if (err == ECONNRESET) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Connection to ser2net "
+ "endpoint \"%s:%s\" was reset: %s", settings->hostname,
+ settings->port, strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to connect to "
+ "serial server \"%s\" port \"%s\".", settings->hostname,
+ settings->port);
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
+ }
+
return -1;
}