output/alsa: implement Interrupt()

This allows canceling the blocking method LockWaitWriteAvailable(),
and thus allows breaking free of misbehaving ALSA drivers, avoiding a
MPD lockup.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/966
This commit is contained in:
Max Kellermann 2020-10-01 15:24:52 +02:00
parent 4cb5e69811
commit 4ad525d939
2 changed files with 49 additions and 0 deletions

2
NEWS
View File

@ -1,4 +1,6 @@
ver 0.22.1 (not yet released) ver 0.22.1 (not yet released)
* output
- alsa: don't deadlock when the ALSA driver is buggy
ver 0.22 (2020/09/23) ver 0.22 (2020/09/23)
* protocol * protocol

View File

@ -25,6 +25,7 @@
#include "lib/alsa/PeriodBuffer.hxx" #include "lib/alsa/PeriodBuffer.hxx"
#include "lib/alsa/Version.hxx" #include "lib/alsa/Version.hxx"
#include "../OutputAPI.hxx" #include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/MixerList.hxx" #include "mixer/MixerList.hxx"
#include "pcm/Export.hxx" #include "pcm/Export.hxx"
#include "system/PeriodClock.hxx" #include "system/PeriodClock.hxx"
@ -177,6 +178,15 @@ class AlsaOutput final
bool drain; bool drain;
/**
* Was Interrupt() called? This will unblock
* LockWaitWriteAvailable(). It will be reset by Cancel() and
* Pause(), as documented by the #AudioOutput interface.
*
* Only initialized while the output is open.
*/
bool interrupted;
/** /**
* This buffer gets allocated after opening the ALSA device. * This buffer gets allocated after opening the ALSA device.
* It contains silence samples, enough to fill one period (see * It contains silence samples, enough to fill one period (see
@ -237,9 +247,12 @@ private:
void Open(AudioFormat &audio_format) override; void Open(AudioFormat &audio_format) override;
void Close() noexcept override; void Close() noexcept override;
void Interrupt() noexcept override;
size_t Play(const void *chunk, size_t size) override; size_t Play(const void *chunk, size_t size) override;
void Drain() override; void Drain() override;
void Cancel() noexcept override; void Cancel() noexcept override;
bool Pause() noexcept override;
/** /**
* Set up the snd_pcm_t object which was opened by the caller. * Set up the snd_pcm_t object which was opened by the caller.
@ -728,6 +741,7 @@ AlsaOutput::Open(AudioFormat &audio_format)
out_frame_size = pcm_export->GetOutputFrameSize(); out_frame_size = pcm_export->GetOutputFrameSize();
drain = false; drain = false;
interrupted = false;
size_t period_size = period_frames * out_frame_size; size_t period_size = period_frames * out_frame_size;
ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(period_size * 4); ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(period_size * 4);
@ -741,6 +755,18 @@ AlsaOutput::Open(AudioFormat &audio_format)
error = {}; error = {};
} }
void
AlsaOutput::Interrupt() noexcept
{
std::unique_lock<Mutex> lock(mutex);
/* the "interrupted" flag will prevent
LockWaitWriteAvailable() from actually waiting, and will
instead throw AudioOutputInterrupted */
interrupted = true;
cond.notify_one();
}
inline int inline int
AlsaOutput::Recover(int err) noexcept AlsaOutput::Recover(int err) noexcept
{ {
@ -912,6 +938,11 @@ AlsaOutput::CancelInternal() noexcept
void void
AlsaOutput::Cancel() noexcept AlsaOutput::Cancel() noexcept
{ {
{
std::unique_lock<Mutex> lock(mutex);
interrupted = false;
}
if (!LockIsActive()) { if (!LockIsActive()) {
/* early cancel, quick code path without thread /* early cancel, quick code path without thread
synchronization */ synchronization */
@ -928,6 +959,17 @@ AlsaOutput::Cancel() noexcept
}); });
} }
bool
AlsaOutput::Pause() noexcept
{
std::unique_lock<Mutex> lock(mutex);
interrupted = false;
/* not implemented - this override exists only to reset the
"interrupted" flag */
return false;
}
void void
AlsaOutput::Close() noexcept AlsaOutput::Close() noexcept
{ {
@ -956,6 +998,11 @@ AlsaOutput::LockWaitWriteAvailable()
if (error) if (error)
std::rethrow_exception(error); std::rethrow_exception(error);
if (interrupted)
/* a CANCEL command is in flight - don't block
here */
throw AudioOutputInterrupted{};
size_t write_available = ring_buffer->write_available(); size_t write_available = ring_buffer->write_available();
if (write_available >= min_available) { if (write_available >= min_available) {
/* reserve room for one extra block, just in /* reserve room for one extra block, just in