diff --git a/.gitignore b/.gitignore index c562d901c..50bbbe42a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,5 +49,6 @@ tests/proxy-test-client tests/proxy-test-server tests/unit-test-client tests/unit-test-server +tests/unit-test-transport tests/version tests/stamp-h2 diff --git a/docs/index.md b/docs/index.md index 6308d10ef..a15b3d1f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -206,6 +206,11 @@ Setter/getter of internal socket: - [modbus_set_socket](modbus_set_socket.md) - [modbus_get_socket](modbus_get_socket.md) +Pluggable I/O transport: + +- [modbus_set_transport](modbus_set_transport.md) +- [modbus_get_transport](modbus_get_transport.md) + Information about header: - [modbus_get_header_length](modbus_get_header_length.md) diff --git a/docs/modbus_get_transport.md b/docs/modbus_get_transport.md new file mode 100644 index 000000000..ce8023604 --- /dev/null +++ b/docs/modbus_get_transport.md @@ -0,0 +1,25 @@ +# modbus_get_transport + +## Name + +modbus_get_transport - get the registered I/O transport + +## Synopsis + +```c +modbus_transport_t *modbus_get_transport(modbus_t *ctx); +``` + +## Description + +The *modbus_get_transport()* function shall return the pluggable I/O transport +registered on the libmodbus context *ctx* with *modbus_set_transport()*, or NULL +if no transport is set. + +## Return value + +The function shall return the registered transport, or NULL if none is set. + +## See also + +- [modbus_set_transport](modbus_set_transport.md) diff --git a/docs/modbus_set_transport.md b/docs/modbus_set_transport.md new file mode 100644 index 000000000..fd8c59c36 --- /dev/null +++ b/docs/modbus_set_transport.md @@ -0,0 +1,125 @@ +# modbus_set_transport + +## Name + +modbus_set_transport - register a pluggable I/O transport + +## Synopsis + +```c +int modbus_set_transport(modbus_t *ctx, modbus_transport_t *transport); +``` + +## Description + +The *modbus_set_transport()* function shall register a custom I/O *transport* on +the libmodbus context *ctx*. The transport replaces the low-level send, recv, +select, connect, close and flush calls while leaving the Modbus framing, CRC and +protocol logic unchanged. It is useful to route Modbus over a custom or +userspace IP stack, to drive a loopback for simulation, or to instrument the I/O +path in tests. + +This function must be called after *modbus_new_tcp()* or *modbus_new_rtu()* and +before *modbus_connect()*. The context takes ownership of the transport: +*modbus_free()* shall call its *free()* member if non-NULL. Passing NULL detaches +a previously registered transport without calling *free()*, so a transport may be +stack-allocated or shared. + +The transport is described by the *modbus_transport_t* structure. Every function +pointer is optional; a NULL member falls back to the default backend behaviour +for that operation. + +```c +typedef struct modbus_transport { + int (*connect)(struct modbus_transport *t); + ssize_t (*send)(struct modbus_transport *t, const uint8_t *buf, int len); + ssize_t (*recv)(struct modbus_transport *t, uint8_t *buf, int len); + int (*select)(struct modbus_transport *t, struct timeval *tv); + int (*flush)(struct modbus_transport *t); + void (*close)(struct modbus_transport *t); + void (*free)(struct modbus_transport *t); + void *priv; /* private transport state, not touched by libmodbus */ + int connected; /* managed by modbus_connect()/modbus_close() */ +} modbus_transport_t; +``` + +The *connect()* member is called by *modbus_connect()*, *send()* transmits a +fully framed ADU, *recv()* reads up to *len* bytes and returns the number read or +0 when the peer closes, *select()* waits until data can be read or the timeout +*tv* expires (NULL waits indefinitely) and returns a positive value when data is +available or 0 on timeout, *flush()* discards pending input, *close()* tears the +connection down without freeing the struct, and *free()* releases all resources. +On error, members return -1 and set errno. With *MODBUS_ERROR_RECOVERY_LINK* (see +[modbus_set_error_recovery](modbus_set_error_recovery.md)), that errno decides +the recovery on every platform: errors such as ECONNRESET or EBADF mean the link +is lost, so libmodbus calls *close()* and *connect()* before retrying. +Connection parameters such as the address and port must be stored in *priv* +before the transport is registered, since the members receive only the +transport pointer. + +## Return value + +The *modbus_set_transport()* function shall return 0 if successful. Otherwise it +shall return -1 and set errno to EINVAL if *ctx* is NULL. + +## Example + +```c +typedef struct { + mystack_conn_t *conn; +} my_priv_t; + +static int my_connect(modbus_transport_t *t) +{ + my_priv_t *p = t->priv; + p->conn = mystack_connect("192.168.1.10", 502); + return p->conn ? 0 : -1; +} + +static ssize_t my_send(modbus_transport_t *t, const uint8_t *buf, int len) +{ + return mystack_send(((my_priv_t *) t->priv)->conn, buf, len); +} + +static ssize_t my_recv(modbus_transport_t *t, uint8_t *buf, int len) +{ + return mystack_recv(((my_priv_t *) t->priv)->conn, buf, len); +} + +static int my_select(modbus_transport_t *t, struct timeval *tv) +{ + return mystack_wait_rx(((my_priv_t *) t->priv)->conn, tv); +} + +static void my_close(modbus_transport_t *t) +{ + mystack_close(((my_priv_t *) t->priv)->conn); +} + +my_priv_t priv = {0}; +modbus_transport_t tr = { + .connect = my_connect, + .send = my_send, + .recv = my_recv, + .select = my_select, + .close = my_close, + .priv = &priv, +}; + +ctx = modbus_new_tcp("192.168.1.10", 502); +modbus_set_transport(ctx, &tr); +modbus_connect(ctx); + +modbus_read_registers(ctx, 0, 10, regs); + +modbus_close(ctx); +modbus_free(ctx); +``` + +## See also + +- [modbus_get_transport](modbus_get_transport.md) +- [modbus_new_tcp](modbus_new_tcp.md) +- [modbus_new_rtu](modbus_new_rtu.md) +- [modbus_connect](modbus_connect.md) +- [modbus_set_socket](modbus_set_socket.md) diff --git a/src/Makefile.am b/src/Makefile.am index 157e9200c..48e464967 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -20,6 +20,7 @@ libmodbus_la_SOURCES = \ modbus-tcp.c \ modbus-tcp.h \ modbus-tcp-private.h \ + modbus-transport.h \ modbus-version.h libmodbus_la_LDFLAGS = -no-undefined \ @@ -35,7 +36,7 @@ endif # Header files to install libmodbusincludedir = $(includedir)/modbus -libmodbusinclude_HEADERS = modbus.h modbus-version.h modbus-rtu.h modbus-tcp.h +libmodbusinclude_HEADERS = modbus.h modbus-version.h modbus-rtu.h modbus-tcp.h modbus-transport.h DISTCLEANFILES = modbus-version.h EXTRA_DIST += modbus-version.h.in diff --git a/src/modbus-private.h b/src/modbus-private.h index ea83187f7..300d7fb1b 100644 --- a/src/modbus-private.h +++ b/src/modbus-private.h @@ -21,6 +21,7 @@ typedef int ssize_t; #include #include "modbus.h" +#include "modbus-transport.h" MODBUS_BEGIN_DECLS @@ -106,6 +107,9 @@ struct _modbus { struct timeval indication_timeout; const modbus_backend_t *backend; void *backend_data; + /* Optional pluggable I/O transport (see modbus-transport.h). + * NULL means use the default backend I/O path. */ + modbus_transport_t *transport; }; void _modbus_init_common(modbus_t *ctx); diff --git a/src/modbus-transport.h b/src/modbus-transport.h new file mode 100644 index 000000000..2170810b3 --- /dev/null +++ b/src/modbus-transport.h @@ -0,0 +1,48 @@ +/* + * Copyright © Stéphane Raimbault + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef MODBUS_TRANSPORT_H +#define MODBUS_TRANSPORT_H + +/* clang-format off */ +#ifndef _MSC_VER +# include +# include +# include +#else +# include "stdint.h" +# include +typedef int ssize_t; +#endif +/* clang-format on */ + +#include "modbus.h" + +MODBUS_BEGIN_DECLS + +/* Pluggable I/O transport: replaces the low-level send, recv, select, connect, + * close and flush calls while leaving the Modbus framing, CRC and protocol + * logic unchanged. Every function pointer is optional; a NULL pointer falls + * back to the default backend behaviour for that operation. See + * modbus_set_transport(3). */ +typedef struct modbus_transport { + int (*connect)(struct modbus_transport *t); + ssize_t (*send)(struct modbus_transport *t, const uint8_t *buf, int len); + ssize_t (*recv)(struct modbus_transport *t, uint8_t *buf, int len); + int (*select)(struct modbus_transport *t, struct timeval *tv); + int (*flush)(struct modbus_transport *t); + void (*close)(struct modbus_transport *t); + void (*free)(struct modbus_transport *t); + void *priv; /* Private transport state, not touched by libmodbus */ + int connected; /* Managed by modbus_connect()/modbus_close() */ +} modbus_transport_t; + +MODBUS_API int modbus_set_transport(modbus_t *ctx, modbus_transport_t *transport); +MODBUS_API modbus_transport_t *modbus_get_transport(modbus_t *ctx); + +MODBUS_END_DECLS + +#endif /* MODBUS_TRANSPORT_H */ diff --git a/src/modbus.c b/src/modbus.c index abdd718c4..2ed1ec1f6 100644 --- a/src/modbus.c +++ b/src/modbus.c @@ -121,7 +121,11 @@ int modbus_flush(modbus_t *ctx) return -1; } - rc = ctx->backend->flush(ctx); + if (ctx->transport && ctx->transport->flush) { + rc = ctx->transport->flush(ctx->transport); + } else { + rc = ctx->backend->flush(ctx); + } if (rc != -1 && ctx->debug) { /* Not all backends are able to return the number of bytes flushed */ printf("Bytes flushed (%d)\n", rc); @@ -182,39 +186,48 @@ static int send_msg(modbus_t *ctx, uint8_t *msg, int msg_length) /* In recovery mode, the write command will be issued until to be successful! Disabled by default. */ do { - rc = ctx->backend->send(ctx, msg, msg_length); + if (ctx->transport && ctx->transport->send) { + rc = ctx->transport->send(ctx->transport, msg, msg_length); + } else { + rc = ctx->backend->send(ctx, msg, msg_length); + } if (rc == -1) { _error_print(ctx, NULL); if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) { #ifdef _WIN32 - int saved_errno = errno; - const int wsa_err = WSAGetLastError(); - - if (wsa_err == WSAENETRESET || wsa_err == WSAENOTCONN || - wsa_err == WSAENOTSOCK || wsa_err == WSAESHUTDOWN || - wsa_err == WSAEHOSTUNREACH || wsa_err == WSAECONNABORTED || - wsa_err == WSAECONNRESET || wsa_err == WSAETIMEDOUT) { - modbus_close(ctx); - _sleep_response_timeout(ctx); - modbus_connect(ctx); - } else { - _sleep_response_timeout(ctx); - modbus_flush(ctx); - } - errno = saved_errno; -#else - int saved_errno = errno; - - if ((errno == EBADF || errno == ECONNRESET || errno == EPIPE)) { - modbus_close(ctx); - _sleep_response_timeout(ctx); - modbus_connect(ctx); - } else { - _sleep_response_timeout(ctx); - modbus_flush(ctx); - } - errno = saved_errno; + /* Only the socket backend reports its errors through Winsock; a + transport sets errno, as on every other platform. */ + if (!ctx->transport) { + int saved_errno = errno; + const int wsa_err = WSAGetLastError(); + + if (wsa_err == WSAENETRESET || wsa_err == WSAENOTCONN || + wsa_err == WSAENOTSOCK || wsa_err == WSAESHUTDOWN || + wsa_err == WSAEHOSTUNREACH || wsa_err == WSAECONNABORTED || + wsa_err == WSAECONNRESET || wsa_err == WSAETIMEDOUT) { + modbus_close(ctx); + _sleep_response_timeout(ctx); + modbus_connect(ctx); + } else { + _sleep_response_timeout(ctx); + modbus_flush(ctx); + } + errno = saved_errno; + } else #endif + { + int saved_errno = errno; + + if ((errno == EBADF || errno == ECONNRESET || errno == EPIPE)) { + modbus_close(ctx); + _sleep_response_timeout(ctx); + modbus_connect(ctx); + } else { + _sleep_response_timeout(ctx); + modbus_flush(ctx); + } + errno = saved_errno; + } } } } while ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) && rc == -1); @@ -383,23 +396,36 @@ int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) } } - if (!ctx->backend->is_connected(ctx)) { - if (ctx->debug) { - fprintf(stderr, "ERROR The connection is not established.\n"); + /* Connection check: transport tracks its own state; backend uses ctx->s */ + if (ctx->transport) { + if (!ctx->transport->connected) { + if (ctx->debug) { + fprintf(stderr, "ERROR Transport is not connected.\n"); + } + errno = EBADF; + return -1; + } + } else { + if (!ctx->backend->is_connected(ctx)) { + if (ctx->debug) { + fprintf(stderr, "ERROR The connection is not established.\n"); + } + return -1; } - return -1; } - /* Add a file descriptor to the set */ + /* fd_set only used by the default backend select path */ FD_ZERO(&rset); - if (ctx->s < 0 || ctx->s >= FD_SETSIZE) { - if (ctx->debug) { - fprintf(stderr, "ERROR Invalid socket descriptor %d\n", ctx->s); + if (!ctx->transport) { + if (ctx->s < 0 || ctx->s >= FD_SETSIZE) { + if (ctx->debug) { + fprintf(stderr, "ERROR Invalid socket descriptor %d\n", ctx->s); + } + errno = EINVAL; + return -1; } - errno = EINVAL; - return -1; + FD_SET(ctx->s, &rset); } - FD_SET(ctx->s, &rset); /* We need to analyse the message step by step. At the first step, we want * to reach the function code because all packets contain this @@ -426,38 +452,56 @@ int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) } while (length_to_read != 0) { - rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read); + if (ctx->transport && ctx->transport->select) { + rc = ctx->transport->select(ctx->transport, p_tv); + if (rc == 0) { + /* Timeout, as select(2) reports it. The backends return -1 with + ETIMEDOUT instead, which the code below expects. */ + errno = ETIMEDOUT; + rc = -1; + } + } else { + rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read); + } if (rc == -1) { _error_print(ctx, "select"); if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) { #ifdef _WIN32 - int saved_errno = errno; - - wsa_err = WSAGetLastError(); - - // no equivalent to ETIMEDOUT when select fails on Windows - if (wsa_err == WSAENETDOWN || wsa_err == WSAENOTSOCK) { - modbus_close(ctx); - modbus_connect(ctx); - } - errno = saved_errno; -#else - int saved_errno = errno; - - if (errno == ETIMEDOUT) { - _sleep_response_timeout(ctx); - modbus_flush(ctx); - } else if (errno == EBADF) { - modbus_close(ctx); - modbus_connect(ctx); - } - errno = saved_errno; + if (!ctx->transport) { + int saved_errno = errno; + + wsa_err = WSAGetLastError(); + + // no equivalent to ETIMEDOUT when select fails on Windows + if (wsa_err == WSAENETDOWN || wsa_err == WSAENOTSOCK) { + modbus_close(ctx); + modbus_connect(ctx); + } + errno = saved_errno; + } else #endif + { + int saved_errno = errno; + + if (errno == ETIMEDOUT) { + _sleep_response_timeout(ctx); + modbus_flush(ctx); + } else if (errno == EBADF) { + modbus_close(ctx); + modbus_connect(ctx); + } + errno = saved_errno; + } } return -1; } - rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read); + if (ctx->transport && ctx->transport->recv) { + rc = ctx->transport->recv(ctx->transport, + msg + msg_length, length_to_read); + } else { + rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read); + } if (rc == 0) { errno = ECONNRESET; rc = -1; @@ -466,28 +510,31 @@ int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) if (rc == -1) { _error_print(ctx, "read"); #ifdef _WIN32 - wsa_err = WSAGetLastError(); - if ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) && - (wsa_err == WSAENOTCONN || wsa_err == WSAENETRESET || - wsa_err == WSAENOTSOCK || wsa_err == WSAESHUTDOWN || - wsa_err == WSAECONNABORTED || wsa_err == WSAETIMEDOUT || - wsa_err == WSAECONNRESET)) { - int saved_errno = errno; - modbus_close(ctx); - modbus_connect(ctx); - /* Could be removed by previous calls */ - errno = saved_errno; - } -#else - if ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) && - (errno == ECONNRESET || errno == ECONNREFUSED || errno == EBADF)) { - int saved_errno = errno; - modbus_close(ctx); - modbus_connect(ctx); - /* Could be removed by previous calls */ - errno = saved_errno; - } + if (!ctx->transport) { + wsa_err = WSAGetLastError(); + if ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) && + (wsa_err == WSAENOTCONN || wsa_err == WSAENETRESET || + wsa_err == WSAENOTSOCK || wsa_err == WSAESHUTDOWN || + wsa_err == WSAECONNABORTED || wsa_err == WSAETIMEDOUT || + wsa_err == WSAECONNRESET)) { + int saved_errno = errno; + modbus_close(ctx); + modbus_connect(ctx); + /* Could be removed by previous calls */ + errno = saved_errno; + } + } else #endif + { + if ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) && + (errno == ECONNRESET || errno == ECONNREFUSED || errno == EBADF)) { + int saved_errno = errno; + modbus_close(ctx); + modbus_connect(ctx); + /* Could be removed by previous calls */ + errno = saved_errno; + } + } return -1; } @@ -1994,6 +2041,9 @@ void _modbus_init_common(modbus_t *ctx) ctx->indication_timeout.tv_sec = 0; ctx->indication_timeout.tv_usec = 0; + + /* No pluggable transport by default; use the backend I/O path. */ + ctx->transport = NULL; } /* Define the slave number */ @@ -2170,6 +2220,13 @@ int modbus_connect(modbus_t *ctx) return -1; } + if (ctx->transport && ctx->transport->connect) { + int rc = ctx->transport->connect(ctx->transport); + if (rc == 0) { + ctx->transport->connected = 1; + } + return rc; + } return ctx->backend->connect(ctx); } @@ -2178,6 +2235,13 @@ void modbus_close(modbus_t *ctx) if (ctx == NULL) return; + if (ctx->transport) { + if (ctx->transport->close) { + ctx->transport->close(ctx->transport); + } + ctx->transport->connected = 0; + return; + } ctx->backend->close(ctx); } @@ -2186,6 +2250,11 @@ void modbus_free(modbus_t *ctx) if (ctx == NULL) return; + /* Free transport resources before the backend (which frees ctx itself) */ + if (ctx->transport && ctx->transport->free) { + ctx->transport->free(ctx->transport); + ctx->transport = NULL; + } ctx->backend->free(ctx); } @@ -2366,3 +2435,27 @@ size_t strlcpy(char *dest, const char *src, size_t dest_size) return (s - src - 1); /* count does not include NUL */ } #endif + +/* Pluggable transport API (see modbus-transport.h) */ + +int modbus_set_transport(modbus_t *ctx, modbus_transport_t *transport) +{ + if (ctx == NULL) { + errno = EINVAL; + return -1; + } + /* NULL detaches without freeing; the caller owns any previous transport */ + ctx->transport = transport; + if (transport) { + transport->connected = 0; + } + return 0; +} + +modbus_transport_t *modbus_get_transport(modbus_t *ctx) +{ + if (ctx == NULL) { + return NULL; + } + return ctx->transport; +} diff --git a/src/modbus.h b/src/modbus.h index 237d3e11f..ed7e3e961 100644 --- a/src/modbus.h +++ b/src/modbus.h @@ -344,6 +344,7 @@ MODBUS_API void modbus_set_float_cdab(float f, uint16_t *dest); #include "modbus-rtu.h" #include "modbus-tcp.h" +#include "modbus-transport.h" MODBUS_END_DECLS diff --git a/tests/Makefile.am b/tests/Makefile.am index 45d0f8e72..eb60c0ab4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -8,6 +8,7 @@ noinst_PROGRAMS = \ random-test-client \ unit-test-server \ unit-test-client \ + unit-test-transport \ proxy-test-server \ proxy-test-client \ version @@ -36,6 +37,9 @@ unit_test_server_LDADD = $(common_ldflags) unit_test_client_SOURCES = unit-test-client.c unit-test.h unit_test_client_LDADD = $(common_ldflags) +unit_test_transport_SOURCES = unit-test-transport.c +unit_test_transport_LDADD = $(common_ldflags) + proxy_test_server_SOURCES = proxy-test-server.c proxy_test_server_LDADD = $(common_ldflags) @@ -57,4 +61,4 @@ AM_CFLAGS = $(LIBMODBUSCFLAGS) $(WARNING_CFLAGS) CLEANFILES = *~ *.log noinst_SCRIPTS=unit-tests.sh -TESTS=./unit-tests.sh +TESTS=./unit-tests.sh unit-test-transport diff --git a/tests/unit-test-transport.c b/tests/unit-test-transport.c new file mode 100644 index 000000000..fa7b19186 --- /dev/null +++ b/tests/unit-test-transport.c @@ -0,0 +1,613 @@ +/* + * Copyright © Stéphane Raimbault + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* Regression test for the pluggable transport layer (modbus-transport.h). + * + * A socketpair provides a loopback: one end drives a Modbus TCP client through + * a custom transport, the other is served by a Modbus TCP server on the normal + * backend path (modbus_set_socket). The test checks the set/get round-trip, the + * connect/send/recv/select/close/free call sequence, that framing and CRC are + * unchanged, and that a context without a transport still behaves normally. + * + * The connection is identified by an opaque pointer handle (conn_t *) rather + * than an int fd, to exercise a transport whose connection object cannot be + * stored in the int-typed ctx->s. + * + * On Windows the round-trip test uses a 127.0.0.1 loopback pair and a server + * thread in place of socketpair() and fork(). */ + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +typedef int sock_t; +#define CLOSESOCK(s) close(s) +#else +#include +#include +#include +typedef SOCKET sock_t; +#define CLOSESOCK(s) closesocket(s) +#endif + +#include "modbus-transport.h" +#include "modbus.h" + +/* The socket is hidden inside a heap-allocated handle; the transport only ever + * sees the conn_t pointer stored in priv. */ +typedef struct conn { + sock_t sock; +} conn_t; + +typedef struct { + conn_t *conn; + int n_connect; + int n_send; + int n_recv; + int n_select; + int n_flush; + int n_close; + int n_free; +} test_priv_t; + +static int transport_connect(modbus_transport_t *t) +{ + ((test_priv_t *) t->priv)->n_connect++; + return 0; +} + +static ssize_t transport_send(modbus_transport_t *t, const uint8_t *buf, int len) +{ + test_priv_t *p = t->priv; + p->n_send++; + return send(p->conn->sock, (const char *) buf, len, 0); +} + +static ssize_t transport_recv(modbus_transport_t *t, uint8_t *buf, int len) +{ + test_priv_t *p = t->priv; + p->n_recv++; + /* Read exactly len bytes; a manual loop is used instead of MSG_WAITALL, + * whose stream support varies on Winsock. */ + int total = 0; + while (total < len) { + int rc = recv(p->conn->sock, (char *) buf + total, len - total, 0); + if (rc == 0) + return total; /* peer closed */ + if (rc < 0) + return -1; + total += rc; + } + return total; +} + +static int transport_select(modbus_transport_t *t, struct timeval *tv) +{ + test_priv_t *p = t->priv; + sock_t s = p->conn->sock; + p->n_select++; + + fd_set rset; + FD_ZERO(&rset); + FD_SET(s, &rset); + return select((int) (s + 1), &rset, NULL, NULL, tv); +} + +static void drain_nonblocking(sock_t fd) +{ + uint8_t discard[256]; +#ifndef _WIN32 + while (recv(fd, (char *) discard, sizeof(discard), MSG_DONTWAIT) > 0) + ; +#else + u_long nb = 1; + ioctlsocket(fd, FIONBIO, &nb); + while (recv(fd, (char *) discard, sizeof(discard), 0) > 0) + ; + nb = 0; + ioctlsocket(fd, FIONBIO, &nb); +#endif +} + +static int transport_flush(modbus_transport_t *t) +{ + test_priv_t *p = t->priv; + p->n_flush++; + drain_nonblocking(p->conn->sock); + return 0; +} + +static void transport_close(modbus_transport_t *t) +{ + test_priv_t *p = t->priv; + p->n_close++; + /* close() tears down the connection but must not free the handle */ + CLOSESOCK(p->conn->sock); + p->conn->sock = (sock_t) -1; +} + +static void transport_free(modbus_transport_t *t) +{ + test_priv_t *p = t->priv; + p->n_free++; + /* free() is the destructor and releases the heap-allocated handle */ + free(p->conn); + p->conn = NULL; +} + +/* Two connected stream sockets. On POSIX this is socketpair(); on Windows it is + * emulated with a 127.0.0.1 loopback listener, connect and accept. Returns 0 on + * success, -1 on failure. */ +static int make_socketpair(sock_t fds[2]) +{ +#ifndef _WIN32 + return socketpair(AF_UNIX, SOCK_STREAM, 0, fds); +#else + struct sockaddr_in addr; + int addrlen = sizeof(addr); + SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == INVALID_SOCKET) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; /* let the OS pick a free port */ + + if (bind(listener, (struct sockaddr *) &addr, sizeof(addr)) != 0 || + listen(listener, 1) != 0 || + getsockname(listener, (struct sockaddr *) &addr, &addrlen) != 0) { + closesocket(listener); + return -1; + } + + SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (client == INVALID_SOCKET) { + closesocket(listener); + return -1; + } + if (connect(client, (struct sockaddr *) &addr, addrlen) != 0) { + closesocket(listener); + closesocket(client); + return -1; + } + SOCKET server = accept(listener, NULL, NULL); + closesocket(listener); + if (server == INVALID_SOCKET) { + closesocket(client); + return -1; + } + fds[0] = client; + fds[1] = server; + return 0; +#endif +} + +static void check(int rc, const char *label) +{ + if (rc == -1) { + fprintf(stderr, "FAIL %s: %s\n", label, modbus_strerror(errno)); + exit(EXIT_FAILURE); + } +} + +/* Service one request against the mapping, then return. Runs in a child process + * (POSIX) or a thread (Windows). */ +typedef struct { + modbus_t *srv; + modbus_mapping_t *map; +} server_arg_t; + +static void server_serve_one(server_arg_t *a) +{ + uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH]; + modbus_set_debug(a->srv, FALSE); + int rc = modbus_receive(a->srv, req); + if (rc > 0) + modbus_reply(a->srv, req, rc, a->map); +} + +#ifdef _WIN32 +static unsigned __stdcall server_thread(void *arg) +{ + server_serve_one((server_arg_t *) arg); + return 0; +} +#endif + +/* Test 1: round-trip read over a custom transport. */ +static void test_transport_read_registers(void) +{ + printf("[1] transport read_registers... "); + fflush(stdout); + + sock_t fds[2]; + assert(make_socketpair(fds) == 0); + + /* Server side: default TCP backend with the socket injected. */ + modbus_t *srv = modbus_new_tcp("127.0.0.1", 1502); + assert(srv); + modbus_set_socket(srv, (int) fds[1]); + + modbus_mapping_t *map = modbus_mapping_new(0, 0, 10, 0); + assert(map); + for (int i = 0; i < 10; i++) + map->tab_registers[i] = (uint16_t) (100 + i); + + /* Client side: custom transport identified by a pointer handle. */ + conn_t *conn = malloc(sizeof(*conn)); + assert(conn); + conn->sock = fds[0]; + test_priv_t priv = {.conn = conn}; + modbus_transport_t tr = { + .connect = transport_connect, + .send = transport_send, + .recv = transport_recv, + .select = transport_select, + .flush = transport_flush, + .close = transport_close, + .free = transport_free, + .priv = &priv, + }; + + modbus_t *cli = modbus_new_tcp("127.0.0.1", 1502); + assert(cli); + assert(modbus_get_transport(cli) == NULL); + + check(modbus_set_transport(cli, &tr), "set_transport"); + assert(modbus_get_transport(cli) == &tr); + + check(modbus_connect(cli), "connect"); + assert(priv.n_connect == 1); + assert(tr.connected == 1); + modbus_set_slave(cli, MODBUS_TCP_SLAVE); + + uint16_t out[10]; + server_arg_t sarg = {.srv = srv, .map = map}; + + /* The socketpair is full-duplex but the exchange is synchronous (the client + * blocks for the reply), so the server runs in parallel. */ +#ifndef _WIN32 + pid_t pid = fork(); + if (pid == 0) { + /* Child: server. */ + CLOSESOCK(fds[0]); + server_serve_one(&sarg); + modbus_mapping_free(map); + modbus_free(srv); + _exit(0); + } + /* Parent: client. */ + CLOSESOCK(fds[1]); +#else + /* Windows: the server runs in a thread, so both sockets stay open and each + * side touches only its own. */ + HANDLE th = (HANDLE) _beginthreadex(NULL, 0, server_thread, &sarg, 0, NULL); + assert(th != NULL); +#endif + + modbus_set_debug(cli, FALSE); + int rc = modbus_read_registers(cli, 0, 10, out); + check(rc, "read_registers"); + assert(rc == 10); + for (int i = 0; i < 10; i++) + assert(out[i] == (uint16_t) (100 + i)); + + assert(priv.n_connect >= 1); + assert(priv.n_send >= 1); + assert(priv.n_recv >= 1); + assert(priv.n_select >= 1); + + /* modbus_flush() must reach the transport, not the backend. */ + assert(modbus_flush(cli) == 0); + assert(priv.n_flush == 1); + + modbus_close(cli); + assert(priv.n_close == 1); + assert(tr.connected == 0); + + modbus_free(cli); /* calls tr.free */ + assert(priv.n_free == 1); + +#ifndef _WIN32 + int status; + waitpid(pid, &status, 0); +#else + WaitForSingleObject(th, INFINITE); + CloseHandle(th); + modbus_mapping_free(map); + modbus_free(srv); +#endif + printf("PASS\n"); +} + +/* Test 2: modbus_set_transport(ctx, NULL) detaches without calling free(). */ +static void test_transport_detach(void) +{ + printf("[2] transport detach (NULL)... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + + test_priv_t priv = {.conn = NULL}; + modbus_transport_t tr = {.priv = &priv}; + + modbus_set_transport(ctx, &tr); + assert(modbus_get_transport(ctx) == &tr); + + modbus_set_transport(ctx, NULL); + assert(modbus_get_transport(ctx) == NULL); + assert(priv.n_free == 0); + + modbus_free(ctx); + assert(priv.n_free == 0); /* ctx no longer owns tr */ + + printf("PASS\n"); +} + +/* Test 3: a context without a transport still works. */ +static void test_no_transport(void) +{ + printf("[3] backwards compat (no transport)... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + assert(modbus_get_transport(ctx) == NULL); + modbus_free(ctx); + + printf("PASS\n"); +} + +/* Test 4: a partial transport (no select) leaves the rest to the backend. */ +static void test_partial_override(void) +{ + printf("[4] partial override (NULL select falls back)... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + + test_priv_t priv = {.conn = NULL}; + modbus_transport_t tr = { + /* select left NULL on purpose */ + .send = transport_send, + .recv = transport_recv, + .close = transport_close, + .priv = &priv, + }; + + modbus_set_transport(ctx, &tr); + assert(modbus_get_transport(ctx) == &tr); + + modbus_free(ctx); + printf("PASS\n"); +} + +/* Test 5: the public setter and getter reject a NULL context, and attaching a + * transport resets its connection state. */ +static void test_api_validation(void) +{ + printf("[5] set/get_transport argument validation... "); + fflush(stdout); + + test_priv_t priv = {.conn = NULL}; + modbus_transport_t tr = {.priv = &priv, .connected = 1}; + + errno = 0; + assert(modbus_set_transport(NULL, &tr) == -1 && errno == EINVAL); + assert(modbus_get_transport(NULL) == NULL); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + assert(modbus_set_transport(ctx, &tr) == 0); + assert(tr.connected == 0); + + modbus_set_transport(ctx, NULL); + modbus_free(ctx); + + printf("PASS\n"); +} + +/* Test 6: a transport that has not been connected fails the receive path with + * EBADF instead of falling through to ctx->s. */ +static void test_receive_not_connected(void) +{ + printf("[6] receive on unconnected transport... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + + test_priv_t priv = {.conn = NULL}; + modbus_transport_t tr = { + .send = transport_send, + .recv = transport_recv, + .select = transport_select, + .priv = &priv, + }; + assert(modbus_set_transport(ctx, &tr) == 0); + assert(tr.connected == 0); + + uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH]; + errno = 0; + assert(modbus_receive(ctx, req) == -1 && errno == EBADF); + assert(priv.n_recv == 0); /* the transport was never asked to read */ + + modbus_set_transport(ctx, NULL); + modbus_free(ctx); + + printf("PASS\n"); +} + +/* A transport with no link behind it, for the error paths: send() can fail + * once, select() always times out, and every call is recorded in order. */ +typedef struct { + int first_send_errno; /* errno for the first send(), 0 to succeed */ + int n_send; + char trace[256]; +} fake_priv_t; + +static void fake_trace(fake_priv_t *p, const char *event) +{ + if (p->trace[0] != '\0') + strcat(p->trace, " "); + strcat(p->trace, event); +} + +static int fake_connect(modbus_transport_t *t) +{ + fake_trace(t->priv, "connect"); + return 0; +} + +static ssize_t fake_send(modbus_transport_t *t, const uint8_t *buf, int len) +{ + fake_priv_t *p = t->priv; + + (void) buf; + if (p->n_send++ == 0 && p->first_send_errno != 0) { + fake_trace(p, "send-fail"); + errno = p->first_send_errno; + return -1; + } + fake_trace(p, "send"); + return len; +} + +static ssize_t fake_recv(modbus_transport_t *t, uint8_t *buf, int len) +{ + (void) buf; + (void) len; + fake_trace(t->priv, "recv"); + errno = ECONNRESET; + return -1; +} + +static int fake_select(modbus_transport_t *t, struct timeval *tv) +{ + (void) tv; + fake_trace(t->priv, "select-timeout"); + return 0; /* the timeout expired, as select(2) reports it */ +} + +static int fake_flush(modbus_transport_t *t) +{ + fake_trace(t->priv, "flush"); + return 0; +} + +static void fake_close(modbus_transport_t *t) +{ + fake_trace(t->priv, "close"); +} + +static modbus_transport_t fake_transport(fake_priv_t *priv) +{ + modbus_transport_t tr = { + .connect = fake_connect, + .send = fake_send, + .recv = fake_recv, + .select = fake_select, + .flush = fake_flush, + .close = fake_close, + .priv = priv, + }; + return tr; +} + +/* Test 7: select() returning 0 is a timeout. The request fails with ETIMEDOUT + * and recv() is never called on an empty link. */ +static void test_select_timeout(void) +{ + printf("[7] select() timeout... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + + fake_priv_t priv = {0}; + modbus_transport_t tr = fake_transport(&priv); + assert(modbus_set_transport(ctx, &tr) == 0); + assert(modbus_connect(ctx) == 0); + + uint16_t reg; + errno = 0; + assert(modbus_read_registers(ctx, 0, 1, ®) == -1 && errno == ETIMEDOUT); + assert(strcmp(priv.trace, "connect send select-timeout") == 0); + + modbus_set_transport(ctx, NULL); + modbus_free(ctx); + + printf("PASS\n"); +} + +/* Test 8: with MODBUS_ERROR_RECOVERY_LINK, a send() failing with ECONNRESET + * closes and reconnects the transport before the retry. The decision comes from + * errno on every platform, not from Winsock, which a transport never sets. */ +static void test_link_recovery(void) +{ + printf("[8] link recovery reconnects... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 502); + assert(ctx); + + fake_priv_t priv = {0}; + priv.first_send_errno = ECONNRESET; + modbus_transport_t tr = fake_transport(&priv); + assert(modbus_set_transport(ctx, &tr) == 0); + modbus_set_error_recovery(ctx, MODBUS_ERROR_RECOVERY_LINK); + /* recovery sleeps for the response timeout */ + modbus_set_response_timeout(ctx, 0, 1000); + assert(modbus_connect(ctx) == 0); + + uint16_t reg; + errno = 0; + assert(modbus_read_registers(ctx, 0, 1, ®) == -1 && errno == ETIMEDOUT); + /* reconnect after the failed send, flush after the timeout */ + assert(strcmp(priv.trace, + "connect send-fail close connect send select-timeout flush") == 0); + + modbus_set_transport(ctx, NULL); + modbus_free(ctx); + + printf("PASS\n"); +} + +int main(void) +{ +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + + printf("=== modbus pluggable transport tests ===\n"); + + test_transport_read_registers(); + test_transport_detach(); + test_no_transport(); + test_partial_override(); + test_api_validation(); + test_receive_not_connected(); + test_select_timeout(); + test_link_recovery(); + + printf("All tests passed.\n"); + +#ifdef _WIN32 + WSACleanup(); +#endif + return EXIT_SUCCESS; +}