diff --git a/serialsock/Readme.md b/serialsock/Readme.md new file mode 100644 index 0000000..1f6ffc8 --- /dev/null +++ b/serialsock/Readme.md @@ -0,0 +1,169 @@ +# Multi-Client Serial Port Proxy + +`sersock` is a lightweight TCP/UNIX socket‑to‑serial proxy that enables **multiple simultaneous +clients** to read from and write to a single serial (TTY) device. It runs as a robust daemon that +automatically respawns its worker child, supports logging, and works with both local and remote +socket connections. + +--- + +## Table of Contents +- [Features](#features) +- [How It Works](#how-it-works) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Command-Line Arguments](#command-line-arguments) +- [Usage Examples](#usage-examples) +- [Socket Node Format](#socket-node-format) +- [Signal Handling](#signal-handling) +- [Logging](#logging) +- [Building from Source](#building-from-source) +- [License](#license) + +--- + +## Features +- **Multi‑client support** – Up to 30 TCP clients can connect simultaneously. +- **Serial device management** – Configurable baud rate, path, and exclusive access. +- **Daemon mode** – Runs in background with automatic child process respawn. +- **Command‑line configuration** – All options available via CLI arguments. +- **Verbose logging** – Adjustable log level, optional file logging with flock(). +- **Signal‑safe** – Proper cleanup of PID files, serial ports, and terminal settings on exit. + +--- + +## How It Works +The program operates in two modes, selected by the `--client` flag: + +| Mode | Description | +|------|-------------| +| **Server** (default) | Opens the serial device, creates a listening socket, and forks a worker child. The child manages all client connections (up to 30). If the child dies, the parent automatically respawns it after a short delay. | +| **Client** | Connects to the server socket and forwards terminal input to the server. Data received from the server is printed on the client’s terminal. | + +The server daemonises itself, writes its PID to a file (default: `/tmp/usbsock.pid`), and changes +the working directory to `/` to avoid keeping any directory busy. + +--- + +## Prerequisites +- Linux operating system +- [`libusefull_macros`](https://github.com/eddyem/snippets_library) – the core utility library used by sersock. + This library provides: + - Command‑line parsing with `sl_parseargs` + - Serial port (TTY) handling via `sl_tty_t` + - TCP socket management (`sl_sock_open`) + - Daemonisation, PID file handling, and logging macros (`LOGMSG`, `LOGWARN`, `LOGERR`, etc.) + +--- + +## Installation + +### 1. Install `libusefull_macros` +```bash +git clone --depth=1 https://github.com/eddyem/snippets_library.git +cd snippets_library +mkdir build && cd build +cmake .. +make +su -c "make install" +``` + +### 2. Build `sersock` +```bash +git clone --depth=1 https://github.com/eddyem/eddys_snippets +cd serialsock +make + +``` + +The resulting binary is `serialsock`. You can copy it wherever you want, e.g. +```bash +su -c "cp serialsock /usr/local/bin +``` + + +--- + +## Command-Line Arguments + +| Option | Argument | Default | Description | +|--------|----------|---------|-------------| +| `-h, --help` | – | – | Show help message and exit. | +| `-d, --devpath` | *path* | – | Path to the serial device (e.g., `/dev/ttyUSB0`). **Required in server mode.** | +| `-s, --speed` | *baud* | `9600` | Serial device baud rate. | +| `-l, --logfile` | *file* | – | File to write logs (uses `flock(LOCK_EX)`). | +| `-p, --pidfile` | *file* | `/tmp/usbsock.pid` | PID file for the server. | +| `-c, --client` | – | – | Run as a client (connect to server). | +| `-n, --node` | *node* | – | Socket node (see [format](#socket-node-format)). | +| `-v, --verbose` | – | – | Increase log verbosity (can be used multiple times). | + +> **Note:** In server mode, `--devpath` must be provided. `--node` must be provided in any mode. + +--- + +## Usage Examples + +### Server – Share a serial device on TCP port 12345 +```bash +sersock -d /dev/ttyACM0 -s 115200 -n :12345 -v -l /var/log/sersock.log +``` +- Listens on TCP port 12345. +- Uses baud rate 115200. +- Logs to `/var/log/sersock.log` with increased verbosity. + +### Client – Connect to the TCP server +```bash +sersock -c -n localhost:12345 +``` +Opens an interactive terminal where every line entered is sent to the serial port; data received +from the serial port is displayed. + +You also can connect to this socket using my [tty_term](https://github.com/eddyem/tty_term), +netcat or any other same terminal socket client. + +--- + +## Socket Node Format + +The `-n` parameter accepts only TCP-sockets. + +- **Server**: `":port"` – listens on all interfaces (e.g., `":12345"`). +- **Client**: `"host:port"` – connects to the specified host (e.g., `"localhost:12345"` or `"192.168.1.10:8080"`). + +If you want to open server listening only local clients, point node as `-n localhost:port`, +e.g. `-n localhost:12345`. + +--- + +## Signal Handling + +| Signal | Effect | +|--------|--------| +| `SIGTERM`, `SIGINT`, `SIGQUIT` | Gracefully close serial port, remove PID file, restore terminal settings (client), and exit. | +| `SIGTSTP` (Ctrl+Z), `SIGHUP` | Ignored in server mode. | + +If the worker child dies unexpectedly, the master process waits 1–10 seconds and respawns it +automatically. + +--- + +## Logging + +- **Console logging** – Messages appear on stderr unless daemonised. +- **File logging** – Use `-l` to write logs to a file. + File writes are guarded with `flock(LOCK_EX)` to be safe across processes. +- **Verbosity** – Each `-v` increases the log level (up to `LOGLEVEL_ANY`). + Default level is `LOGLEVEL_WARN`. + +--- + +## License + +Copyright 2022 Edward V. Emelianov + +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 3 of the License, or +(at your option) any later version. + +Full text of the GPLv3 is available at . diff --git a/serialsock/canserver.config b/serialsock/canserver.config index c49fee0..320fc0e 100644 --- a/serialsock/canserver.config +++ b/serialsock/canserver.config @@ -1,6 +1,6 @@ // Add predefined macros for your project here. For example: // #define THE_ANSWER 42 -#define EBUG +//#define EBUG #define _GNU_SOURCE #define _XOPEN_SOURCE=1111 diff --git a/serialsock/canserver.creator.user b/serialsock/canserver.creator.user index 106092c..cd1b0e6 100644 --- a/serialsock/canserver.creator.user +++ b/serialsock/canserver.creator.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -89,12 +89,14 @@ true + 0 ProjectExplorer.Project.Target.0 Desktop + true Desktop Desktop {91347f2c-5221-46a7-80b1-0a054ca02f79} @@ -112,8 +114,8 @@ GenericProjectManager.GenericMakeStep 1 - Сборка - Сборка + Build + Build ProjectExplorer.BuildSteps.Build @@ -125,8 +127,8 @@ GenericProjectManager.GenericMakeStep 1 - Очистка - Очистка + Clean + Clean ProjectExplorer.BuildSteps.Clean 2 @@ -136,13 +138,47 @@ Default GenericProjectManager.GenericBuildConfiguration + 0 + 0 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + true + + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + ProjectExplorer.CustomExecutableRunConfiguration + + false + + true + true + + 1 1 0 - Развёртывание - Развёртывание + Deploy + Deploy ProjectExplorer.BuildSteps.Deploy 1 @@ -155,6 +191,7 @@ true true true + 2 @@ -164,6 +201,7 @@ ProjectExplorer.CustomExecutableRunConfiguration false + true true @@ -174,10 +212,6 @@ ProjectExplorer.Project.TargetCount 1 - - ProjectExplorer.Project.Updater.FileVersion - 22 - Version 22 diff --git a/serialsock/canserver.files b/serialsock/canserver.files index a52c6f3..bf52a1b 100644 --- a/serialsock/canserver.files +++ b/serialsock/canserver.files @@ -1,3 +1,4 @@ +Readme.md cmdlnopts.c cmdlnopts.h main.c diff --git a/serialsock/cmdlnopts.c b/serialsock/cmdlnopts.c index e9ff7ce..2c167fb 100644 --- a/serialsock/cmdlnopts.c +++ b/serialsock/cmdlnopts.c @@ -25,13 +25,11 @@ // default PID filename: #define DEFAULT_PIDFILE "/tmp/usbsock.pid" -#define DEFAULT_SOCKPATH "\0canbus" static int help; static glob_pars G = { .pidfile = DEFAULT_PIDFILE, .speed = 9600, - .path = DEFAULT_SOCKPATH, .logfile = NULL // don't save logs }; glob_pars *GP = &G; @@ -47,8 +45,8 @@ static sl_option_t cmdlnopts[] = { {"logfile", NEED_ARG, NULL, 'l', arg_string, APTR(&G.logfile), _("file to save logs (default: none)")}, {"pidfile", NEED_ARG, NULL, 'p', arg_string, APTR(&G.pidfile), _("pidfile (default: " DEFAULT_PIDFILE ")")}, {"client", NO_ARGS, NULL, 'c', arg_int, APTR(&G.client), _("run as client")}, - {"sockpath",NEED_ARG, NULL, 'f', arg_string, APTR(&G.path), _("socket path (start from \\0 for no files) or port")}, - {"verbose", NO_ARGS, NULL, 'v', arg_none, APTR(&G.verbose), _("increase log verbose level (default: LOG_WARN) and messages (default: none)")}, + {"node", NEED_ARG, NULL, 'n', arg_string, APTR(&G.path), _("socket node: 'localhost:port' or just ':port' for non-local socket")}, + {"verbose", NO_ARGS, NULL, 'v', arg_none, APTR(&G.verbose), _("increase log print_message level (default: LOG_WARN) and messages (default: none)")}, end_option }; @@ -74,11 +72,11 @@ void parse_args(int argc, char **argv){ } /** - * @brief verbose - print additional messages depending of G.verbose (add '\n' at end) + * @brief print_message - print additional messages depending of G.verbose (add '\n' at end) * @param levl - message level * @param fmt - message */ -void verbose(int levl, const char *fmt, ...){ +void print_message(int levl, const char *fmt, ...){ va_list ar; if(levl > G.verbose) return; //printf("%s: ", __progname); diff --git a/serialsock/cmdlnopts.h b/serialsock/cmdlnopts.h index 15de59e..243a416 100644 --- a/serialsock/cmdlnopts.h +++ b/serialsock/cmdlnopts.h @@ -17,8 +17,6 @@ */ #pragma once -#ifndef CMDLNOPTS_H__ -#define CMDLNOPTS_H__ /* * here are some typedef's for global data @@ -29,13 +27,12 @@ typedef struct{ char *logfile; // logging to this file char *path; // path to socket file int speed; // connection speed - int verbose; // verbose level: for messages & logging + int verbose; // print_message level: for messages & logging int client; // ==1 if application runs in client mode } glob_pars; extern glob_pars *GP; void parse_args(int argc, char **argv); -void verbose(int levl, const char *fmt, ...); +void print_message(int levl, const char *fmt, ...); -#endif // CMDLNOPTS_H__ diff --git a/serialsock/main.c b/serialsock/main.c index d1f4253..ed0d976 100644 --- a/serialsock/main.c +++ b/serialsock/main.c @@ -53,33 +53,34 @@ void signals(int sig){ } int main(int argc, char **argv){ - char *self = strdup(argv[0]); sl_init(); parse_args(argc, argv); - if(GP->logfile){ - int lvl = LOGLEVEL_WARN + GP->verbose; + if(!GP->path) ERRX("Point node to listen/connect"); + int lvl = LOGLEVEL_WARN; + if(GP->client) server = 0; + else if(!GP->devpath) ERRX("You should point serial device path"); + if(chdir("/")) ERR("chdir()"); + if(GP->logfile && server){ + lvl += GP->verbose; DBG("level = %d", lvl); if(lvl > LOGLEVEL_ANY) lvl = LOGLEVEL_ANY; - verbose(1, "Log file %s @ level %d\n", GP->logfile, lvl); + print_message(1, "Log file %s @ level %d\n", GP->logfile, lvl); OPENLOG(GP->logfile, lvl, 1); - if(!sl_globlog) WARNX("Can't create log file"); + if(!sl_globlog) ERRX("Can't create log file"); + sl_deletelog(&sl_globlog); } - if(GP->client) server = 0; - else if(!GP->devpath){ - LOGERR("You should point serial device path"); - ERRX("You should point serial device path"); - } - if(server) sl_check4running(self, GP->pidfile); - // signal reactions: - signal(SIGTERM, signals); // kill (-15) - quit - signal(SIGHUP, SIG_IGN); // hup - ignore - signal(SIGINT, signals); // ctrl+C - quit - signal(SIGQUIT, signals); // ctrl+\ - quit - signal(SIGTSTP, SIG_IGN); // ignore ctrl+Z - LOGMSG("Started"); -#ifndef EBUG if(server){ - unsigned int pause = 5; +#ifndef EBUG + sl_check4running(NULL, GP->pidfile); + sl_daemonize(); + if(GP->logfile) OPENLOG(GP->logfile, lvl, 1); + // signal reactions: + signal(SIGTERM, signals); // kill (-15) - quit + signal(SIGINT, signals); // ctrl+C - quit + signal(SIGQUIT, signals); // ctrl+\ - quit + signal(SIGTSTP, SIG_IGN); // ignore ctrl+Z + LOGMSG("Started"); + unsigned int pause = 1; while(1){ childpid = fork(); if(childpid){ // master @@ -87,16 +88,16 @@ int main(int argc, char **argv){ LOGMSG("Created child with pid %d", childpid); wait(NULL); LOGWARN("Child %d died", childpid); - if(sl_dtime() - t0 < 1.) pause += 5; + if(sl_dtime() - t0 < 1.) ++pause; else pause = 1; - if(pause > 60) pause = 60; + if(pause > 10) pause = 10; sleep(pause); // wait a little before respawn }else{ // slave prctl(PR_SET_PDEATHSIG, SIGTERM); // send SIGTERM to child when parent dies break; } } - } #endif + } return start_socket(server, GP->path, &dev); } diff --git a/serialsock/sersock.c b/serialsock/sersock.c index 9bd964b..588f310 100644 --- a/serialsock/sersock.c +++ b/serialsock/sersock.c @@ -21,7 +21,6 @@ #include #include #include -#include // unix socket #include "cmdlnopts.h" #include "sersock.h" @@ -49,37 +48,6 @@ static int handle_socket(int sock, sl_tty_t *d){ return TRUE; } -/** - * check data from fd - * @param fd - file descriptor - * @return 0 in case of timeout, 1 in case of fd have data, -1 if error - */ -static int canberead(int fd){ - fd_set fds; - struct timeval timeout; - timeout.tv_sec = 0; - timeout.tv_usec = 100; - FD_ZERO(&fds); - FD_SET(fd, &fds); - do{ - int rc = select(fd+1, &fds, NULL, NULL, &timeout); - if(rc < 0){ - if(errno != EINTR){ - LOGWARN("select()"); - WARN("select()"); - return -1; - } - continue; - } - break; - }while(1); - if(FD_ISSET(fd, &fds)){ - DBG("FD_ISSET"); - return 1; - } - return 0; -} - static void server_(int sock, sl_tty_t *d){ if(listen(sock, MAXCLIENTS) == -1){ WARN("listen"); @@ -98,22 +66,24 @@ static void server_(int sock, sl_tty_t *d){ poll_set[0].fd = sock; poll_set[0].events = POLLIN; while(1){ - poll(poll_set, nfd, 1); // max timeout - 1ms + if(-1 == poll(poll_set, nfd, 1)) continue; // max timeout - 1ms if(poll_set[0].revents & POLLIN){ // check main for accept() struct sockaddr_in addr; socklen_t len = sizeof(addr); int client = accept(sock, (struct sockaddr*)&addr, &len); - DBG("New connection"); - LOGMSG("Connection, fd=%d", client); - if(nfd == MAXCLIENTS + 1){ - LOGWARN("Max amount of connections, disconnect fd=%d", client); - WARNX("Limit of connections reached"); - close(client); - }else{ - memset(&poll_set[nfd], 0, sizeof(struct pollfd)); - poll_set[nfd].fd = client; - poll_set[nfd].events = POLLIN; - ++nfd; + if(client > -1){ + DBG("New connection"); + LOGMSG("Connection, fd=%d", client); + if(nfd == MAXCLIENTS + 1){ + LOGWARN("Max amount of connections, disconnect fd=%d", client); + WARNX("Limit of connections reached"); + close(client); + }else{ + memset(&poll_set[nfd], 0, sizeof(struct pollfd)); + poll_set[nfd].fd = client; + poll_set[nfd].events = POLLIN; + ++nfd; + } } } int l = sl_tty_read(d); @@ -148,33 +118,16 @@ static void server_(int sock, sl_tty_t *d){ } } -// read console char - for client -static int rc(){ - int rb; - struct timeval tv; - int retval; - fd_set rfds; - FD_ZERO(&rfds); - FD_SET(STDIN_FILENO, &rfds); - tv.tv_sec = 0; tv.tv_usec = 100; - retval = select(1, &rfds, NULL, NULL, &tv); - if(!retval) rb = 0; - else { - if(FD_ISSET(STDIN_FILENO, &rfds)) rb = getchar(); - else rb = 0; - } - return rb; -} - /** * @brief mygetline - silently and non-blocking getline * @return zero-terminated string with '\n' at end (or without in case of overflow) */ static char *mygetline(){ - static char buf[BUFLEN+1]; + static char buf[BUFLEN+2]; static int i = 0; + if(!sl_canread(STDIN_FILENO)) return NULL; while(i < BUFLEN){ - char rd = rc(); + char rd = sl_getchar(); if(!rd) return NULL; if(rd == 0x7f && i){ // backspace buf[--i] = 0; @@ -186,6 +139,7 @@ static char *mygetline(){ fflush(stdout); if(rd == '\n') break; } + if(buf[i-1] != '\n') buf[i++] = '\n'; buf[i] = 0; i = 0; return buf; @@ -206,7 +160,12 @@ static void client_(int sock){ if(msg[L-1] == '\n') msg[L-1] = 0; LOGMSG("TERMINAL: %s", msg); } - if(1 != canberead(sock)) continue; + int canread = sl_canread(sock); + if(canread == -1){ + WARNX("Server disconnected!"); + signals(0); + } + if(canread == 0) continue; int n = read(sock, recvBuff, Bufsiz-1); if(n == 0){ WARNX("Server disconnected"); @@ -238,91 +197,23 @@ static sl_tty_t *openserialdev(char *path, int speed){ } int start_socket(int server, char *path, sl_tty_t **dev){ - char apath[128]; DBG("path: %s", path); - char *eptr; - long port = strtol(path, &eptr, 0); - if(eptr && *eptr){ - DBG("UNIX socket"); - port = -1; - if(*path == 0){ - DBG("convert name"); - apath[0] = 0; - strncpy(apath+1, path+1, 126); - }else if(strncmp("\\0", path, 2) == 0){ - DBG("convert name"); - apath[0] = 0; - strncpy(apath+1, path+2, 126); - }else strcpy(apath, path); - } if(server){ if(!dev) return 1; if(!(*dev = openserialdev(GP->devpath, GP->speed))){ LOGERR("Can't open serial device %s", GP->devpath); ERR("Can't open serial device %s", GP->devpath); } - unlink(path); // remove old socket } - int sock = -1; - struct addrinfo hints = {0}, *res; - struct sockaddr_un unaddr = {0}; - unaddr.sun_family = AF_UNIX; - if(port > 0){ - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = AI_PASSIVE; - if(getaddrinfo("127.0.0.1", path, &hints, &res) != 0){ - ERR("getaddrinfo"); - } - }else{ - memcpy(unaddr.sun_path, apath, 106); // if sun_path[0] == 0 we don't create a file - hints.ai_addr = (struct sockaddr*) &unaddr; - hints.ai_addrlen = sizeof(unaddr); - hints.ai_family = AF_UNIX; - hints.ai_socktype = SOCK_SEQPACKET; - res = &hints; - } - for(struct addrinfo *p = res; p; p = p->ai_next){ - if((sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0){ // or SOCK_STREAM? - LOGWARN("socket()"); - WARN("socket()"); - continue; - } - if(server){ - int reuseaddr = 1; - if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(int)) == -1){ - LOGWARN("setsockopt()"); - WARN("setsockopt()"); - close(sock); sock = -1; - continue; - } - if(bind(sock, p->ai_addr, p->ai_addrlen) == -1){ - LOGWARN("bind()"); - WARN("bind()"); - close(sock); sock = -1; - continue; - } - int enable = 1; - if(ioctl(sock, FIONBIO, (void *)&enable) < 0){ // make socket nonblocking - LOGERR("Can't make socket nonblocking"); - ERRX("ioctl()"); - } - }else{ - if(connect(sock, p->ai_addr, p->ai_addrlen) == -1){ - LOGWARN("connect()"); - WARN("connect()"); - close(sock); sock = -1; - } - } - break; - } - if(sock < 0){ + int fd = sl_sock_open(SOCKT_NET, path, server, 0); + if(fd < 0){ + WARNX("Can't open socket"); LOGERR("Can't open socket"); - ERRX("Can't open socket"); + return 1; } - if(server) server_(sock, *dev); - else client_(sock); - close(sock); + if(server) server_(fd, *dev); + else client_(fd); + close(fd); signals(0); return 0; } diff --git a/serialsock/sersock.h b/serialsock/sersock.h index ebbb853..22be863 100644 --- a/serialsock/sersock.h +++ b/serialsock/sersock.h @@ -17,8 +17,6 @@ */ #pragma once -#ifndef SERSOCK_H__ -#define SERSOCK_H__ #define BUFLEN (1024) // Max amount of connections @@ -28,4 +26,4 @@ int start_socket(int server, char *path, sl_tty_t **dev); -#endif // SERSOCK_H__ +