From b565f0a09301736e3c49ce909bb5c005eb28bd1e Mon Sep 17 00:00:00 2001 From: Robert Heaton Date: Sat, 13 Sep 2025 00:31:42 +0100 Subject: [PATCH] Added logging library and udp2file. Updated README to reflect. --- Logging/logging_lib.c | 321 ++++++++++++++++++++++++++++++ Logging/logging_lib.h | 118 +++++++++++ Networking/udp2file.c | 443 ++++++++++++++++++++++++++++++++++++++++++ README.md | 12 +- 4 files changed, 893 insertions(+), 1 deletion(-) create mode 100644 Logging/logging_lib.c create mode 100644 Logging/logging_lib.h create mode 100644 Networking/udp2file.c diff --git a/Logging/logging_lib.c b/Logging/logging_lib.c new file mode 100644 index 0000000..ca43fad --- /dev/null +++ b/Logging/logging_lib.c @@ -0,0 +1,321 @@ +//*============================================================================*/ +// Title: logging.c +// Author: RSH +// Description: +// General purpose logging function with timestamping and choice of output +// streams: stout, stderr, or a supplied file (opened in append mode). +// TODO: +// * Have a logging receive server +// * provide iso compliant option +// * test output options +// * useful print/formatting outputs (lines, tabs, etc.) +//TODO: would be nice if logger auto chunked at 80 chars +/*============================================================================*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "logging_lib.h" + + +// static const int tsBufLen = strlen("2025-01-01T01:50:01.234567890Z") + 1 ; +// static const int tsTmpLen = strlen("2025-01-01T01:50:01.") + 1 ; + +// #define LOG_MSG getTimestamp(tsBuf); snprintf(msgBuffer, tsBufLen+2, "%s: ", tsBuf); sprintf(msgBuffer + tsBufLen +1, +// #define RP ) + +// static int getTimestamp(char * tsBuf) +// { +// struct timespec now; +// struct tm tm; +// int retval = clock_gettime(CLOCK_REALTIME, &now); +// gmtime_r(&now.tv_sec, &tm); +// strftime(tsBuf, tsTmpLen, "%Y-%m-%dT%H:%M:%S.", &tm); +// sprintf(tsBuf + tsTmpLen -1, "%09luZ", now.tv_nsec); +// return retval; +// } + + +// Initialise buffers for the different severity strings +static char logDbg[] = DEBUG_PREFIX; +static char logInfo[] = INFO_PREFIX; +static char logWarn[] = WARN_PREFIX; +static char logErr[] = ERR_PREFIX; + +static char logInfoColour[] = COLOUR_INFO_PREFIX; +static char logWarnColour[] = COLOUR_WARN_PREFIX; +static char logErrColour[] = COLOUR_ERR_PREFIX; + +// Constants for the lenght of various elements of the timestamp and function ID +static const int tsTmpLen = strlen(STD_TIMESTAMP_SEC)+1; +static const int tsBufLen = strlen(STD_TIMESTAMP_WHOLE)+1; +static const int tsFunLen = strlen(FUNC_STR); + + +static char logFileName[MAX_LOG_FILE_NAME_LEN] = ""; +static FILE *logOutput = NULL; +static T_LogOutput outputState = notSet; +static int envNoColour = 0; + +/*============================================================================*/ +/* Logging_CheckInited */ +/*----------------------------------------------------------------------------*/ +// This checks that the library has been inited and is called by other +// logging functions - throws a warning and inits the library to stdout if it +// has not already been initialised. +/*----------------------------------------------------------------------------*/ +static void Logging_CheckInited(char *timestamp, const char *caller) +{ + if ( (logOutput == NULL) || (outputState == notSet) ) + { + logOutput = stdout; + outputState = standardOut; + fprintf(logOutput + , "%s%sLogging function %s() called without logging library being initialised >>>> defaulting to stdout\n" + , timestamp, logWarn, caller); + } +} +/*============================================================================*/ + +/*============================================================================*/ +/* Logging_LogMsgTS */ +/*----------------------------------------------------------------------------*/ +// Example intended usage: +// #define LOG_SOURCE "FileName" +// #define ME someFunction +// int Verbose = 1 // could be set at runtime via arg (e.g. ./prog -v) +// void someFunc (void) +// { +// Logging_LogMsgTS(LOG_SOURCE, ME, LOG_DEBUG, "This is some noisy info"); +// } +// #undef +// Output: +// [ 2025-06-07 02:12:02.005 ] (DEBUG) | FileName in function: someFunction(): This is some noisy info +// +// +// OR without custom defines: +// void someFunc (void) +// { +// Logging_LogMsgTS(__FILE__, __func__, LOG_DEBUG, "This is some noisy info"); +// } +// Output: +// [ 2025-06-07 02:12:02.005 ] (DEBUG) | logTest.c in function: someFunc(): This is some noisy info +/*----------------------------------------------------------------------------*/ +void Logging_LogMsgTS( const char * where + , const char * caller + , int severity + , const char * message, ... ) +{ + char buffer[MAX_LOG_BUF_LEN]; + int callerLen = strlen(where) + strlen(caller) + tsFunLen; + char *severityMsg = NULL; + int severityLen = 0; + int printfColour = 0; + + va_list args; + va_start(args, message); + + // first get the current time and insert at the front of the buffer + struct timespec now; + struct tm tm; + if ( clock_gettime(CLOCK_REALTIME, &now) < 0) + { + // Output the error to the file if used + if(outputState == logFile) + { + LOGMSG_N_ERR("Logging", "Logging_LogMsgTS", "Unable to get system time [errno: %d (%s)]" + , errno, strerror(errno)) + } + // Put error on stderr as well + fprintf(logOutput + , "%sLogging function Logging_LogMsgTS(): Unable to get system time [errno: %d (%s)]\n" + , (envNoColour == 1) ? logErr : logErrColour , errno, strerror(errno)); + return; + } + gmtime_r(&now.tv_sec, &tm); + strftime(buffer, tsTmpLen, "[ %Y-%m-%d %H:%M:%S.", &tm); + sprintf( (buffer + tsTmpLen -1), "%03lu ] ", (now.tv_nsec/1000000) % 1000000); + + + // check if the function has been called (making use of the pre-assembled timestamp) + Logging_CheckInited(buffer, __func__); + + // determine which string to use for the severity print out based on message + // severity level, output stream and whether the environment has NO_COLOR + // set [see Logging_Init()] + printfColour = ( (envNoColour == 1) || (outputState == logFile) ) ? NO_COLOUR : PRINT_COLOUR; + switch (severity) + { + case 0: + severityMsg = logDbg; + break; + case 1: + severityMsg = (printfColour == NO_COLOUR) ? logInfo : logInfoColour; + break; + case 2: + severityMsg = (printfColour == NO_COLOUR) ? logWarn : logWarnColour; + break; + case 3: + severityMsg = (printfColour == NO_COLOUR) ? logErr : logErrColour; + break; + default: + severityMsg = (printfColour == NO_COLOUR) ? logInfo : logInfoColour; + } + severityLen = strlen(severityMsg); + + // add the severity to the buffer + sprintf( (buffer + tsBufLen), "%s", severityMsg); + // then add the process/file name and function name to the output buffer + sprintf( (buffer + tsBufLen + severityLen), "%s in function: %s(): ", where, caller); + // finally add the supplied message and format with args + vsprintf( (buffer + tsBufLen + severityLen + callerLen), message, args ); + // print the message to the output + fprintf(logOutput, "%s\n", buffer); + va_end(args); +} +/*============================================================================*/ + +/*============================================================================*/ +/* Logging_LogMsg*/ +/*----------------------------------------------------------------------------*/ +// Like Logging_LogMsgTS above, but does not insert a timestamp +/*----------------------------------------------------------------------------*/ +void Logging_LogMsg( const char * where + , const char * caller + , int severity + , const char * message, ... ) +{ + char buffer[MAX_LOG_BUF_LEN]; + int callerLen = strlen(where) + strlen(caller) + tsFunLen; + char * severityMsg = NULL; + int severityLen = 0; + int printfColour = 0; + + va_list args; + va_start(args, message); + + // check if the function has been called (making use of the pre-assembled timestamp) + Logging_CheckInited(NULL, __func__); + + // determine which string to use for the severity print out based on message + // severity level, output stream and whether the environment has NO_COLOR + // set [see Logging_Init()] + printfColour = ( (envNoColour == 1) || (outputState == logFile) ) ? NO_COLOUR : PRINT_COLOUR; + switch (severity) + { + case 0: + severityMsg = logDbg; + break; + case 1: + severityMsg = (printfColour == NO_COLOUR) ? logInfo : logInfoColour; + break; + case 2: + severityMsg = (printfColour == NO_COLOUR) ? logWarn : logWarnColour; + break; + case 3: + severityMsg = (printfColour == NO_COLOUR) ? logErr : logErrColour; + break; + default: + severityMsg = (printfColour == NO_COLOUR) ? logInfo : logInfoColour; + } + severityLen = strlen(severityMsg); + + // add the severity to the buffer + sprintf( buffer, "%s", severityMsg); + // then add the process/file name and function name to the output buffer + sprintf( (buffer + severityLen), "%s in function: %s(): ", where, caller); + // finally add the supplied message and format with args + vsprintf( (buffer + severityLen + callerLen), message, args ); + // print the message to the output + fprintf(logOutput, "%s\n", buffer); + va_end(args); +} +/*============================================================================*/ + +/*============================================================================*/ +/* Logging_Init */ +/*----------------------------------------------------------------------------*/ +// It is not necessary to call Logging_Init, but it is strongly recommended. +// If it is not called, the first call to a logging function will set the +// output to stdout as default and throw a warning. +/*----------------------------------------------------------------------------*/ +#define ME "Logging_Init" +int Logging_Init(T_LogOutput output, char * fileName) +{ + // check if the ENV variable NO_COLOR is set (https://no-color.org/) + char *colorEnv = getenv("NO_COLOR"); + if( (colorEnv != NULL) && (colorEnv[0] != '\0') ) + { + envNoColour = 1; + } + else + { + envNoColour = 0; + } + + // if a file name has been supplied, attempt to open it, + // else check if the user wants the output to be + // stderr - otherwise the default output type is stdout + if (fileName != NULL) + { + logOutput = fopen(fileName, "a"); + if (logOutput == NULL) + { + logOutput = stdout; + Logging_LogMsg( __FILE__, ME, LOG_ERR, + "Can't open file: %s [errno: %d (%s)] >>>>>>> defaulting to stdout" + , fileName, errno, strerror(errno) ); + outputState = standardOut; + //return a fail in case the caller wants to handle (retry, terminate, etc.) + return 1; + } + else + { + //logOuput already set by successful fopen() + outputState = logFile; + memcpy(logFileName, fileName, (strlen(fileName) * sizeof(char))); + Logging_LogMsgTS(__FILE__, ME, LOG_INFO, "Set log output to file: %s", logFileName); + } + } + else if (output == standardErr) + { + logOutput = stderr; + outputState = standardErr; + Logging_LogMsgTS(__FILE__, ME, LOG_INFO, "Set log output to stderr"); + } + else + { + logOutput = stdout; + outputState = standardOut; + Logging_LogMsgTS(__FILE__, ME, LOG_INFO, "Set log output to stdout"); + } + return 0; +} +#undef ME +/*============================================================================*/ +/*============================================================================*/ +/* Logging_Term */ +/*----------------------------------------------------------------------------*/ +// If Logging_Init was not called prior, or the output stream is set to stdout +// or stderr, then there is strictly no need to call Logging_Term though it is +// strongly recommended. +// It is necessary to call Logging_Term if the logging output is set to a file! +/*----------------------------------------------------------------------------*/ +#define ME "Logging_Term" +void Logging_Term( void ) +{ + if (outputState == logFile) + { + fclose(logOutput); + memset(logFileName, '\0', sizeof(logFileName)); + } + logOutput = NULL; + outputState = notSet; +} +#undef ME +/*============================================================================*/ \ No newline at end of file diff --git a/Logging/logging_lib.h b/Logging/logging_lib.h new file mode 100644 index 0000000..1d572e4 --- /dev/null +++ b/Logging/logging_lib.h @@ -0,0 +1,118 @@ +#ifndef LOGGING_H +#define LOGGING_H + +#define MAX_LOG_BUF_LEN 1024 +#define MAX_LOG_FILE_NAME_LEN 400 + +// define severity levels for log message highlighting +#define LOG_DEBUG 0 +#define LOG_INFO 1 +#define LOG_WARN 2 +#define LOG_ERR 3 + +// Macros for verbosity level +#define LOG_VERBOSE 1 +#define LOG_QUIET 0 + +// Macros for colour console printing +#define PRINT_COLOUR 1 +#define NO_COLOUR 0 + +#define DEBUG_PREFIX "(DEBUG) | " +#define INFO_PREFIX "(INFO) | " +#define WARN_PREFIX "(WARNING) | " +#define ERR_PREFIX "(ERROR) | " + +// \x1b[31m ERROR \x1b[0m : \x1b[31m sets string to red, we then print 'ERROR', then reset terminal so it doesn stay red! +// ANSI escape code colouring for highlighting different messages +#define COLOUR_INFO_PREFIX "(\x1b[36mINFO\x1b[0m) | " +#define COLOUR_WARN_PREFIX "(\x1b[33mWARNING\x1b[0m) | " +#define COLOUR_ERR_PREFIX "(\x1b[31mERROR\x1b[0m) | " + +// Define the timstamp format strings and function caller ID string +#define STD_TIMESTAMP_SEC "[ 2025-06-04 12:52:11." +#define STD_TIMESTAMP_WHOLE "[ 2025-06-04 12:52:11.999 ]" +#define FUNC_STR " in function: (): " + +// Enum for use in determining current output stream +typedef enum { + notSet = 0, + standardOut = 1, + standardErr = 2, + logFile = 3 +}T_LogOutput; + +//*============================================================================*/ +// Macros for initialisation function +//*----------------------------------------------------------------------------*/ +#define LOGGING_INIT_STDOUT Logging_Init(standardOut, NULL); + +#define LOGGING_INIT_STDERR Logging_Init(standardErr, NULL); + +#define LOGGING_INIT_FILE(FileName) Logging_Init(logFile, FileName); +/*============================================================================*/ + +/*============================================================================*/ +// Useful variadic macros for logging functions - see https://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc/Variadic-Macros.html +//*----------------------------------------------------------------------------*/ +// Timestamped logging with file and func preprocessors used to define 'where' and 'caller' +//*----------------------------------------------------------------------------*/ +#define LOGMSG_TS_DBG(message, ...) if(Verbose == LOG_VERBOSE) { Logging_LogMsgTS(__FILE__, __func__, LOG_DEBUG, message, ##__VA_ARGS__);} + +#define LOGMSG_TS_INFO(message, ...) Logging_LogMsgTS(__FILE__, __func__, LOG_INFO, message, ##__VA_ARGS__); + +#define LOGMSG_TS_WARN(message, ...) Logging_LogMsgTS(__FILE__, __func__, LOG_WARN, message, ##__VA_ARGS__); + +#define LOGMSG_TS_ERR(message, ...) Logging_LogMsgTS(__FILE__, __func__, LOG_ERR, message, ##__VA_ARGS__); +//*----------------------------------------------------------------------------*/ +// Basic logging with file and func preprocessors used to define 'where' and 'caller' +//*----------------------------------------------------------------------------*/ +#define LOGMSG_DBG(message, ...) if(Verbose == LOG_VERBOSE) { Logging_LogMsg(__FILE__, __func__, LOG_DEBUG, message, ##__VA_ARGS__);} + +#define LOGMSG_INFO(message, ...) Logging_LogMsg(__FILE__, __func__, LOG_INFO, message, ##__VA_ARGS__); + +#define LOGMSG_WARN(message, ...) Logging_LogMsg(__FILE__, __func__, LOG_WARN, message, ##__VA_ARGS__); + +#define LOGMSG_ERR(message, ...) Logging_LogMsg(__FILE__, __func__, LOG_ERR, message, ##__VA_ARGS__); +//*----------------------------------------------------------------------------*/ +// Timestamped logging with arguments used to define 'where' and 'caller' (N = name) +//*----------------------------------------------------------------------------*/ +#define LOGMSG_TS_N_DBG(what, who, message, ...) if(Verbose == LOG_VERBOSE) {Logging_LogMsgTS(what, who, LOG_DEBUG, message, ##__VA_ARGS__);} + +#define LOGMSG_TS_N_INFO(what, who, message, ...) Logging_LogMsgTS(what, who, LOG_INFO, message, ##__VA_ARGS__); + +#define LOGMSG_TS_N_WARN(what, who, message, ...) Logging_LogMsgTS(what, who, LOG_WARN, message, ##__VA_ARGS__); + +#define LOGMSG_TS_N_ERR(what, who, message, ...) Logging_LogMsgTS(what, who, LOG_ERR, message, ##__VA_ARGS__); +//*----------------------------------------------------------------------------*/ +// Basic logging with arguments used to define 'where' and 'caller' (N = name) +//*----------------------------------------------------------------------------*/ +#define LOGMSG_N_DBG(what, who, message, ...) if(Verbose == LOG_VERBOSE) { Logging_LogMsg(what, who, LOG_DEBUG, message, ##__VA_ARGS__);} + +#define LOGMSG_N_INFO(what, who, message, ...) Logging_LogMsg(what, who, LOG_INFO, message, ##__VA_ARGS__); + +#define LOGMSG_N_WARN(what, who, message, ...) Logging_LogMsg(what, who, LOG_WARN, message, ##__VA_ARGS__); + +#define LOGMSG_N_ERR(what, who, message, ...) Logging_LogMsg(what, who, LOG_ERR, message, ##__VA_ARGS__); +//*============================================================================*/ + +//*============================================================================*/ +// Prototypes for logging functions +//*----------------------------------------------------------------------------*/ + +void Logging_LogMsgTS( const char * where + , const char * caller + , int severity + , const char * message, ... ); + +void Logging_LogMsg( const char * where + , const char * caller + , int severity + , const char * message, ... ); + +int Logging_Init(T_LogOutput output, char * fileName); + +void Logging_Term( void ); +//*============================================================================*/ + +#endif //LOGGING_H \ No newline at end of file diff --git a/Networking/udp2file.c b/Networking/udp2file.c new file mode 100644 index 0000000..6426a83 --- /dev/null +++ b/Networking/udp2file.c @@ -0,0 +1,443 @@ +/*============================================================================*/ +// +// Title: udp2file.c +// Author: RH +// Last modified: 13/09/2025 +// +//============================================================================ +// +// Description: +// ------------ +// Tool that takes a file path, ip address, port, and capture size. The tool +// then listens on the supplied address and port for UDP datagrams and captures +// messages in chunks up to the capture size at a time, before then writing +// the binary data to the specified filepath. +// +// Compilation: +// ------------ +// gcc -Wall -Wextra ../Logging/logging_lib.c udp2file.c -o udp2file +// +/*============================================================================*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Logging/logging_lib.h" + +// program, verbose (optional), buffer check (optional), file path, ip address, +// port, capture size +#define MIN_NUM_ARGS 4 +#define MAX_NUM_ARGS 6 +#define NUM_REQ_ARGS 3 + +#define MAX_PAYLOAD_SIZE 65507 + +// ---------------------------------------------------------------------------- +// print usage information to the terminal to provide information on how the +// program expects to be run +// ---------------------------------------------------------------------------- + +void usage (void) +{ + printf("\n"); + printf("---------------------------------------------------------------------------------------------\n"); + printf(" Usage:\n"); + printf(" ./udp2file [options] \n"); + printf("---------------------------------------------------------------------------------------------\n"); + printf(" Required Arguments:\n"); + printf(" :\n"); + printf(" \tThe path to the file to write the captured binary data to. Will create the file if it\n"); + printf(" \tdoes not already exist and will overwite it if it does.\n"); + printf(" :\n"); + printf(" \tThe port to listen on for the UDP datagrams.\n"); + printf(" :\n"); + printf(" \tThe capture size (in bytes) to read from the system UDP buffer. This is the size of\n"); + printf(" \tthe datagram payload, so an MTU of 9000 (jumbo frames) means a capture size of 8972\n"); + printf(" \tbytes. [Range: 1 to 65507.]\n"); + printf("\n"); + printf(" Optional Arguments:\n"); + printf(" -h, --help\n"); + printf(" \tDisplay this usage information then exit.\n"); + printf(" -v, --verbose\n"); + printf(" \tVerbose flag to enable additional debug output messages - at high data rate this can\n"); + printf(" \tbe very noisy and so is not recommended for operational use.\n"); + printf(" -b, --buffer-check\n"); + printf(" \tEnable additional checking of the system UDP buffer that warns if the queue is\n"); + printf(" \tgrowing and reports an error when datagrams are dropped. This is not recommended for\n"); + printf(" \tvery high data rates (>512 mbps).\n"); + printf("---------------------------------------------------------------------------------------------\n"); + printf(" Example:\n"); + printf(" ./udp_receiver ~/udp_captures/catpure_6.bin 8080 8972\n"); + printf("---------------------------------------------------------------------------------------------\n"); +} + +// ---------------------------------------------------------------------------- +// signal handler to allow application to exit cleanly +// ---------------------------------------------------------------------------- + +static int run = 1; + +// the handler just sets 'run' to 0, which allows the main loop to exit +// and the cleanup code after the main loop to run +static void SignalHandler ( int dummy ) +{ + dummy++ ; + LOGMSG_INFO(">>>>>>>>>>>> Signal received, terminating...") + run = 0; +} + +// ---------------------------------------------------------------------------- +// main program - parse arguments, open output file, create listener +// socket, then enter a loop waiting for datagrams and writing them to file +// ---------------------------------------------------------------------------- + +int main(int argc, char **argv) +{ + // initiliase logger to print messages to stdout + int Verbose = 0; + LOGGING_INIT_STDOUT + + // signal handlers to ensure program exits cleanly + (void) signal(SIGINT , SignalHandler) ; + (void) signal(SIGTERM, SignalHandler) ; + + // ------------------------------------------------------------------------- + // parse input arguments + // ------------------------------------------------------------------------- + + // check for help argument + if ( (strncmp ( argv[1], "-h", strlen("-h")) == 0) + || (strncmp ( argv[1], "--help", strlen("--help")) == 0) + ) + { + usage(); + return -1; + } + + if ( (argc < MIN_NUM_ARGS) || (argc > MAX_NUM_ARGS) ) + { + LOGMSG_ERR("Error: incorrect number of arguments supplied. Received %d, minimum %d -> maximum %d", + argc, MIN_NUM_ARGS, MAX_NUM_ARGS) + usage(); + return -1; + } + + // check optional arguments (excluding --help) + int current_arg = 1; + int buffer_check = 0; + // check combinations first: -vb, -bv + if ( (strncmp ( argv[current_arg], "-vb", strlen("-vb")) == 0) + || (strncmp ( argv[current_arg], "-bv", strlen("-bv")) == 0) + ) + { + Verbose = 1; + buffer_check = 1; + current_arg++; + } + + // check for verbose supplied individually - Verbose == 0 used to prevent + // '-v' from incrementing current_args if '-vb' above has already been set + if ( ( Verbose == 0 ) + && ( (strncmp ( argv[current_arg], "-v", strlen("-v")) == 0) + || (strncmp ( argv[current_arg], "--verbose", strlen("--verbose")) == 0) ) + ) + { + Verbose = 1; + current_arg++; + } + + // check buffer flag supplied individually - buffer_check == 0 used to prevent + // '-b' from incrementing current_args if '-bv' above has already been set + if ( ( buffer_check == 0 ) + && ( (strncmp ( argv[current_arg], "-b", strlen("-b")) == 0) + || (strncmp ( argv[current_arg], "--buffer-check", strlen("--buffer-check")) == 0) ) + ) + { + buffer_check = 1; + current_arg++; + } + + // check required arguments (file name, port, payload size) + // there should only be 3 arguments left to process + char *pFileName = NULL; + uint32_t port = 0; + size_t payloadSize = 0; + if ( (argc - current_arg) != NUM_REQ_ARGS ) + { + LOGMSG_ERR("Error parsing input arguments: Received %d, minimum %d -> maximum %d", + argc, MIN_NUM_ARGS, MAX_NUM_ARGS) + usage(); + return -1; + } + else + { + pFileName = argv[current_arg++]; + port = atoi(argv[current_arg++]); + payloadSize = atoi(argv[current_arg]); + } + + if ( (payloadSize > MAX_PAYLOAD_SIZE) || (payloadSize == 0) ) + { + LOGMSG_ERR("Error: supplied payload size [%d] must be greater than 0 and not" + " more than the maximum possible UDP datagram payload size [%d]", + payloadSize, MAX_PAYLOAD_SIZE) + usage(); + return -1; + } + + LOGMSG_INFO("Running udp_receiver | Verbose: %s, Buffer checking: %s, Output" + " file: %s, Port: %d, Payload size: %d", + (Verbose == 1) ? "Yes" : "No", (buffer_check == 1) ? "Yes" : "No", + pFileName, port, payloadSize) + + // ------------------------------------------------------------------------- + // open the file to write the binary data to + // ------------------------------------------------------------------------- + FILE *pDestFile = NULL; + pDestFile = fopen(pFileName, "w"); + size_t bytes_written = 0; + if (pDestFile == NULL) + { + LOGMSG_ERR("Error: unable to open the requested file %s. Errno: %s [%d]", + pFileName, strerror(errno), errno) + return -1; + } + else + { + LOGMSG_DBG("Opened file: %s", pFileName) + } + + // ------------------------------------------------------------------------- + // create the socket and bind to the requested port + // ------------------------------------------------------------------------- + + // the buffer to read the datagram payload, size set by program argument + char udpRecvBuffer[payloadSize]; + + // socket and interface information + int rx_fd = 0; + ssize_t bytes_received = 0; + struct sockaddr_in local_addr; + socklen_t sock_length = sizeof(struct sockaddr); + + // clear local_addr + memset(&local_addr, '\0', sizeof(local_addr)); + + // populate information about the remote and the port to listen on + local_addr.sin_family = AF_INET; + local_addr.sin_port = htons(port); + local_addr.sin_addr.s_addr = INADDR_ANY; + + // create socket file descriptor + if ( (rx_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) + { + LOGMSG_ERR("Error: unable to create UDP listener socket. Errno: %s [%d]", + strerror(errno), errno) + fclose(pDestFile); + return -1; + } + + // set the socket REUSEADDR option + int option = 1 ; + if ( setsockopt(rx_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) < 0 ) + { + LOGMSG_ERR("Error: unable to set socket options. Errno: %s [%d]", + strerror(errno), errno) + fclose(pDestFile); + close(rx_fd); + return -1; + } + + // bind the socket to the local address + if ( bind(rx_fd, (const struct sockaddr *) &local_addr, sizeof(local_addr)) < 0 ) + { + LOGMSG_ERR("Error: unable to bind to listener socket. Errno: %s [%d]", + strerror(errno), errno) + fclose(pDestFile); + close(rx_fd); + return -1; + } + + LOGMSG_DBG("Socket %d opened for port: %d", rx_fd, port) + + // ------------------------------------------------------------------------- + // buffer checking variables for use if it is enabled + // ------------------------------------------------------------------------- + + // variables to store buffer stats into + uint32_t local_port = 0; + uint32_t rx_buffer_used_space = 0; + uint32_t prev_rx_buffer_used_space = 0; + uint32_t prev_dopped_dgram_count = 0; + uint32_t curr_dopped_dgram_count = 0; + + // variables for reading the buffer stats file + ssize_t buffer_stats_bytes_read = 0; + size_t line_len = 0; + char *stat_line = NULL; + + char udp_buffer_stats_path[] = "/proc/net/udp"; + FILE *pBufStatFile = NULL; + + // check the file can be opened + if (buffer_check == 1) + { + pBufStatFile = fopen(udp_buffer_stats_path, "r"); + if (pBufStatFile == NULL) + { + LOGMSG_ERR("Error: unable to open UDP buffer statistics file at %s. Errno: %s [%d]", + udp_buffer_stats_path, strerror(errno), errno) + buffer_check = 0; + } + } + + // variables to flag bad buffer states so we don't spam the log + int buffer_overflow = 0; + int rx_queue_growing = 0; + + // ------------------------------------------------------------------------- + // main loop + // ------------------------------------------------------------------------- + + LOGMSG_DBG("Capturing UDP datagrams - payload size: %d port: %d output file: %s", + payloadSize, port, pFileName) + + while(run == 1) + { + // non-blocking function to loop until a message appears in the UDP buffer, + // then read bytes from the system buffer up to the amount specified by + // payloadSize. + // non-blocking means that the while-loop can be exited when receiving + // a system signal + bytes_received = recvfrom(rx_fd, udpRecvBuffer, payloadSize, MSG_DONTWAIT, (struct sockaddr *) &local_addr, &sock_length); + + // we have read in some number of bytes successfully + if ( bytes_received > 0 ) + { + LOGMSG_DBG("Successfully received %d bytes (buffer size %d)", bytes_received, payloadSize) + + // attempt to write the captured bytes to file + bytes_written = fwrite(udpRecvBuffer, sizeof(char), bytes_received, pDestFile); + + // we have written the data to file successfully + if ( bytes_written == ((size_t)bytes_received) ) + { + LOGMSG_DBG("Successfully written %d bytes to file: %s", bytes_written, pFileName) + } + else // error + { + LOGMSG_ERR("Error: Failed to write all captured data to the output file %s: Errno: %s [%d] Bytes read: %d Bytes written: %d", + pFileName, strerror(errno), errno, bytes_received, bytes_written) + } + } + else + { + if (bytes_received == 0) + { + LOGMSG_WARN("Received 0 byte message") + } + else if ( (errno != EAGAIN) && (errno != EWOULDBLOCK) ) // error + { + LOGMSG_ERR("Error: UDP receive function failed: Errno: %s [%d] Bytes read: %d", + strerror(errno), errno, bytes_received) + } + } + + // check the system buffer and report if we are dropping datagrams + if (buffer_check == 1) + { + // need to use freopen as existing lines in the file have changed + freopen(udp_buffer_stats_path, "r", pBufStatFile); + while( (buffer_stats_bytes_read = getline(&stat_line, &line_len, pBufStatFile)) != -1 ) + { + // onl process the line if it is not the header + if ( strstr(stat_line, "sl local_address rem_address") == NULL ) + { + // parse the line and store into variables + // %*d allows the fomratting to skip over an element (the * means to not assign to destination) + sscanf(stat_line, "%*d: %*X:%4X %*X:%*X %*d %*X:%8X %*d:%*X %*X %*d %*d %*d %*d %*X %d", + &local_port, &rx_buffer_used_space, &curr_dopped_dgram_count ); + + // check if we have dropped any datagrams on the port we are listening to + if (local_port == port) + { + //TODO: include with a logging interval + // LOGMSG_DBG("port: %04X tx_queue: %X rx_queue: %X dropped_datagrams: %d", + // local_port, tx_queue, rx_queue, dropped_datagrams); + + // log once if a buffer overflow occurs, until the rx queue is cleared + if ( (curr_dopped_dgram_count != prev_dopped_dgram_count) + && (buffer_overflow == 0) + ) + { + LOGMSG_ERR("Error: UDP BUFFER OVERFLOW ON PORT %X - Rx Buffer: %X | Dropped datagrams: %d (previously %d)", + local_port, rx_buffer_used_space, curr_dopped_dgram_count, prev_dopped_dgram_count) + buffer_overflow = 1; + } + + // warn if the rx queue is growing (impending overflow) - at high UDP data rates there will likely be + // no warning if the system buffer is small as a gigabit link could fill the buffer within a check period + if ( (rx_buffer_used_space > prev_rx_buffer_used_space) + && (rx_queue_growing == 0) + ) + { + LOGMSG_WARN("UDP RX BUFFER QUEUE GROWING ON PORT %X - Rx Buffer: %X (previously %X) | Dropped datagrams: %d (previously %d)", + local_port, rx_buffer_used_space, prev_rx_buffer_used_space, curr_dopped_dgram_count, prev_dopped_dgram_count ) + rx_queue_growing = 1; + } + + // clear the buffer_overflow flag so we can log further unique instances + if ( (rx_buffer_used_space == 0) + && ( (rx_queue_growing == 1) || (buffer_overflow == 1) ) + ) + { + // clear flags as buffer has returned to a nominal state + buffer_overflow = 0; + rx_queue_growing = 0; + + LOGMSG_INFO("UDP RX Buffer on port %X has returned to nominal conditions - Rx Buffer: %X (previously %X) | Total dropped datagrams: %d", + rx_buffer_used_space, prev_rx_buffer_used_space, curr_dopped_dgram_count) + } + + // update variables used to detect error conditions + prev_dopped_dgram_count = curr_dopped_dgram_count; + prev_rx_buffer_used_space = rx_buffer_used_space; + + // we have parsed the line relevant to our port, so we can skip the rest of the file + break; + } + } + + } + } + + // short sleep to not hog resource - 1000 us at 8972 bytes gives a max + // throughput of 8972000 Bytes/s (71776000 b/s) + // a usleep of 50 us is recommended for gigabit throughput using jumbo + // frames: 8972 bytes per 50 us means 179440000 Bytes/s + // (1435520000 b/s [1.4 Gbps]) + // also beware disk usage limitations when attempting gigabit + // captures (~178 MBps) + usleep(1000); + } + + + // ------------------------------------------------------------------------- + // clean up + // ------------------------------------------------------------------------- + fclose(pDestFile); + fclose(pBufStatFile); + close(rx_fd); + + LOGMSG_INFO("\n######################\n\tProgram exit\n######################") + + return 0; +} \ No newline at end of file diff --git a/README.md b/README.md index 0cfe981..f99fe32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # Utilities -Collection of C programs to perform various utilities and provide examples. \ No newline at end of file +Collection of C programs to perform various utilities and provide examples. +Compilation instructions are given at the top of each source file, or in a README if a Makefile is used. + +## Logging + +A general purpose logging library with optional timestamping and terminal colouring for log message severity. +This is used by most applications I build, certainly all within this repository. + +## Networking + +Contains a standard UDP listener implementation that writes to a file. \ No newline at end of file