can-utils/isobusfs/isobusfs_cmn.c

701 lines
18 KiB
C

// SPDX-License-Identifier: LGPL-2.0-only
// SPDX-FileCopyrightText: 2023 Oleksij Rempel <linux@rempel-privat.de>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <linux/errqueue.h>
#include <linux/net_tstamp.h>
#include <linux/netlink.h>
#include "isobusfs_cmn.h"
#include "isobusfs_srv.h"
static log_level_t log_level = LOG_LEVEL_INFO;
static bool interactive_mode;
#define LOG_BUFFER_SIZE 1024
#define LOG_ENTRY_MAX_SIZE 256
struct isobusfs_log_buffer {
char buffer[LOG_BUFFER_SIZE][LOG_ENTRY_MAX_SIZE];
int write_index;
};
struct isobusfs_log_buffer log_buffer = { .write_index = 0 };
void add_log_to_buffer(const char *log_entry)
{
int idx = log_buffer.write_index;
char *buffer = log_buffer.buffer[idx];
strncpy(buffer, log_entry, LOG_ENTRY_MAX_SIZE);
buffer[LOG_ENTRY_MAX_SIZE - 1] = '\0'; /* Ensure null termination */
log_buffer.write_index = (idx + 1) % LOG_BUFFER_SIZE;
}
void isobusfs_print_log_buffer(void)
{
printf("\n---- Log Buffer Start ----\n");
for (int i = 0; i < LOG_BUFFER_SIZE; i++) {
int idx = (log_buffer.write_index + i) % LOG_BUFFER_SIZE;
if (log_buffer.buffer[idx][0] != '\0')
printf("%s\n", log_buffer.buffer[idx]);
}
printf("\n---- Log Buffer End ----\n");
}
void isobusfs_log(log_level_t level, const char *fmt, ...)
{
char complete_log_entry[LOG_ENTRY_MAX_SIZE];
char log_entry[LOG_ENTRY_MAX_SIZE - 64];
const char *level_str;
struct timeval tv;
struct tm *time_info;
char time_buffer[64];
int milliseconds;
va_list args;
if (level > log_level)
return;
switch (level) {
case LOG_LEVEL_DEBUG:
level_str = "DEBUG";
break;
case LOG_LEVEL_INT:
case LOG_LEVEL_INFO:
level_str = "INFO";
break;
case LOG_LEVEL_WARN:
level_str = "WARNING";
break;
case LOG_LEVEL_ERROR:
level_str = "ERROR";
break;
default:
level_str = "UNKNOWN";
break;
}
gettimeofday(&tv, NULL);
time_info = localtime(&tv.tv_sec);
milliseconds = tv.tv_usec / 1000;
strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S",
time_info);
snprintf(time_buffer + strlen(time_buffer),
sizeof(time_buffer) - strlen(time_buffer),
".%03d", milliseconds);
va_start(args, fmt);
vsnprintf(log_entry, sizeof(log_entry), fmt, args);
va_end(args);
snprintf(complete_log_entry, sizeof(complete_log_entry),
"[%.40s] [%.10s]: %.150s", time_buffer, level_str, log_entry);
if (interactive_mode) {
add_log_to_buffer(complete_log_entry);
if (level == LOG_LEVEL_INT) {
fprintf(stdout, "%s", log_entry);
fflush(stdout);
}
} else {
fprintf(stdout, "%s\n", complete_log_entry);
}
}
void isobusfs_set_interactive(bool interactive)
{
interactive_mode = interactive;
}
/* set log level */
void isobusfs_log_level_set(log_level_t level)
{
log_level = level;
}
const char *isobusfs_error_to_str(enum isobusfs_error err)
{
switch (err) {
case ISOBUSFS_ERR_ACCESS_DENIED:
return "Access Denied";
case ISOBUSFS_ERR_INVALID_ACCESS:
return "Invalid Access";
case ISOBUSFS_ERR_TOO_MANY_FILES_OPEN:
return "Too many files open";
case ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND:
return "File or path not found";
case ISOBUSFS_ERR_INVALID_HANDLE:
return "Invalid handle";
case ISOBUSFS_ERR_INVALID_SRC_NAME:
return "Invalid given source name";
case ISOBUSFS_ERR_INVALID_DST_NAME:
return "Invalid given destination name";
case ISOBUSFS_ERR_NO_SPACE:
return "Volume out of free space";
case ISOBUSFS_ERR_ON_WRITE:
return "Failure during a write operation";
case ISOBUSFS_ERR_MEDIA_IS_NOT_PRESENT:
return "Media is not present";
case ISOBUSFS_ERR_VOLUME_NOT_INITIALIZED:
return "Volume is possibly not initialized";
case ISOBUSFS_ERR_ON_READ:
return "Failure during a read operation";
case ISOBUSFS_ERR_FUNC_NOT_SUPPORTED:
return "Function not supported";
case ISOBUSFS_ERR_INVALID_REQUESTED_LENGHT:
return "Invalid request length";
case ISOBUSFS_ERR_OUT_OF_MEM:
return "Out of memory";
case ISOBUSFS_ERR_OTHER:
return "Any other error";
case ISOBUSFS_ERR_END_OF_FILE:
return "End of file reached, will only be reported when file pointer is at end of file";
default:
return "<unknown>";
}
}
enum isobusfs_error linux_error_to_isobusfs_error(int linux_err)
{
switch (linux_err) {
case 0:
return ISOBUSFS_ERR_SUCCESS;
case -EINVAL:
return ISOBUSFS_ERR_INVALID_DST_NAME;
case -EACCES:
return ISOBUSFS_ERR_ACCESS_DENIED;
case -ENOTDIR:
return ISOBUSFS_ERR_INVALID_ACCESS;
case -EMFILE:
return ISOBUSFS_ERR_TOO_MANY_FILES_OPEN;
case -ENOENT:
return ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND;
case -EBADF:
return ISOBUSFS_ERR_INVALID_HANDLE;
case -ENAMETOOLONG:
return ISOBUSFS_ERR_INVALID_SRC_NAME;
case -ENOSPC:
return ISOBUSFS_ERR_NO_SPACE;
case -EIO:
return ISOBUSFS_ERR_ON_WRITE;
case -ENODEV:
return ISOBUSFS_ERR_MEDIA_IS_NOT_PRESENT;
case -EROFS:
return ISOBUSFS_ERR_VOLUME_NOT_INITIALIZED;
case -EFAULT:
return ISOBUSFS_ERR_ON_READ;
case -ENOSYS:
return ISOBUSFS_ERR_FUNC_NOT_SUPPORTED;
case -EMSGSIZE:
return ISOBUSFS_ERR_INVALID_REQUESTED_LENGHT;
case -ENOMEM:
return ISOBUSFS_ERR_OUT_OF_MEM;
case -EPERM:
return ISOBUSFS_ERR_OTHER;
case -ESPIPE:
return ISOBUSFS_ERR_END_OF_FILE;
case -EPROTO:
return ISOBUSFS_ERR_TAN_ERR;
case -EILSEQ:
return ISOBUSFS_ERR_MALFORMED_REQUEST;
default:
return ISOBUSFS_ERR_OTHER;
}
}
static void isobusfs_print_timestamp(struct isobusfs_err_msg *emsg,
const char *name, struct timespec *cur)
{
struct isobusfs_stats *stats = emsg->stats;
/* TODO: make it configurable */
return;
if (!(cur->tv_sec | cur->tv_nsec))
return;
fprintf(stderr, " %s: %llu s %llu us (seq=%u/%u, send=%u)",
name, (unsigned long long)cur->tv_sec, (unsigned long long)cur->tv_nsec / 1000,
stats->tskey_sch, stats->tskey_ack, stats->send);
fprintf(stderr, "\n");
}
static const char *isobusfs_tstype_to_str(int tstype)
{
switch (tstype) {
case SCM_TSTAMP_SCHED:
return " ENQ";
case SCM_TSTAMP_SND:
return " SND";
case SCM_TSTAMP_ACK:
return " ACK";
default:
return " unk";
}
}
/* Check the stats of SCM_TIMESTAMPING_OPT_STATS */
static void isobusfs_scm_opt_stats(struct isobusfs_err_msg *emsg, void *buf, int len)
{
struct isobusfs_stats *stats = emsg->stats;
int offset = 0;
while (offset < len) {
struct nlattr *nla = (struct nlattr *) ((char *)buf + offset);
switch (nla->nla_type) {
case J1939_NLA_BYTES_ACKED:
stats->send = *(uint32_t *)((char *)nla + NLA_HDRLEN);
break;
default:
warnx("not supported J1939_NLA field\n");
}
offset += NLA_ALIGN(nla->nla_len);
}
}
static int isobusfs_extract_serr(struct isobusfs_err_msg *emsg)
{
struct isobusfs_stats *stats = emsg->stats;
struct sock_extended_err *serr = emsg->serr;
struct scm_timestamping *tss = emsg->tss;
switch (serr->ee_origin) {
case SO_EE_ORIGIN_TIMESTAMPING:
/*
* We expect here following patterns:
* serr->ee_info == SCM_TSTAMP_ACK
* Activated with SOF_TIMESTAMPING_TX_ACK
* or
* serr->ee_info == SCM_TSTAMP_SCHED
* Activated with SOF_TIMESTAMPING_SCHED
* and
* serr->ee_data == tskey
* session message counter which is activate
* with SOF_TIMESTAMPING_OPT_ID
* the serr->ee_errno should be ENOMSG
*/
if (serr->ee_errno != ENOMSG)
warnx("serr: expected ENOMSG, got: %i",
serr->ee_errno);
if (serr->ee_info == SCM_TSTAMP_SCHED)
stats->tskey_sch = serr->ee_data;
else
stats->tskey_ack = serr->ee_data;
isobusfs_print_timestamp(emsg, isobusfs_tstype_to_str(serr->ee_info),
&tss->ts[0]);
if (serr->ee_info == SCM_TSTAMP_SCHED)
return -EINTR;
else
return 0;
case SO_EE_ORIGIN_LOCAL:
/*
* The serr->ee_origin == SO_EE_ORIGIN_LOCAL is
* currently used to notify about locally
* detected protocol/stack errors.
* Following patterns are expected:
* serr->ee_info == J1939_EE_INFO_TX_ABORT
* is used to notify about session TX
* abort.
* serr->ee_data == tskey
* session message counter which is activate
* with SOF_TIMESTAMPING_OPT_ID
* serr->ee_errno == actual error reason
* error reason is converted from J1939
* abort to linux error name space.
*/
if (serr->ee_info != J1939_EE_INFO_TX_ABORT)
warnx("serr: unknown ee_info: %i",
serr->ee_info);
isobusfs_print_timestamp(emsg, " ABT", &tss->ts[0]);
warnx("serr: tx error: %i, %s", serr->ee_errno, strerror(serr->ee_errno));
return serr->ee_errno;
default:
warnx("serr: wrong origin: %u", serr->ee_origin);
}
return 0;
}
static int isobusfs_parse_cm(struct isobusfs_err_msg *emsg,
struct cmsghdr *cm)
{
const size_t hdr_len = CMSG_ALIGN(sizeof(struct cmsghdr));
if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_TIMESTAMPING) {
emsg->tss = (void *)CMSG_DATA(cm);
} else if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_TIMESTAMPING_OPT_STATS) {
void *jstats = (void *)CMSG_DATA(cm);
/* Activated with SOF_TIMESTAMPING_OPT_STATS */
isobusfs_scm_opt_stats(emsg, jstats, cm->cmsg_len - hdr_len);
} else if (cm->cmsg_level == SOL_CAN_J1939 &&
cm->cmsg_type == SCM_J1939_ERRQUEUE) {
emsg->serr = (void *)CMSG_DATA(cm);
} else
warnx("serr: not supported type: %d.%d",
cm->cmsg_level, cm->cmsg_type);
return 0;
}
int isobusfs_recv_err(int sock, struct isobusfs_err_msg *emsg)
{
char control[200];
struct cmsghdr *cm;
int ret;
struct msghdr msg = {
.msg_control = control,
.msg_controllen = sizeof(control),
};
ret = recvmsg(sock, &msg, MSG_ERRQUEUE | MSG_DONTWAIT);
if (ret == -1) {
ret = -errno;
pr_err("recvmsg error notification: %i (%s)", ret, strerror(ret));
return ret;
}
if (msg.msg_flags & MSG_CTRUNC) {
pr_err("recvmsg error notification: truncated");
return -EINVAL;
}
emsg->serr = NULL;
emsg->tss = NULL;
for (cm = CMSG_FIRSTHDR(&msg); cm && cm->cmsg_len;
cm = CMSG_NXTHDR(&msg, cm)) {
isobusfs_parse_cm(emsg, cm);
if (emsg->serr && emsg->tss)
return isobusfs_extract_serr(emsg);
}
return 0;
}
/* send NACK message. function should use src, dst and socket as parameters */
void isobusfs_send_nack(int sock, struct isobusfs_msg *msg)
{
struct sockaddr_can addr = msg->peername;
struct isobusfs_nack nack;
int ret;
nack.ctrl = ISOBUS_ACK_CTRL_NACK;
nack.group_function = msg->buf[0];
memset(&nack.reserved[0], 0xff, sizeof(nack.reserved));
nack.address_nack = addr.can_addr.j1939.addr;
memcpy(&nack.pgn_nack[0], &addr.can_addr.j1939.pgn,
sizeof(nack.pgn_nack));
addr.can_addr.j1939.pgn = ISOBUS_PGN_ACK;
ret = sendto(sock, &nack, sizeof(nack), MSG_DONTWAIT,
(struct sockaddr *)&addr,
sizeof(addr));
if (ret < 0) {
ret = -errno;
pr_warn("failed to send NACK: %i (%s)", ret, strerror(ret));
}
pr_debug("send NACK");
}
/* store data to a recursive buffer */
void isobufs_store_tx_data(struct isobusfs_buf_log *buffer, uint8_t *data)
{
struct isobusfs_buf *entry = &buffer->entries[buffer->index];
/* we assume :) that data is at least 8 bytes long */
memcpy(entry->data, data, sizeof(entry->data));
clock_gettime(CLOCK_REALTIME, &entry->ts);
buffer->index = (buffer->index + 1) % ISOBUSFS_MAX_BUF_ENTRIES;
}
void isobusfs_dump_tx_data(const struct isobusfs_buf_log *buffer)
{
uint i;
for (i = 0; i < ISOBUSFS_MAX_BUF_ENTRIES; ++i) {
const struct isobusfs_buf *entry = &buffer->entries[i];
char data_str[ISOBUSFS_MIN_TRANSFER_LENGH * 3] = {0};
uint j;
for (j = 0; j < ISOBUSFS_MIN_TRANSFER_LENGH; ++j)
snprintf(data_str + j * 3, 4, "%02X ", entry->data[j]);
pr_debug("Entry %u: %s Timestamp: %ld.%09ld\n", i, data_str,
entry->ts.tv_sec, entry->ts.tv_nsec);
}
}
/* wrapper for sendto() */
int isobusfs_sendto(int sock, const void *data, size_t len,
const struct sockaddr_can *addr,
struct isobusfs_buf_log *isobusfs_tx_buffer)
{
int ret;
/* store to tx buffer */
isobufs_store_tx_data(isobusfs_tx_buffer, (uint8_t *)data);
ret = sendto(sock, data, len, MSG_DONTWAIT,
(struct sockaddr *)addr, sizeof(*addr));
if (ret == -1) {
ret = -errno;
pr_warn("failed to send data: %i (%s)", ret, strerror(ret));
return ret;
}
return 0;
}
/* wrapper for send() */
int isobusfs_send(int sock, const void *data, size_t len,
struct isobusfs_buf_log *isobusfs_tx_buffer)
{
int ret;
/* store to tx buffer */
isobufs_store_tx_data(isobusfs_tx_buffer, (uint8_t *)data);
ret = send(sock, data, len, MSG_DONTWAIT);
if (ret == -1) {
ret = -errno;
pr_warn("failed to send data: %i (%s)", ret, strerror(ret));
return ret;
}
return 0;
}
/**
* isobusfs_cmn_configure_socket_filter - Configure a J1939 socket filter
* @sock: Socket file descriptor
* @pgn: Parameter Group Number to filter
*
* This function configures a J1939 socket filter for the provided PGN.
* It allows ISOBUS FS role-specific PGN and ACK messages for troubleshooting.
* Returns 0 on success or a negative error code on failure.
*/
int isobusfs_cmn_configure_socket_filter(int sock, pgn_t pgn)
{
struct j1939_filter sock_filter[2] = {0};
int ret;
if (pgn != ISOBUSFS_PGN_CL_TO_FS && pgn != ISOBUSFS_PGN_FS_TO_CL) {
pr_err("invalid pgn: %d", pgn);
return -EINVAL;
}
/* Allow ISOBUS FS role specific PGN */
sock_filter[0].pgn = pgn;
sock_filter[0].pgn_mask = J1939_PGN_PDU1_MAX;
/*
* ISO 11783-3:2018 - 5.4.5 Acknowledgment.
* Allow ACK messages for troubleshooting
*/
sock_filter[1].pgn = ISOBUS_PGN_ACK;
sock_filter[1].pgn_mask = J1939_PGN_PDU1_MAX;
ret = setsockopt(sock, SOL_CAN_J1939, SO_J1939_FILTER, &sock_filter,
sizeof(sock_filter));
if (ret < 0) {
ret = -errno;
pr_err("failed to set j1939 filter: %d (%s)", ret, strerror(ret));
return ret;
}
return 0;
}
/**
* isobusfs_cmn_configure_timestamping - Configure timestamping options for a
* socket
* @sock: Socket file descriptor
*
* This function configures various timestamping options for the given socket,
* such as software timestamping, CMSG timestamping, transmission
* acknowledgment, transmission scheduling, statistics, and timestamp-only
* options. These options are needed to get a response from different kernel
* j1939 stack layers about egress status, allowing the caller to know if the
* ETP session has finished or if status messages have actually been sent.
* These options make sense only in combination with SO_J1939_ERRQUEUE. Returns
* 0 on success or a negative error code on failure.
*/
static int isobusfs_cmn_configure_timestamping(int sock)
{
unsigned int sock_opt;
int ret;
sock_opt = SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_OPT_CMSG |
SOF_TIMESTAMPING_TX_ACK | SOF_TIMESTAMPING_TX_SCHED |
SOF_TIMESTAMPING_OPT_STATS | SOF_TIMESTAMPING_OPT_TSONLY |
SOF_TIMESTAMPING_OPT_ID;
ret = setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING,
(char *)&sock_opt, sizeof(sock_opt));
if (ret < 0) {
ret = -errno;
pr_err("setsockopt timestamping: %d (%s)", ret, strerror(ret));
return ret;
}
return 0;
}
/**
* isobusfs_cmn_configure_error_queue - Configure error queue for a J1939 socket
* @sock: socket file descriptor
*
* This function configures the error queue for a given J1939 socket, enabling
* timestamping options and the error queue itself. SO_J1939_ERRQUEUE enables
* the actual feedback channel from the kernel J1939 stack. Timestamping options
* are configured to subscribe the socket to different notifications over this
* channel, providing information on egress status. This helps in determining if
* an ETP session has finished or if status messages were actually sent.
*
* Return: 0 on success, or a negative error code on failure.
*/
int isobusfs_cmn_configure_error_queue(int sock)
{
int err_queue = true;
int ret;
ret = setsockopt(sock, SOL_CAN_J1939, SO_J1939_ERRQUEUE,
&err_queue, sizeof(err_queue));
if (ret < 0) {
ret = -errno;
pr_err("set recverr: %d (%s)", ret, strerror(ret));
return ret;
}
ret = isobusfs_cmn_configure_timestamping(sock);
if (ret < 0)
return ret;
return 0;
}
int isobusfs_cmn_connect_socket(int sock, struct sockaddr_can *addr)
{
int ret;
ret = connect(sock, (void *)addr, sizeof(*addr));
if (ret < 0) {
ret = -errno;
pr_err("failed to connect socket: %d (%s)", ret, strerror(ret));
return ret;
}
return 0;
}
/* FIXME: linger is currently not supported by the kernel J1939 stack
* but it would be nice to have it. Especially if we wont to stop sending
* messages on a socket when the connection is lost.
*/
int isobusfs_cmn_set_linger(int sock)
{
struct linger linger_opt;
int ret;
linger_opt.l_onoff = 1;
linger_opt.l_linger = 0;
ret = setsockopt(sock, SOL_SOCKET, SO_LINGER, &linger_opt,
sizeof(linger_opt));
if (ret < 0) {
ret = -errno;
pr_err("setsockopt(SO_LINGER): %d (%s)", ret, strerror(ret));
return ret;
}
return 0;
}
void isobusfs_cmn_dump_last_x_bytes(const uint8_t *buffer, size_t buffer_size,
size_t x)
{
size_t start_offset = 0;
char *output_ptr;
unsigned char c;
int remaining;
char output[80];
int n, j;
if (x > 0 && x < buffer_size)
start_offset = (buffer_size - x) & ~0x7;
for (size_t i = start_offset; i < buffer_size; i += 8) {
output_ptr = output;
remaining = (int)sizeof(output);
n = snprintf(output_ptr, remaining, "%08lx: ",
(unsigned long)(start_offset + i));
if (n < 0 || n >= remaining)
break;
output_ptr += n;
remaining -= n;
for (j = 0; j < 8 && i + j < buffer_size; ++j) {
n = snprintf(output_ptr, remaining, "%02x ", buffer[i+j]);
if (n < 0 || n >= remaining)
break;
output_ptr += n;
remaining -= n;
}
for (j = buffer_size - i; j < 8; ++j) {
n = snprintf(output_ptr, remaining, " ");
if (n < 0 || n >= remaining)
break;
output_ptr += n;
remaining -= n;
}
n = snprintf(output_ptr, remaining, " ");
if (n < 0 || n >= remaining)
break;
output_ptr += n;
remaining -= n;
for (j = 0; j < 8 && i + j < buffer_size; ++j) {
c = buffer[i+j];
n = snprintf(output_ptr, remaining, "%c",
isprint(c) ? c : '.');
if (n < 0 || n >= remaining)
break;
output_ptr += n;
remaining -= n;
}
pr_debug("%s", output);
if (n < 0 || n >= remaining)
break;
}
}