From 77c2e4fbe597ecbd6910743c586459bdf222bd33 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 24 Aug 2026 17:07:07 -0400 Subject: [PATCH 1/3] playlist: allow enumerator errors to propagate SongEnumerator::NextSong() can throw while reading a remote playlist. The provider helpers are called by functions that already propagate errors, but their noexcept specifiers turn these failures into std::terminate. Remove noexcept so the command handler can report the I/O error. --- src/playlist/Length.cxx | 2 +- src/playlist/Print.cxx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/playlist/Length.cxx b/src/playlist/Length.cxx index 79c9376eb5..55e21f0fdf 100644 --- a/src/playlist/Length.cxx +++ b/src/playlist/Length.cxx @@ -28,7 +28,7 @@ static void playlist_provider_length(Response &r, const SongLoader &loader, const char *uri, - SongEnumerator &e) noexcept + SongEnumerator &e) { const auto base_uri = uri != nullptr ? PathTraitsUTF8::GetParent(uri) diff --git a/src/playlist/Print.cxx b/src/playlist/Print.cxx index 2947d007e5..898d8aa735 100644 --- a/src/playlist/Print.cxx +++ b/src/playlist/Print.cxx @@ -27,7 +27,7 @@ playlist_provider_print(Response &r, SongEnumerator &e, unsigned start_index, unsigned end_index, - bool detail) noexcept + bool detail) { const auto base_uri = uri != nullptr ? PathTraitsUTF8::GetParent(uri) @@ -61,7 +61,7 @@ playlist_provider_search_print(Response &r, SongEnumerator &e, unsigned start_index, unsigned end_index, - SongFilter *filter) noexcept + SongFilter *filter) { const auto base_uri = uri != nullptr ? PathTraitsUTF8::GetParent(uri) From a29eb8930a9a56e29cb719161f36f5082501c68a Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 24 Aug 2026 17:07:07 -0400 Subject: [PATCH 2/3] input/alsa: lock mutex when storing stream errors DispatchSockets() writes postponed_exception from its catch handler without holding the input stream mutex. A synchronous reader can access the same state after it is notified. Acquire the mutex before storing the exception and notifying readers. --- src/input/plugins/AlsaInputPlugin.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/input/plugins/AlsaInputPlugin.cxx b/src/input/plugins/AlsaInputPlugin.cxx index 5fc8dd99af..19534135f3 100644 --- a/src/input/plugins/AlsaInputPlugin.cxx +++ b/src/input/plugins/AlsaInputPlugin.cxx @@ -244,6 +244,7 @@ try { CommitWriteBuffer(nbytes); } catch (...) { + const std::lock_guard protect{mutex}; postponed_exception = std::current_exception(); InvokeOnAvailable(); } From 6785a5412a4c66f94e311276496d331d5d4fccc7 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 24 Aug 2026 17:07:07 -0400 Subject: [PATCH 3/3] input: wake synchronous callers on availability AsyncInputStream::Read() waits on caller_cond, but several asynchronous EOF and error paths only notify the InputStream handler. Since bd7830794, the synchronous caller is not the installed handler and can remain blocked forever. Wrap InvokeOnAvailable() so every availability notification also wakes caller_cond. --- src/input/AsyncInputStream.cxx | 13 ++----------- src/input/AsyncInputStream.hxx | 8 ++++++++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/input/AsyncInputStream.cxx b/src/input/AsyncInputStream.cxx index 0701d6f4ea..1eae5f6230 100644 --- a/src/input/AsyncInputStream.cxx +++ b/src/input/AsyncInputStream.cxx @@ -141,7 +141,6 @@ AsyncInputStream::SeekDone() noexcept open = true; seek_state = SeekState::NONE; - caller_cond.notify_one(); InvokeOnAvailable(); } @@ -207,10 +206,8 @@ AsyncInputStream::CommitWriteBuffer(size_t nbytes) noexcept if (!IsReady()) SetReady(); - else { - caller_cond.notify_one(); + else InvokeOnAvailable(); - } } void @@ -240,10 +237,8 @@ AsyncInputStream::AppendToBuffer(std::span src) noexcept if (!IsReady()) SetReady(); - else { - caller_cond.notify_one(); + else InvokeOnAvailable(); - } } void @@ -254,7 +249,6 @@ AsyncInputStream::DeferredResume() noexcept if (postponed_exception) [[unlikely]] { /* do not proceed, first the caller must handle the pending error */ - caller_cond.notify_one(); InvokeOnAvailable(); return; } @@ -263,7 +257,6 @@ AsyncInputStream::DeferredResume() noexcept Resume(); } catch (...) { postponed_exception = std::current_exception(); - caller_cond.notify_one(); InvokeOnAvailable(); } } @@ -279,7 +272,6 @@ AsyncInputStream::DeferredSeek() noexcept /* do not proceed, first the caller must handle the pending error */ seek_state = SeekState::NONE; - caller_cond.notify_one(); InvokeOnAvailable(); return; } @@ -295,7 +287,6 @@ AsyncInputStream::DeferredSeek() noexcept } catch (...) { seek_state = SeekState::NONE; postponed_exception = std::current_exception(); - caller_cond.notify_one(); InvokeOnAvailable(); } } diff --git a/src/input/AsyncInputStream.hxx b/src/input/AsyncInputStream.hxx index 4135efce89..c81dae6747 100644 --- a/src/input/AsyncInputStream.hxx +++ b/src/input/AsyncInputStream.hxx @@ -56,6 +56,14 @@ class AsyncInputStream : public InputStream { protected: std::exception_ptr postponed_exception; + /** + * Notify both the synchronous caller and the InputStream handler. + */ + void InvokeOnAvailable() noexcept { + caller_cond.notify_one(); + InputStream::InvokeOnAvailable(); + } + public: AsyncInputStream(EventLoop &event_loop, std::string_view _url, Mutex &_mutex,