Skip to content

Commit bc6591e

Browse files
Merge pull request #58 from mr-light-show/fix/reconnect-playlist-advance-race
Fix heap corruption when reconnect races playlist advance
2 parents ca333ef + ce6f711 commit bc6591e

6 files changed

Lines changed: 166 additions & 16 deletions

File tree

src/bar_state.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,24 @@ void BarStateSetPlaylist(BarApp_t *app, PianoSong_t *playlist) {
264264
BarStateSignalPlaybackManager(app);
265265
}
266266

267+
/* Advance playlist (pop head for history) (thread-safe)
268+
* Returns the finished song detached from the list, or NULL if empty.
269+
*/
270+
PianoSong_t *BarStateAdvancePlaylist(BarApp_t *app) {
271+
assert(app != NULL);
272+
273+
PianoSong_t *finished = NULL;
274+
WITH_STATE_LOCK(app, "AdvancePlaylist", NULL) {
275+
if (app->playlist != NULL) {
276+
finished = app->playlist;
277+
app->playlist = PianoListNextP(app->playlist);
278+
finished->head.next = NULL;
279+
}
280+
}
281+
BarStateSignalPlaybackManager(app);
282+
return finished;
283+
}
284+
267285
/* Drain playlist (destroy and clear) (thread-safe)
268286
*/
269287
void BarStateDrainPlaylist(BarApp_t *app) {

src/bar_state.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ PianoStation_t *BarStateGetStationList(const BarApp_t *app);
185185
/* Playlist access (thread-safe) */
186186
PianoSong_t *BarStateGetPlaylist(const BarApp_t *app);
187187
void BarStateSetPlaylist(BarApp_t *app, PianoSong_t *playlist);
188+
/* Pop head song for history; NULL if playlist empty or already drained. */
189+
PianoSong_t *BarStateAdvancePlaylist(BarApp_t *app);
188190
void BarStateDrainPlaylist(BarApp_t *app);
189191
void BarStateSwitchStation(BarApp_t *app, PianoStation_t *station);
190192

src/main.c

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,11 @@ static void BarMainLoop (BarApp_t *app) {
524524
* song */
525525
if (BarPlayerGetMode (player) == PLAYER_DEAD) {
526526
/* what's next? */
527-
PianoSong_t *playlist = BarStateGetPlaylist(app);
528-
if (playlist != NULL) {
529-
PianoSong_t *histsong = playlist;
530-
BarStateSetPlaylist(app, PianoListNextP (playlist));
531-
histsong->head.next = NULL;
532-
BarUiHistoryPrepend (app, histsong);
527+
PianoSong_t *finished = BarStateAdvancePlaylist(app);
528+
if (finished != NULL) {
529+
BarUiHistoryPrepend (app, finished);
533530
}
534-
playlist = BarStateGetPlaylist(app);
531+
PianoSong_t *playlist = BarStateGetPlaylist(app);
535532
PianoStation_t *nextStation = BarStateGetNextStation(app);
536533
if (playlist == NULL && nextStation != NULL && !app->doQuit) {
537534
PianoStation_t *curStation = BarStateGetCurrentStation(app);

src/playback_manager.c

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,17 +270,14 @@ static void *BarPlaybackManagerThread(void *data) {
270270
log_write(DEBUG_UI, "PlaybackMgr: Player idle\n");
271271
}
272272

273-
/* Advance playlist */
274-
PianoSong_t *playlist = BarStateGetPlaylist(app);
275-
if (playlist != NULL) {
276-
PianoSong_t *histsong = playlist;
277-
BarStateSetPlaylist(app, PianoListNextP(playlist));
278-
histsong->head.next = NULL;
279-
BarUiHistoryPrepend(app, histsong);
273+
/* Advance playlist (atomic under state lock — safe vs DrainPlaylist) */
274+
PianoSong_t *finished = BarStateAdvancePlaylist(app);
275+
if (finished != NULL) {
276+
BarUiHistoryPrepend(app, finished);
280277
}
281-
278+
282279
/* Fetch more songs if needed */
283-
playlist = BarStateGetPlaylist(app);
280+
PianoSong_t *playlist = BarStateGetPlaylist(app);
284281
PianoStation_t *nextStation = BarStateGetNextStation(app);
285282

286283
if (playlist == NULL && nextStation != NULL && !app->doQuit) {

test/unit/test_bar_state.c

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,104 @@ START_TEST(test_bar_state_drain_playlist) {
331331
}
332332
END_TEST
333333

334+
START_TEST(test_bar_state_advance_playlist) {
335+
BarApp_t app;
336+
PianoSong_t song1, song2;
337+
memset(&song1, 0, sizeof(song1));
338+
memset(&song2, 0, sizeof(song2));
339+
song1.head.next = &song2.head;
340+
song1.title = (char *)"first";
341+
song2.title = (char *)"second";
342+
bar_state_test_setup(&app, BAR_UI_MODE_WEB);
343+
344+
BarStateSetPlaylist(&app, &song1);
345+
346+
PianoSong_t *finished = BarStateAdvancePlaylist(&app);
347+
ck_assert_ptr_eq(finished, &song1);
348+
ck_assert_ptr_null(finished->head.next);
349+
ck_assert_ptr_eq(BarStateGetPlaylist(&app), &song2);
350+
351+
finished = BarStateAdvancePlaylist(&app);
352+
ck_assert_ptr_eq(finished, &song2);
353+
ck_assert_ptr_null(BarStateGetPlaylist(&app));
354+
355+
finished = BarStateAdvancePlaylist(&app);
356+
ck_assert_ptr_null(finished);
357+
358+
bar_state_test_teardown(&app);
359+
}
360+
END_TEST
361+
362+
START_TEST(test_bar_state_advance_playlist_both) {
363+
BarApp_t app;
364+
PianoSong_t song;
365+
memset(&song, 0, sizeof(song));
366+
song.title = (char *)"both-mode";
367+
bar_state_test_setup(&app, BAR_UI_MODE_BOTH);
368+
369+
BarStateSetPlaylist(&app, &song);
370+
PianoSong_t *finished = BarStateAdvancePlaylist(&app);
371+
ck_assert_ptr_eq(finished, &song);
372+
ck_assert_ptr_null(BarStateGetPlaylist(&app));
373+
374+
bar_state_test_teardown(&app);
375+
}
376+
END_TEST
377+
378+
typedef struct {
379+
BarApp_t *app;
380+
int iterations;
381+
} bar_state_playlist_thread_args_t;
382+
383+
static void *advance_playlist_worker(void *arg) {
384+
const bar_state_playlist_thread_args_t *args = arg;
385+
for (int i = 0; i < args->iterations; ++i) {
386+
PianoSong_t *finished = BarStateAdvancePlaylist(args->app);
387+
(void) finished;
388+
}
389+
return NULL;
390+
}
391+
392+
static void *drain_playlist_worker(void *arg) {
393+
const bar_state_playlist_thread_args_t *args = arg;
394+
for (int i = 0; i < args->iterations; ++i) {
395+
BarStateDrainPlaylist(args->app);
396+
}
397+
return NULL;
398+
}
399+
400+
/* Regression: reconnect/disconnect DrainPlaylist vs playback-manager advance. */
401+
START_TEST(test_bar_state_advance_playlist_race_with_drain) {
402+
BarApp_t app;
403+
bar_state_test_setup(&app, BAR_UI_MODE_WEB);
404+
405+
for (int round = 0; round < 50; ++round) {
406+
PianoSong_t *s1 = calloc(1, sizeof(*s1));
407+
PianoSong_t *s2 = calloc(1, sizeof(*s2));
408+
ck_assert_ptr_nonnull(s1);
409+
ck_assert_ptr_nonnull(s2);
410+
s1->head.next = &s2->head;
411+
BarStateSetPlaylist(&app, s1);
412+
413+
bar_state_playlist_thread_args_t args = { .app = &app, .iterations = 200 };
414+
pthread_t advance_tid;
415+
pthread_t drain_tid;
416+
ck_assert_int_eq(pthread_create(&advance_tid, NULL,
417+
advance_playlist_worker, &args), 0);
418+
ck_assert_int_eq(pthread_create(&drain_tid, NULL,
419+
drain_playlist_worker, &args), 0);
420+
ck_assert_int_eq(pthread_join(advance_tid, NULL), 0);
421+
ck_assert_int_eq(pthread_join(drain_tid, NULL), 0);
422+
423+
if (BarStateGetPlaylist(&app) != NULL) {
424+
BarStateDrainPlaylist(&app);
425+
}
426+
}
427+
428+
bar_state_test_teardown(&app);
429+
}
430+
END_TEST
431+
334432
/* SwitchStation: drains playlist and sets nextStation (test with playlist NULL) */
335433
START_TEST(test_bar_state_switch_station) {
336434
BarApp_t app;
@@ -657,6 +755,9 @@ Suite *bar_state_suite(void) {
657755
tcase_add_test(tc_playlist, test_bar_state_playlist_get_set);
658756
tcase_add_test(tc_playlist, test_bar_state_playlist_get_set_web);
659757
tcase_add_test(tc_playlist, test_bar_state_drain_playlist);
758+
tcase_add_test(tc_playlist, test_bar_state_advance_playlist);
759+
tcase_add_test(tc_playlist, test_bar_state_advance_playlist_both);
760+
tcase_add_test(tc_playlist, test_bar_state_advance_playlist_race_with_drain);
660761
tcase_add_test(tc_playlist, test_bar_state_switch_station);
661762
suite_add_tcase(s, tc_playlist);
662763

test/unit/test_playback_manager.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,40 @@ START_TEST(test_manager_thread_one_loop_iteration) {
193193
}
194194
END_TEST
195195

196+
/* Covers idle PLAYER_DEAD path: BarStateAdvancePlaylist + BarUiHistoryPrepend. */
197+
START_TEST(test_manager_idle_advances_playlist) {
198+
BarApp_t app;
199+
PianoSong_t song;
200+
201+
memset(&song, 0, sizeof(song));
202+
song.title = (char *)"idle-advance";
203+
204+
memset(&app, 0, sizeof(app));
205+
app.settings.uiMode = BAR_UI_MODE_WEB;
206+
app.settings.history = 10;
207+
BarStateInit(&app);
208+
pthread_mutex_init(&app.player.lock, NULL);
209+
pthread_cond_init(&app.player.cond, NULL);
210+
app.player.mode = PLAYER_DEAD;
211+
BarStateSetPlaylist(&app, &song);
212+
213+
ck_assert(BarPlaybackManagerStart(&app));
214+
pthread_mutex_lock(&app.player.lock);
215+
pthread_cond_broadcast(&app.player.cond);
216+
pthread_mutex_unlock(&app.player.lock);
217+
usleep(100000);
218+
219+
BarPlaybackManagerStop(&app);
220+
221+
ck_assert_ptr_null(BarStateGetPlaylist(&app));
222+
ck_assert_ptr_eq(app.songHistory, &song);
223+
224+
pthread_mutex_destroy(&app.player.lock);
225+
pthread_cond_destroy(&app.player.cond);
226+
BarStateDestroy(&app);
227+
}
228+
END_TEST
229+
196230
START_TEST(test_should_park_idle_when_dead_and_empty) {
197231
BarApp_t app;
198232
memset(&app, 0, sizeof(app));
@@ -301,6 +335,7 @@ Suite *playback_manager_suite(void) {
301335
tcase_add_test(tc, test_complete_song_cleanup_interrupt_on_quit);
302336
tcase_add_test(tc, test_complete_song_cleanup_no_interrupt_log_when_not_quitting);
303337
tcase_add_test(tc, test_manager_thread_one_loop_iteration);
338+
tcase_add_test(tc, test_manager_idle_advances_playlist);
304339
tcase_add_test(tc, test_should_park_idle_when_dead_and_empty);
305340
tcase_add_test(tc, test_should_not_park_when_next_station_set);
306341
tcase_add_test(tc, test_should_not_park_when_not_dead);

0 commit comments

Comments
 (0)