Skip to content

Commit e295a0d

Browse files
authored
Fix MQTT cleanup lifetime race, ONVIF concurrency/parsing safety, and MQTT docs payload mismatch (#452)
* Initial plan * Fix MQTT and ONVIF safety issues with doc sync * Harden MQTT cleanup timeout synchronization * Address review nits in ONVIF and MQTT timeout logging * Handle MQTT timeout clock and log edge cases safely --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 4aaae6f commit e295a0d

3 files changed

Lines changed: 116 additions & 57 deletions

File tree

docs/MQTT_INTEGRATION.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ Detection events are published as JSON with the following structure:
119119
{
120120
"stream": "front_door",
121121
"timestamp": 1706745600,
122-
"timestamp_iso": "2024-02-01T12:00:00Z",
123122
"count": 2,
124123
"snapshot_path": "/var/lib/lightnvr/recordings/snapshots/front_door/20240201_120000.jpg",
125124
"snapshot_url": "/api/snapshots/front_door/20240201_120000.jpg",
@@ -154,7 +153,6 @@ Detection events are published as JSON with the following structure:
154153
|-------|-------------|
155154
| `stream` | Name of the camera/stream |
156155
| `timestamp` | Unix timestamp (seconds since epoch) |
157-
| `timestamp_iso` | ISO 8601 formatted timestamp |
158156
| `count` | Number of detections in this event |
159157
| `snapshot_path` | Local filesystem path of the event snapshot (omitted if capture failed) |
160158
| `snapshot_url` | Relative URL to fetch the snapshot from the LightNVR web server (uses normal authentication) |
@@ -289,7 +287,7 @@ def on_connect(client, userdata, flags, rc):
289287
def on_message(client, userdata, msg):
290288
data = json.loads(msg.payload)
291289
stream = data['stream']
292-
timestamp = data['timestamp_iso']
290+
timestamp = data['timestamp']
293291
294292
for det in data['detections']:
295293
print(f"[{timestamp}] {stream}: {det['label']} ({det['confidence']:.0%})")
@@ -430,4 +428,3 @@ client_id = lightnvr-garage
430428
client_id = lightnvr-frontyard
431429
```
432430

433-

src/core/mqtt_client.c

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,11 +1221,14 @@ typedef enum {
12211221
MQTT_OP_LIB_CLEANUP
12221222
} mqtt_cleanup_op_t;
12231223

1224-
// Thread argument structure - uses a simple volatile flag for completion
1224+
// Thread argument structure for timeout-aware cleanup worker
12251225
typedef struct {
12261226
struct mosquitto *mosq;
12271227
mqtt_cleanup_op_t op;
1228-
volatile int *done_flag; // Pointer to completion flag
1228+
pthread_mutex_t mutex;
1229+
pthread_cond_t cond;
1230+
bool done;
1231+
bool timed_out;
12291232
} mqtt_cleanup_arg_t;
12301233

12311234
/**
@@ -1234,7 +1237,6 @@ typedef struct {
12341237
static void *mqtt_cleanup_thread(void *arg) {
12351238
log_set_thread_context("MQTT", NULL);
12361239
mqtt_cleanup_arg_t *cleanup_arg = (mqtt_cleanup_arg_t *)arg;
1237-
volatile int *done_flag = cleanup_arg->done_flag;
12381240

12391241
switch (cleanup_arg->op) {
12401242
case MQTT_OP_LOOP_STOP:
@@ -1248,58 +1250,105 @@ static void *mqtt_cleanup_thread(void *arg) {
12481250
break;
12491251
}
12501252

1251-
// Signal completion by setting the flag
1252-
// Note: cleanup_arg is on the stack of the caller and remains valid
1253-
// until the caller returns (after timeout or completion)
1254-
*done_flag = 1;
1253+
pthread_mutex_lock(&cleanup_arg->mutex);
1254+
cleanup_arg->done = true;
1255+
pthread_cond_signal(&cleanup_arg->cond);
1256+
bool timed_out = cleanup_arg->timed_out;
1257+
pthread_mutex_unlock(&cleanup_arg->mutex);
1258+
1259+
// On timeout, caller detached and returned; worker owns cleanup.
1260+
if (timed_out) {
1261+
pthread_cond_destroy(&cleanup_arg->cond);
1262+
pthread_mutex_destroy(&cleanup_arg->mutex);
1263+
free(cleanup_arg);
1264+
}
12551265

12561266
return NULL;
12571267
}
12581268

12591269
/**
12601270
* Run a mosquitto cleanup operation with a timeout
1261-
* Uses simple polling with usleep - portable across glibc and musl
1271+
* Uses a timed condition wait with a worker thread
12621272
* Returns true if completed within timeout, false if timed out
12631273
*/
12641274
static bool mqtt_run_with_timeout(struct mosquitto *m, mqtt_cleanup_op_t op, int timeout_sec, const char *op_name) {
12651275
pthread_t thread;
1266-
volatile int done_flag = 0;
1276+
mqtt_cleanup_arg_t *arg = (mqtt_cleanup_arg_t *)calloc(1, sizeof(mqtt_cleanup_arg_t));
12671277

1268-
// Argument structure on stack - valid for duration of this function
1269-
mqtt_cleanup_arg_t arg;
1270-
arg.mosq = m;
1271-
arg.op = op;
1272-
arg.done_flag = &done_flag;
1278+
if (arg == NULL) {
1279+
log_warn("MQTT: Failed to allocate resources for %s", op_name);
1280+
return false;
1281+
}
1282+
1283+
if (pthread_mutex_init(&arg->mutex, NULL) != 0) {
1284+
free(arg);
1285+
log_warn("MQTT: Failed to initialize synchronization objects for %s", op_name);
1286+
return false;
1287+
}
1288+
if (pthread_cond_init(&arg->cond, NULL) != 0) {
1289+
pthread_mutex_destroy(&arg->mutex);
1290+
free(arg);
1291+
log_warn("MQTT: Failed to initialize synchronization objects for %s", op_name);
1292+
return false;
1293+
}
1294+
1295+
arg->mosq = m;
1296+
arg->op = op;
12731297

12741298
// Create thread to run the operation
1275-
if (pthread_create(&thread, NULL, mqtt_cleanup_thread, &arg) != 0) {
1299+
if (pthread_create(&thread, NULL, mqtt_cleanup_thread, arg) != 0) {
12761300
log_warn("MQTT: Failed to create thread for %s", op_name);
1301+
pthread_cond_destroy(&arg->cond);
1302+
pthread_mutex_destroy(&arg->mutex);
1303+
free(arg);
12771304
return false;
12781305
}
12791306

1280-
// Detach the thread so it cleans up automatically if we timeout
1281-
pthread_detach(thread);
1307+
struct timespec ts;
1308+
if (clock_gettime(CLOCK_REALTIME, &ts) != 0) {
1309+
log_warn("MQTT: Failed to get clock time for %s timeout", op_name);
1310+
pthread_mutex_lock(&arg->mutex);
1311+
arg->timed_out = true;
1312+
pthread_mutex_unlock(&arg->mutex);
1313+
pthread_detach(thread);
1314+
return false;
1315+
}
1316+
ts.tv_sec += timeout_sec;
12821317

1283-
// Poll for completion with 50ms intervals
1284-
int timeout_ms = timeout_sec * 1000;
1285-
int elapsed_ms = 0;
1286-
const int poll_interval_ms = 50;
1318+
pthread_mutex_lock(&arg->mutex);
1319+
int wait_rc = 0;
1320+
while (!arg->done && wait_rc != ETIMEDOUT) {
1321+
wait_rc = pthread_cond_timedwait(&arg->cond, &arg->mutex, &ts);
1322+
}
1323+
bool completed = arg->done;
1324+
if (!completed) {
1325+
arg->timed_out = true;
1326+
}
1327+
pthread_mutex_unlock(&arg->mutex);
12871328

1288-
while (elapsed_ms < timeout_ms) {
1289-
if (done_flag) {
1290-
// Operation completed successfully
1291-
return true;
1329+
if (completed) {
1330+
// Operation completed successfully; caller reclaims resources.
1331+
if (pthread_join(thread, NULL) != 0) {
1332+
log_warn("MQTT: Failed to join thread for %s", op_name);
12921333
}
1293-
usleep(poll_interval_ms * 1000);
1294-
elapsed_ms += poll_interval_ms;
1334+
pthread_cond_destroy(&arg->cond);
1335+
pthread_mutex_destroy(&arg->mutex);
1336+
free(arg);
1337+
return true;
12951338
}
12961339

1297-
// Timeout - the thread is still running but we'll return anyway
1298-
// The thread will eventually complete (or process will exit)
1340+
// Timeout - detach so worker can finish and free its resources.
1341+
pthread_detach(thread);
1342+
12991343
// Use write() first to ensure we see the timeout even if logger is blocked
13001344
char timeout_msg[128];
1301-
snprintf(timeout_msg, sizeof(timeout_msg), "[MQTT DEBUG] %s timed out after %d seconds\n", op_name, timeout_sec);
1302-
(void)write(STDERR_FILENO, timeout_msg, strlen(timeout_msg));
1345+
int written = snprintf(timeout_msg, sizeof(timeout_msg), "[MQTT DEBUG] %s timed out after %d seconds\n", op_name, timeout_sec);
1346+
if (written >= 0) {
1347+
size_t msg_len = ((size_t)written < sizeof(timeout_msg)) ? (size_t)written : sizeof(timeout_msg) - 1;
1348+
if (msg_len > 0) {
1349+
(void)write(STDERR_FILENO, timeout_msg, msg_len);
1350+
}
1351+
}
13031352
log_warn("MQTT: %s timed out after %d seconds", op_name, timeout_sec);
13041353
return false;
13051354
}
@@ -1493,4 +1542,3 @@ int mqtt_reinit(const config_t *config) {
14931542
}
14941543

14951544
#endif /* ENABLE_MQTT */
1496-

src/video/onvif_detection.c

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,11 @@ static char *send_onvif_request(const char *url, const char *username, const cha
248248

249249
// Create full URL
250250
char full_url[512];
251-
snprintf(full_url, sizeof(full_url), "%s/onvif/%s", url, service);
251+
int written = snprintf(full_url, sizeof(full_url), "%s/onvif/%s", url, service);
252+
if (written < 0 || (size_t)written >= sizeof(full_url)) {
253+
log_error("ONVIF request URL too long or formatting failed");
254+
return NULL;
255+
}
252256

253257
return send_onvif_request_to_url(full_url, username, password, request_body, action);
254258
}
@@ -265,14 +269,19 @@ static char *extract_subscription_address(const char *response) {
265269
};
266270

267271
for (int i = 0; i < 3; i++) {
268-
const char *start = strstr(response, patterns[(ptrdiff_t)i*2]);
269-
const char *end = strstr(response, patterns[(ptrdiff_t)i*2+1]);
270-
271-
if (start && end) {
272-
start += strlen(patterns[(ptrdiff_t)i * 2]);
273-
int length = (int)(end - start);
274-
275-
return strndup(start, length);
272+
const char *open_tag = patterns[(ptrdiff_t)i * 2];
273+
const char *close_tag = patterns[(ptrdiff_t)i * 2 + 1];
274+
const char *start = strstr(response, open_tag);
275+
276+
if (start) {
277+
size_t open_tag_len = strlen(open_tag);
278+
const char *content_start = start + open_tag_len;
279+
const char *end = strstr(content_start, close_tag);
280+
281+
if (end) {
282+
size_t length = (size_t)(end - content_start);
283+
return strndup(content_start, length);
284+
}
276285
}
277286
}
278287

@@ -608,13 +617,14 @@ static bool has_motion_event(const char *response) {
608617
* Initialize the ONVIF detection system
609618
*/
610619
int init_onvif_detection_system(void) {
620+
pthread_mutex_lock(&curl_mutex);
621+
611622
if (initialized && curl_handle) {
612623
log_info("ONVIF detection system already initialized");
613-
return 0; // Already initialized and curl handle is valid
624+
pthread_mutex_unlock(&curl_mutex);
625+
return 0; // Already initialized and curl handle is present
614626
}
615627

616-
pthread_mutex_lock(&curl_mutex);
617-
618628
// If we have a curl handle but initialized is false, clean it up first
619629
if (curl_handle) {
620630
log_warn("ONVIF detection system has a curl handle but is marked as uninitialized, cleaning up");
@@ -637,15 +647,14 @@ int init_onvif_detection_system(void) {
637647
return -1;
638648
}
639649

640-
pthread_mutex_unlock(&curl_mutex);
641-
642650
// Initialize subscriptions
643651
pthread_mutex_lock(&subscription_mutex);
644652
subscription_count = 0;
645653
memset(subscriptions, 0, sizeof(subscriptions));
646654
pthread_mutex_unlock(&subscription_mutex);
647655

648656
initialized = true;
657+
pthread_mutex_unlock(&curl_mutex);
649658
log_info("ONVIF detection system initialized successfully");
650659
return 0;
651660
}
@@ -654,22 +663,22 @@ int init_onvif_detection_system(void) {
654663
* Shutdown the ONVIF detection system
655664
*/
656665
void shutdown_onvif_detection_system(void) {
666+
pthread_mutex_lock(&curl_mutex);
657667
log_info("Shutting down ONVIF detection system (initialized: %s, curl_handle: %p)",
658668
initialized ? "yes" : "no", (void*)curl_handle);
659669

660670
// Cleanup curl handle if it exists
661-
pthread_mutex_lock(&curl_mutex);
662671
if (curl_handle) {
663672
log_info("Cleaning up curl handle");
664673
curl_easy_cleanup(curl_handle);
665674
curl_handle = NULL;
666675
}
667-
pthread_mutex_unlock(&curl_mutex);
668676

669677
// Note: Don't call curl_global_cleanup() here - it's managed centrally in curl_init.c
670678
// The global cleanup will happen at program shutdown
671679

672680
initialized = false;
681+
pthread_mutex_unlock(&curl_mutex);
673682
log_info("ONVIF detection system shutdown complete");
674683
}
675684

@@ -763,13 +772,18 @@ int detect_motion_onvif(const char *onvif_url, const char *username, const char
763772

764773
if (!response) {
765774
log_error("Failed to pull messages from subscription");
766-
767-
// If pulling messages fails, the subscription might be invalid
768-
// Mark it as inactive so we'll create a new one next time
775+
776+
// If pulling messages fails, the subscription might be invalid.
777+
// Re-lookup by URL while holding the mutex to avoid stale pointer use.
769778
pthread_mutex_lock(&subscription_mutex);
770-
subscription->active = false;
779+
for (int i = 0; i < subscription_count; i++) {
780+
if (strcmp(subscriptions[i].camera_url, onvif_url) == 0) {
781+
subscriptions[i].active = false;
782+
break;
783+
}
784+
}
771785
pthread_mutex_unlock(&subscription_mutex);
772-
786+
773787
return -1;
774788
}
775789

0 commit comments

Comments
 (0)