tio/src/socket.c
Peter Collingbourne fb453160ef Add support for external control via a Unix domain socket.
This feature allows an external program to inject output into and
listen to input from a serial port via a Unix domain socket (path
specified via the -S/--socket command line flag, or the socket
config file option) while tio is running. This is useful for ad-hoc
scripting of serial port interactions while still permitting manual
control. Since many serial devices (at least on Linux) get confused
when opened by multiple processes, and most commands do not know
how to correctly open a serial device, this allows a more convenient
usage model than directly writing to the device node from an external
program.

Any input from clients connected to the socket is sent on the serial
port as if entered at the terminal where tio is running (except that
ctrl-t sequences are not recognized), and any input from the serial
port is multiplexed to the terminal and all connected clients.

Sockets remain open while the serial port is disconnected, and writes
will block.

Example usage 1 (issue a command):

  echo command | nc -UN /path/to/socket > /dev/null

Example usage 2 (use the expect command to script an interaction):

  #!/usr/bin/expect -f

  set timeout -1
  log_user 0

  spawn nc -UN /path/to/socket
  set uart $spawn_id

  send -i $uart "command1\n"
  expect -i $uart "prompt> "
  send -i $uart "command2\n"
  expect -i $uart "prompt> "
2022-04-18 14:06:33 -07:00

195 lines
4.9 KiB
C

/*
* tio - a simple serial terminal I/O tool
*
* Copyright (c) 2014-2022 Martin Lund
* Copyright (c) 2022 Google LLC
*
* 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 2
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#include "socket.h"
#include "options.h"
#include "print.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define MAX_SOCKET_CLIENTS 16
static int sockfd;
static int clientfds[MAX_SOCKET_CLIENTS];
static const char *socket_filename(void)
{
/* skip 'unix:' */
return option.socket + 5;
}
static void socket_exit(void)
{
unlink(socket_filename());
}
void socket_configure(void)
{
if (!option.socket)
{
return;
}
if (strncmp(option.socket, "unix:", 5) != 0)
{
error_printf("%s: Invalid socket scheme, must be 'unix:'", option.socket);
exit(EXIT_FAILURE);
}
struct sockaddr_un sockaddr = {};
if (strlen(socket_filename()) > sizeof(sockaddr.sun_path) - 1)
{
error_printf("Socket file path %s too long", option.socket);
exit(EXIT_FAILURE);
}
sockaddr.sun_family = AF_UNIX;
strncpy(sockaddr.sun_path, socket_filename(), sizeof(sockaddr.sun_path) - 1);
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0)
{
error_printf("Failed to create socket: %s", strerror(errno));
exit(EXIT_FAILURE);
}
unlink(socket_filename());
if (bind(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
{
error_printf("Failed to bind to socket %s: %s", socket_filename(), strerror(errno));
exit(EXIT_FAILURE);
}
if (listen(sockfd, MAX_SOCKET_CLIENTS) < 0)
{
error_printf("Failed to listen on socket %s: %s", socket_filename(), strerror(errno));
exit(EXIT_FAILURE);
}
memset(clientfds, -1, sizeof(clientfds));
atexit(socket_exit);
}
void socket_write(char input_char)
{
if (!option.socket)
{
return;
}
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
{
if (clientfds[i] != -1)
{
if (write(clientfds[i], &input_char, 1) <= 0)
{
error_printf_silent("Failed to write to socket: %s", strerror(errno));
close(clientfds[i]);
clientfds[i] = -1;
}
}
}
}
int socket_add_fds(fd_set *rdfs, bool connected)
{
if (!option.socket)
{
return 0;
}
int numclients = 0, maxfd = 0;
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
{
if (clientfds[i] != -1)
{
/* let clients block if they try to send while we're disconnected */
if (connected)
{
FD_SET(clientfds[i], rdfs);
maxfd = MAX(maxfd, clientfds[i]);
}
numclients++;
}
}
/* don't bother to accept clients if we're already full */
if (numclients != MAX_SOCKET_CLIENTS)
{
FD_SET(sockfd, rdfs);
maxfd = MAX(maxfd, sockfd);
}
return maxfd;
}
bool socket_handle_input(fd_set *rdfs, char *output_char)
{
if (!option.socket)
{
return false;
}
if (FD_ISSET(sockfd, rdfs))
{
int clientfd = accept(sockfd, NULL, NULL);
/* this loop should always succeed because we don't select on sockfd when full */
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
{
if (clientfds[i] == -1)
{
clientfds[i] = clientfd;
break;
}
}
}
for (int i = 0; i != MAX_SOCKET_CLIENTS; ++i)
{
if (clientfds[i] != -1 && FD_ISSET(clientfds[i], rdfs))
{
int status = read(clientfds[i], output_char, 1);
if (status == 0)
{
close(clientfds[i]);
clientfds[i] = -1;
continue;
}
if (status < 0)
{
error_printf_silent("Failed to read from socket: %s", strerror(errno));
close(clientfds[i]);
clientfds[i] = -1;
continue;
}
/* match the behavior of a terminal in raw mode */
if (*output_char == '\n')
{
*output_char = '\r';
}
return true;
}
}
return false;
}