aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-17 11:57:52 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-17 11:57:52 +0100
commit48a9df043df64887cb99e03d7613379c947d11d8 (patch)
tree897d94bcc55c481a82878c5d2de5ec3369df33ed
parenta54421dd976fd8081e96c11c2621076876c9986b (diff)
downloadBobinkCOpcUa-48a9df043df64887cb99e03d7613379c947d11d8.tar.gz
BobinkCOpcUa-48a9df043df64887cb99e03d7613379c947d11d8.zip
Add configurable log level as optional CLI argument
All three programs now accept an optional second argument [log-level] (trace, debug, info, warning, error, fatal) defaulting to info. The level is applied by setting the logger context pointer directly, avoiding a memory leak that would occur from overwriting the heap-allocated logger struct. Also documents the ASan leak-check workflow in CLAUDE.md.
-rw-r--r--CLAUDE.md19
-rw-r--r--src/client_find_servers.c17
-rw-r--r--src/common.c20
-rw-r--r--src/common.h11
-rw-r--r--src/server_lds.c21
-rw-r--r--src/server_register.c19
-rwxr-xr-xtests/run_test.sh46
7 files changed, 123 insertions, 30 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 4135924..cabc688 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -61,7 +61,7 @@ Existing certs live in `certs/`. Only regenerate if missing.
## Running
-Each program takes a single argument: a configuration file. Example config files are in `config/`. Start programs in order in separate terminals from the project root:
+Each program takes a configuration file and an optional log level: `<program> <config-file> [log-level]`. The log level defaults to `info`. Valid levels: `trace`, `debug`, `info`, `warning`, `error`, `fatal`. Example config files are in `config/`. Start programs in order in separate terminals from the project root:
**1. Local Discovery Server (LDS)**
@@ -163,6 +163,23 @@ Each test starts ServerLDS (port 14840) and ServerRegister (port 14841), runs Cl
To add a new test case: create a directory under `tests/` with 3 config files (`server_lds.conf`, `server_register.conf`, `client_find_servers.conf`), then add a `"name;ExpectedPolicy"` entry to the `INTEGRATION_TESTS` list in `CMakeLists.txt`.
+### Memory Leak Check (ASan)
+
+Rebuild with AddressSanitizer, then run the tests:
+
+```sh
+cmake -B build -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
+cmake --build build --parallel
+ASAN_OPTIONS="detect_leaks=1" ctest --test-dir build --output-on-failure
+```
+
+Reconfigure without the flags afterwards to return to a normal build:
+
+```sh
+cmake -B build -DCMAKE_C_FLAGS="" -DCMAKE_EXE_LINKER_FLAGS=""
+cmake --build build --parallel
+```
+
## Project Structure
| Path | Purpose |
diff --git a/src/client_find_servers.c b/src/client_find_servers.c
index e50623f..a85b63f 100644
--- a/src/client_find_servers.c
+++ b/src/client_find_servers.c
@@ -211,10 +211,21 @@ readServerTime (UA_Client *client,
int
main (int argc, char **argv)
{
- if (argc != 2)
+ if (argc < 2 || argc > 3)
{
UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
- "Usage: %s <config-file>", argv[0]);
+ "Usage: %s <config-file> [log-level]", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ const char *logLevelStr = (argc == 3) ? argv[2] : "info";
+ int logLevel = parseLogLevel (logLevelStr);
+ if (logLevel < 0)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown log level: %s "
+ "(expected trace, debug, info, warning, error, fatal)",
+ logLevelStr);
return EXIT_FAILURE;
}
@@ -302,6 +313,8 @@ main (int argc, char **argv)
configFree (&cfg);
return EXIT_FAILURE;
}
+ UA_ClientConfig *clientConfig = UA_Client_getConfig (client);
+ clientConfig->logging->context = (void *)(uintptr_t)logLevel;
UA_ApplicationDescription *applicationDescriptionArray = NULL;
size_t applicationDescriptionArraySize = 0;
diff --git a/src/common.c b/src/common.c
index d102868..7d378f1 100644
--- a/src/common.c
+++ b/src/common.c
@@ -59,6 +59,26 @@ loadFile (const char *const path)
* Security Helpers
* ======================================================================== */
+int
+parseLogLevel (const char *name)
+{
+ static const struct
+ {
+ const char *name;
+ UA_LogLevel level;
+ } levels[] = {
+ { "trace", UA_LOGLEVEL_TRACE }, { "debug", UA_LOGLEVEL_DEBUG },
+ { "info", UA_LOGLEVEL_INFO }, { "warning", UA_LOGLEVEL_WARNING },
+ { "error", UA_LOGLEVEL_ERROR }, { "fatal", UA_LOGLEVEL_FATAL },
+ };
+ for (size_t i = 0; i < sizeof (levels) / sizeof (levels[0]); i++)
+ {
+ if (strcmp (name, levels[i].name) == 0)
+ return (int)levels[i].level;
+ }
+ return -1;
+}
+
UA_MessageSecurityMode
parseSecurityMode (const char *name)
{
diff --git a/src/common.h b/src/common.h
index e3d2f4c..e8c0c78 100644
--- a/src/common.h
+++ b/src/common.h
@@ -44,6 +44,17 @@ UA_Server *createSecureServer (UA_UInt16 port, const char *applicationUri,
UA_StatusCode *retval);
/**
+ * @brief Parses a log-level name into the corresponding UA_LogLevel value.
+ *
+ * Accepted names (case-sensitive): "trace", "debug", "info", "warning",
+ * "error", "fatal".
+ *
+ * @param name Log-level name string.
+ * @return The matching UA_LogLevel, or -1 if the name is not recognized.
+ */
+int parseLogLevel (const char *name);
+
+/**
* @brief Parses a security mode name into the corresponding enum value.
*
* Accepted names: "None", "Sign", "SignAndEncrypt".
diff --git a/src/server_lds.c b/src/server_lds.c
index c6960d5..2fe508f 100644
--- a/src/server_lds.c
+++ b/src/server_lds.c
@@ -35,10 +35,21 @@ main (int argc, char *argv[])
signal (SIGINT, stopHandler);
signal (SIGTERM, stopHandler);
- if (argc != 2)
+ if (argc < 2 || argc > 3)
{
UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
- "Usage: %s <config-file>", argv[0]);
+ "Usage: %s <config-file> [log-level]", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ const char *logLevelStr = (argc == 3) ? argv[2] : "info";
+ int logLevel = parseLogLevel (logLevelStr);
+ if (logLevel < 0)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown log level: %s "
+ "(expected trace, debug, info, warning, error, fatal)",
+ logLevelStr);
return EXIT_FAILURE;
}
@@ -116,6 +127,12 @@ main (int argc, char *argv[])
}
UA_ServerConfig *serverConfig = UA_Server_getConfig (server);
+ serverConfig->logging->context = (void *)(uintptr_t)logLevel;
+
+ /* Some OPC UA stacks omit the timestamp in the request header. The
+ default behaviour rejects these requests with BadInvalidTimestamp.
+ Downgrade to a warning so third-party servers can still register. */
+ serverConfig->verifyRequestTimestamp = UA_RULEHANDLING_WARN;
/* Configure access control after server creation because
UA_ServerConfig_setDefaultWithSecurityPolicies (called by
diff --git a/src/server_register.c b/src/server_register.c
index ae8e959..5bdba8e 100644
--- a/src/server_register.c
+++ b/src/server_register.c
@@ -42,10 +42,21 @@ main (int argc, char **argv)
signal (SIGINT, stopHandler);
signal (SIGTERM, stopHandler);
- if (argc != 2)
+ if (argc < 2 || argc > 3)
{
UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
- "Usage: %s <config-file>", argv[0]);
+ "Usage: %s <config-file> [log-level]", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ const char *logLevelStr = (argc == 3) ? argv[2] : "info";
+ int logLevel = parseLogLevel (logLevelStr);
+ if (logLevel < 0)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown log level: %s "
+ "(expected trace, debug, info, warning, error, fatal)",
+ logLevelStr);
return EXIT_FAILURE;
}
@@ -182,6 +193,7 @@ main (int argc, char **argv)
}
UA_ServerConfig *serverConfig = UA_Server_getConfig (server);
+ serverConfig->logging->context = (void *)(uintptr_t)logLevel;
/* Configure access control after server creation because
UA_ServerConfig_setDefaultWithSecurityPolicies (called by
@@ -224,6 +236,7 @@ main (int argc, char **argv)
configFree (&cfg);
return EXIT_FAILURE;
}
+ clientConfig.logging->context = (void *)(uintptr_t)logLevel;
if (clientUsername)
UA_ClientConfig_setAuthenticationUsername (&clientConfig, clientUsername,
clientPassword);
@@ -254,6 +267,7 @@ main (int argc, char **argv)
trustPaths, trustSize, securityMode, securityPolicyUri);
if (retval == UA_STATUSCODE_GOOD)
{
+ clientConfig.logging->context = (void *)(uintptr_t)logLevel;
if (clientUsername)
UA_ClientConfig_setAuthenticationUsername (
&clientConfig, clientUsername, clientPassword);
@@ -278,6 +292,7 @@ main (int argc, char **argv)
trustSize, securityMode, securityPolicyUri);
if (retval == UA_STATUSCODE_GOOD)
{
+ clientConfig.logging->context = (void *)(uintptr_t)logLevel;
if (clientUsername)
UA_ClientConfig_setAuthenticationUsername (
&clientConfig, clientUsername, clientPassword);
diff --git a/tests/run_test.sh b/tests/run_test.sh
index 5173fa3..8a87824 100755
--- a/tests/run_test.sh
+++ b/tests/run_test.sh
@@ -29,18 +29,18 @@ FAILURES=0
# ── cleanup ────────────────────────────────────────────────────
cleanup() {
- [ -n "$LDS_PID" ] && kill "$LDS_PID" 2>/dev/null && wait "$LDS_PID" 2>/dev/null
- [ -n "$SR_PID" ] && kill "$SR_PID" 2>/dev/null && wait "$SR_PID" 2>/dev/null
- [ -n "$TMPFILE" ] && rm -f "$TMPFILE"
+ [ -n "$LDS_PID" ] && kill "$LDS_PID" 2>/dev/null && wait "$LDS_PID" 2>/dev/null
+ [ -n "$SR_PID" ] && kill "$SR_PID" 2>/dev/null && wait "$SR_PID" 2>/dev/null
+ [ -n "$TMPFILE" ] && rm -f "$TMPFILE"
}
trap cleanup EXIT
# ── port check ─────────────────────────────────────────────────
for port in $LDS_PORT $SR_PORT; do
- if ss -tlnp 2>/dev/null | grep -q ":${port} "; then
- echo "FAIL: port $port is already in use"
- exit 1
- fi
+ if ss -tlnp 2>/dev/null | grep -q ":${port} "; then
+ echo "FAIL: port $port is already in use"
+ exit 1
+ fi
done
# ── start LDS ──────────────────────────────────────────────────
@@ -48,8 +48,8 @@ build/ServerLDS "$CONFIG_DIR/server_lds.conf" >/dev/null 2>&1 &
LDS_PID=$!
sleep 2
if ! kill -0 "$LDS_PID" 2>/dev/null; then
- echo "FAIL: ServerLDS exited prematurely"
- exit 1
+ echo "FAIL: ServerLDS exited prematurely"
+ exit 1
fi
# ── start ServerRegister ───────────────────────────────────────
@@ -57,8 +57,8 @@ build/ServerRegister "$CONFIG_DIR/server_register.conf" >/dev/null 2>&1 &
SR_PID=$!
sleep 3
if ! kill -0 "$SR_PID" 2>/dev/null; then
- echo "FAIL: ServerRegister exited prematurely"
- exit 1
+ echo "FAIL: ServerRegister exited prematurely"
+ exit 1
fi
# ── run client ─────────────────────────────────────────────────
@@ -70,13 +70,13 @@ CLIENT_OUTPUT=$(<"$TMPFILE")
# ── validation checks ─────────────────────────────────────────
check() {
- local label="$1" result="$2"
- if [ "$result" -eq 0 ]; then
- echo "PASS: $label"
- else
- echo "FAIL: $label"
- FAILURES=$((FAILURES + 1))
- fi
+ local label="$1" result="$2"
+ if [ "$result" -eq 0 ]; then
+ echo "PASS: $label"
+ else
+ echo "FAIL: $label"
+ FAILURES=$((FAILURES + 1))
+ fi
}
# 1. Exit code
@@ -97,10 +97,10 @@ check "endpoint contains $EXPECTED_POLICY" $?
# ── result ─────────────────────────────────────────────────────
if [ "$FAILURES" -ne 0 ]; then
- echo ""
- echo "--- client output ---"
- echo "$CLIENT_OUTPUT"
- echo "--- end ---"
- exit 1
+ echo ""
+ echo "--- client output ---"
+ echo "$CLIENT_OUTPUT"
+ echo "--- end ---"
+ exit 1
fi
exit 0