diff --git a/NEWS b/NEWS
index 10d8aaf37f..663096a682 100644
--- a/NEWS
+++ b/NEWS
@@ -34,6 +34,8 @@ ver 0.25 (not yet released)
ver 0.24.14 (not yet released)
* storage
- curl: bound WebDAV PROPFIND responses
+* storage
+ - nfs: fix use-after-free bug after timeout
* input
- qobuz: use HTTPS for API requests
* decoder
@@ -44,8 +46,16 @@ ver 0.24.14 (not yet released)
- mad: ignore implausible Xing frame counts
- modplug, openmpt: fix error handling bug
* output
+ - alsa: remove logging calls from the real-time thread
+ - alsa: remove option "thesycon_dsd_workaround"
+ - alsa: fix corruption bug with "stop_dsd_silence"
- osx: fix format selection bugs
- osx: fix volume truncation
+ - pipewire: remove logging calls from the real-time thread
+ - pipewire: fix uninitialized variable
+ - pipewire: fix thread-safety bugs
+ - pipewire: fix deadlock bug
+ - pipewire: fix ring buffer corruption bug
* Windows
- enable the "mpg123" decoder plugin
- fix shutdown in console mode
diff --git a/doc/plugins.rst b/doc/plugins.rst
index 7772fe4fef..528815186b 100644
--- a/doc/plugins.rst
+++ b/doc/plugins.rst
@@ -943,11 +943,6 @@ The `Advanced Linux Sound Architecture (ALSA) `_ p
("stop" or "pause") in DSD mode (native DSD or DoP). This is a
workaround for some DACs which emit noise when stopping DSD
playback.
- * - **thesycon_dsd_workaround yes|no**
- - If enabled, enables a workaround for a bug in Thesycon USB
- audio receivers. On these devices, playing DSD512 or PCM
- causes all subsequent attempts to play other DSD rates to fail,
- which can be fixed by briefly playing PCM at 44.1 kHz.
* - **allowed_formats F1 F2 ...**
- Specifies a list of allowed audio formats, separated by a space. All items may contain asterisks as a wild card, and may be followed by "=dop" to enable DoP (DSD over PCM) for this particular format. The first matching format is used, and if none matches, MPD chooses the best fallback of this list.
diff --git a/src/lib/nfs/Base.cxx b/src/lib/nfs/Base.cxx
index 3faf4c90a0..ea851c1347 100644
--- a/src/lib/nfs/Base.cxx
+++ b/src/lib/nfs/Base.cxx
@@ -2,13 +2,13 @@
// Copyright The Music Player Daemon Project
#include "Base.hxx"
+#include "util/StringAPI.hxx"
+#include "util/StringCompare.hxx"
#include // for std::copy()
#include
#include
-#include
-
static std::array nfs_base_server;
static std::array nfs_base_export_name;
static size_t nfs_base_export_name_length;
@@ -33,9 +33,8 @@ nfs_check_base(const char *server, const char *path) noexcept
assert(server != nullptr);
assert(path != nullptr);
- return strcmp(nfs_base_server.data(), server) == 0 &&
- memcmp(nfs_base_export_name.data(), path,
- nfs_base_export_name_length) == 0 &&
+ return StringIsEqual(nfs_base_server.data(), server) &&
+ StringStartsWith(path, {nfs_base_export_name.data(), nfs_base_export_name_length}) &&
(path[nfs_base_export_name_length] == 0 ||
path[nfs_base_export_name_length] == '/')
? path + nfs_base_export_name_length
diff --git a/src/lib/nfs/Blocking.cxx b/src/lib/nfs/Blocking.cxx
index 5ac20ffe48..c8c3def538 100644
--- a/src/lib/nfs/Blocking.cxx
+++ b/src/lib/nfs/Blocking.cxx
@@ -14,8 +14,11 @@ BlockingNfsOperation::Run()
[this](){ connection.AddLease(*this); });
/* wait for completion */
- if (!LockWaitFinished())
+ if (!LockWaitFinished()) {
+ BlockingCall(connection.GetEventLoop(),
+ [this](){ connection.RemoveLease(*this); });
throw std::runtime_error("Timeout");
+ }
/* check for error */
if (error)
diff --git a/src/lib/nfs/Error.cxx b/src/lib/nfs/Error.cxx
index 5ec812c287..b8fe7b8974 100644
--- a/src/lib/nfs/Error.cxx
+++ b/src/lib/nfs/Error.cxx
@@ -34,7 +34,7 @@ FormatNfsClientError(int err, struct nfs_context *nfs, void *data,
const char *msg2 = (const char *)data;
if (data == nullptr || *(const char *)data == 0) {
msg2 = nfs_get_error(nfs);
- if (msg2 == nullptr)
+ if (msg2 == nullptr || *msg2 == 0)
msg2 = strerror(-err);
}
diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx
index 7587324531..101cd5af27 100644
--- a/src/output/plugins/AlsaOutputPlugin.cxx
+++ b/src/output/plugins/AlsaOutputPlugin.cxx
@@ -25,6 +25,7 @@
#include "event/InjectEvent.hxx"
#include "event/FineTimerEvent.hxx"
#include "event/Call.hxx"
+#include "event/Loop.hxx"
#include "util/RingBuffer.hxx"
#include "Log.hxx"
@@ -164,16 +165,6 @@ class AlsaOutput final
* Are we currently draining with #stop_dsd_silence?
*/
bool in_stop_dsd_silence;
-
- /**
- * Enable the DSD sync workaround for Thesycon USB audio
- * receivers? On this device, playing DSD512 or PCM causes
- * all subsequent attempts to play other DSD rates to fail,
- * which can be fixed by briefly playing PCM at 44.1 kHz.
- */
- const bool thesycon_dsd_workaround;
-
- bool need_thesycon_dsd_workaround = thesycon_dsd_workaround;
#endif
/**
@@ -234,6 +225,13 @@ class AlsaOutput final
std::atomic_bool paused;
bool hw_can_pause = false;
+ /**
+ * Set to true from a real-time thread to ask the output
+ * thread to log an xrun which required the producer to
+ * generate silence.
+ */
+ std::atomic_bool silence_inserted;
+
public:
AlsaOutput(EventLoop &loop, const ConfigBlock &block);
@@ -371,8 +369,12 @@ class AlsaOutput final
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept;
void LockCaughtError() noexcept {
+ assert(GetEventLoop().IsInside());
+
period_buffer.Clear();
+ silence_timer.Cancel();
+
const std::lock_guard lock{mutex};
error = std::current_exception();
active = false;
@@ -446,8 +448,6 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
/* legacy name from MPD 0.18 and older: */
block.GetBlockValue("dsd_usb", false)),
stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)),
- thesycon_dsd_workaround(block.GetBlockValue("thesycon_dsd_workaround",
- false)),
#endif
close_on_pause(block.GetBlockValue("close_on_pause", true))
{
@@ -678,101 +678,11 @@ BestMatch(const std::forward_list &haystack,
return haystack.front();
}
-#ifdef ENABLE_DSD
-
-static void
-Play_44_1_Silence(snd_pcm_t *pcm)
-{
- snd_pcm_hw_params_t *hw;
- snd_pcm_hw_params_alloca(&hw);
-
- int err;
-
- err = snd_pcm_hw_params_any(pcm, hw);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
-
- err = snd_pcm_hw_params_set_access(pcm, hw,
- SND_PCM_ACCESS_RW_INTERLEAVED);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
-
- err = snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_set_format() failed");
-
- unsigned channels = 1;
- err = snd_pcm_hw_params_set_channels_near(pcm, hw, &channels);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_set_channels_near() failed");
-
- constexpr snd_pcm_uframes_t rate = 44100;
- err = snd_pcm_hw_params_set_rate(pcm, hw, rate, 0);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_set_rate() failed");
-
- snd_pcm_uframes_t buffer_size = 1;
- err = snd_pcm_hw_params_set_buffer_size_near(pcm, hw, &buffer_size);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_size_near() failed");
-
- snd_pcm_uframes_t period_size = 1;
- int dir = 0;
- err = snd_pcm_hw_params_set_period_size_near(pcm, hw, &period_size,
- &dir);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_size_near() failed");
-
- err = snd_pcm_hw_params(pcm, hw);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
-
- snd_pcm_sw_params_t *sw;
- snd_pcm_sw_params_alloca(&sw);
-
- err = snd_pcm_sw_params_current(pcm, sw);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed");
-
- err = snd_pcm_sw_params_set_start_threshold(pcm, sw, period_size);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed");
-
- err = snd_pcm_sw_params(pcm, sw);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
-
- err = snd_pcm_prepare(pcm);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_prepare() failed");
-
- AllocatedArray buffer{channels * period_size};
- buffer = std::span{};
-
- /* play at least 250ms of silence */
- for (snd_pcm_uframes_t remaining_frames = rate / 4;;) {
- auto n = snd_pcm_writei(pcm, buffer.data(),
- period_size);
- if (n < 0)
- throw Alsa::MakeError(err, "snd_pcm_writei() failed");
-
- if (snd_pcm_uframes_t(n) >= remaining_frames)
- break;
-
- remaining_frames -= snd_pcm_uframes_t(n);
- }
-
- err = snd_pcm_drain(pcm);
- if (err < 0)
- throw Alsa::MakeError(err, "snd_pcm_drain() failed");
-}
-
-#endif
-
void
AlsaOutput::Open(AudioFormat &audio_format)
{
paused = false;
+ silence_inserted.store(false, std::memory_order_relaxed);
#ifdef ENABLE_DSD
bool dop;
@@ -809,22 +719,6 @@ AlsaOutput::Open(AudioFormat &audio_format)
pcm_name,
snd_pcm_type_name(snd_pcm_type(pcm)));
-#ifdef ENABLE_DSD
- if (need_thesycon_dsd_workaround &&
- audio_format.format == SampleFormat::DSD &&
- audio_format.sample_rate <= 256 * 44100 / 8) {
- LogDebug(alsa_output_domain, "Playing some 44.1 kHz silence");
-
- try {
- Play_44_1_Silence(pcm);
- } catch (...) {
- LogError(std::current_exception());
- }
-
- need_thesycon_dsd_workaround = false;
- }
-#endif
-
PcmExport::Params params;
try {
@@ -845,11 +739,6 @@ AlsaOutput::Open(AudioFormat &audio_format)
use_dsd = audio_format.format == SampleFormat::DSD;
in_stop_dsd_silence = false;
- if (thesycon_dsd_workaround &&
- (!use_dsd ||
- audio_format.sample_rate > 256 * 44100 / 8))
- need_thesycon_dsd_workaround = true;
-
if (params.dsd_mode == PcmExport::DsdMode::DOP)
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
#endif
@@ -1082,7 +971,12 @@ AlsaOutput::Drain()
Activate();
- cond.wait(lock, [this]{ return !drain || !active; });
+ cond.wait(lock, [this]{ return !drain || !active || interrupted; });
+
+ /* the stream is discontinuous now; discard the incomplete
+ block which may still be inside the PcmExport instance,
+ because it would otherwise be prepended to the next song */
+ pcm_export->Reset();
if (error)
std::rethrow_exception(error);
@@ -1135,6 +1029,7 @@ AlsaOutput::Cancel() noexcept
in_stop_dsd_silence = true;
drain = true;
cond.wait(lock, [this]{ return !drain || !active; });
+ pcm_export->Reset();
return;
}
#endif
@@ -1257,6 +1152,13 @@ AlsaOutput::Play(std::span src)
assert(!src.empty());
assert(src.size() % in_frame_size == 0);
+ if (silence_inserted.load(std::memory_order_relaxed)) {
+ silence_inserted.store(false, std::memory_order_relaxed);
+
+ if (throttle_silence_log.CheckUpdate(std::chrono::seconds(5)))
+ LogWarning(alsa_output_domain, "Decoder is too slow; playing silence to avoid xrun");
+ }
+
const bool was_paused = paused.exchange(false);
if (was_paused) {
@@ -1388,8 +1290,9 @@ try {
return;
}
- if (throttle_silence_log.CheckUpdate(std::chrono::seconds(5)))
- LogWarning(alsa_output_domain, "Decoder is too slow; playing silence to avoid xrun");
+ /* this is a real-time thread, so we must not log
+ here; let the output thread do it */
+ silence_inserted.store(true, std::memory_order_relaxed);
/* insert some silence if the buffer has not enough
data yet, to avoid ALSA xrun */
diff --git a/src/output/plugins/PipeWireOutputPlugin.cxx b/src/output/plugins/PipeWireOutputPlugin.cxx
index 475692ca87..8001a825ca 100644
--- a/src/output/plugins/PipeWireOutputPlugin.cxx
+++ b/src/output/plugins/PipeWireOutputPlugin.cxx
@@ -11,6 +11,7 @@
#include "pcm/Silence.hxx"
#include "lib/fmt/ExceptionFormatter.hxx"
#include "system/Error.hxx"
+#include "time/PeriodClock.hxx"
#include "util/BitReverse.hxx"
#include "util/Domain.hxx"
#include "util/RingBuffer.hxx"
@@ -38,6 +39,7 @@
#include
#include
+#include
#include
#include
#include
@@ -53,6 +55,12 @@ class PipeWireOutput final : AudioOutput {
struct pw_thread_loop *thread_loop = nullptr;
struct pw_stream *stream;
+ /**
+ * If #disconnected, this contains a human-readable
+ * description of the problem. Used by CheckThrowError().
+ *
+ * Protected by #thread_loop's lock.
+ */
std::string error_message;
std::byte pod_buffer[1024];
@@ -144,6 +152,15 @@ class PipeWireOutput final : AudioOutput {
bool drained;
+ /**
+ * Set to true from a real-time thread to ask the output
+ * thread to log an xrun which required the producer to
+ * generate silence.
+ */
+ std::atomic_bool silence_inserted;
+
+ PeriodClock throttle_silence_log;
+
explicit PipeWireOutput(const ConfigBlock &block);
public:
@@ -165,6 +182,9 @@ class PipeWireOutput final : AudioOutput {
}
private:
+ /**
+ * Caller must lock the #thread_loop.
+ */
void CheckThrowError() {
if (disconnected) {
if (error_message.empty())
@@ -352,7 +372,12 @@ PipeWireOutput::Enable()
if (thread_loop == nullptr)
throw MakeErrno("pw_thread_loop_new() failed");
- pw_thread_loop_start(thread_loop);
+ if (int error = pw_thread_loop_start(thread_loop); error < 0) {
+ pw_thread_loop_destroy(thread_loop);
+ thread_loop = nullptr;
+
+ throw PipeWire::MakeError(error, "pw_thread_loop_start() failed");
+ }
stream = nullptr;
}
@@ -486,6 +511,7 @@ PipeWireOutput::Open(AudioFormat &audio_format)
restore_volume = true;
paused = false;
+ silence_inserted.store(false, std::memory_order_relaxed);
/* stay inactive (PW_STREAM_FLAG_INACTIVE) until the ring
buffer has been filled */
@@ -500,6 +526,8 @@ PipeWireOutput::Open(AudioFormat &audio_format)
PW_KEY_APP_NAME, "Music Player Daemon",
PW_KEY_APP_ICON_NAME, "mpd",
nullptr);
+ if (props == nullptr)
+ throw MakeErrno("pw_properties_new() failed");
pw_properties_setf(props, PW_KEY_NODE_NAME, "mpd.%s", name);
@@ -649,7 +677,7 @@ inline void
PipeWireOutput::DsdFormatChanged(const struct spa_pod ¶m) noexcept
{
uint32_t media_type, media_subtype;
- struct spa_audio_info_dsd dsd;
+ struct spa_audio_info_dsd dsd{};
if (spa_format_parse(¶m, &media_type, &media_subtype) >= 0 &&
media_type == SPA_MEDIA_TYPE_audio &&
@@ -761,8 +789,17 @@ PipeWireOutput::Process() noexcept
auto &d = buffer.datas[0];
const std::span dest{reinterpret_cast(d.data), d.maxsize};
- if (dest.data() == nullptr)
+ if (dest.data() == nullptr) {
+ /* this is not supposed to happen: due to
+ PW_STREAM_FLAG_MAP_BUFFERS, libpipewire maps all
+ buffers for us, except for DmaBufs which are not
+ marked mappable, and we never negotiate DmaBuf; but
+ just in case, give the buffer back instead of
+ leaking it */
+ d.chunk->size = 0;
+ pw_stream_queue_buffer(stream, b);
return;
+ }
std::size_t chunk_size = frame_size;
@@ -781,7 +818,9 @@ PipeWireOutput::Process() noexcept
nbytes = max_chunks * chunk_size;
PcmSilence(dest.first(nbytes), sample_format);
- LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
+ /* this is a real-time thread, so we must not log
+ here; let the output thread do it */
+ silence_inserted.store(true, std::memory_order_relaxed);
}
auto &chunk = *d.chunk;
@@ -803,8 +842,6 @@ PipeWireOutput::Process() noexcept
std::chrono::steady_clock::duration
PipeWireOutput::Delay() const noexcept
{
- const PipeWire::ThreadLoopLock lock(thread_loop);
-
auto result = std::chrono::steady_clock::duration::zero();
if (paused)
/* idle while paused */
@@ -816,6 +853,13 @@ PipeWireOutput::Delay() const noexcept
std::size_t
PipeWireOutput::Play(std::span src)
{
+ if (silence_inserted.load(std::memory_order_relaxed)) {
+ silence_inserted.store(false, std::memory_order_relaxed);
+
+ if (throttle_silence_log.CheckUpdate(std::chrono::seconds(5)))
+ LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
+ }
+
const PipeWire::ThreadLoopLock lock(thread_loop);
paused = false;
@@ -879,9 +923,6 @@ PipeWireOutput::Cancel() noexcept
if (drained)
return;
- /* clear MPD's ring buffer */
- ring_buffer.Clear();
-
/* clear libpipewire's buffer */
pw_stream_flush(stream, false);
drained = true;
@@ -894,6 +935,11 @@ PipeWireOutput::Cancel() noexcept
active = false;
pw_stream_set_active(stream, false);
}
+
+ /* clear MPD's ring buffer; this must be done only after the
+ "process" callback has been disabled, because
+ RingBuffer::Clear() is not thread-safe */
+ ring_buffer.Clear();
}
bool
@@ -925,8 +971,6 @@ PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept
void
PipeWireOutput::SendTag(const Tag &tag)
{
- CheckThrowError();
-
static constexpr struct {
TagType mpd;
const char *pipewire;
@@ -954,10 +998,10 @@ PipeWireOutput::SendTag(const Tag &tag)
struct spa_dict dict = SPA_DICT_INIT(items.data(), (uint32_t)items.size());
const PipeWire::ThreadLoopLock lock(thread_loop);
+ CheckThrowError();
- auto rc = pw_stream_update_properties(stream, &dict);
- if (rc < 0)
- LogWarning(pipewire_output_domain, "Error updating properties");
+ if (int error = pw_stream_update_properties(stream, &dict); error < 0)
+ throw PipeWire::MakeError(error, "pw_stream_update_properties() failed");
}
void
diff --git a/src/util/RingBuffer.hxx b/src/util/RingBuffer.hxx
index 03216aba9c..5fc26ad614 100644
--- a/src/util/RingBuffer.hxx
+++ b/src/util/RingBuffer.hxx
@@ -176,14 +176,11 @@ public:
std::size_t WriteFramesFrom(std::span src, std::size_t frame_size) noexcept {
// TODO optimize, eliminate duplicate atomic reads
- std::size_t available = WriteAvailable();
- std::size_t frames_available = available / frame_size;
- std::size_t rounded_available = frames_available * frame_size;
-
- if (rounded_available < src.size())
- src = src.first(rounded_available);
-
- return WriteFrom(src);
+ const std::size_t available = std::min(WriteAvailable(), src.size());
+ const std::size_t rounded_available =
+ available - available % frame_size;
+
+ return WriteFrom(src.first(rounded_available));
}
/**
@@ -277,14 +274,11 @@ public:
std::size_t ReadFramesTo(std::span dest, std::size_t frame_size) noexcept {
// TODO optimize, eliminate duplicate atomic reads
- std::size_t available = ReadAvailable();
- std::size_t frames_available = available / frame_size;
- std::size_t rounded_available = frames_available * frame_size;
+ const std::size_t available = std::min(ReadAvailable(), dest.size());
+ const std::size_t rounded_available =
+ available - available % frame_size;
- if (rounded_available < dest.size())
- dest = dest.first(rounded_available);
-
- return ReadTo(dest);
+ return ReadTo(dest.first(rounded_available));
}
/**
diff --git a/subprojects/openssl.wrap b/subprojects/openssl.wrap
index 873d55106e..e775bb104f 100644
--- a/subprojects/openssl.wrap
+++ b/subprojects/openssl.wrap
@@ -1,15 +1,14 @@
[wrap-file]
-directory = openssl-3.0.8
-source_url = https://www.openssl.org/source/openssl-3.0.8.tar.gz
-source_filename = openssl-3.0.8.tar.gz
-source_hash = 6c13d2bf38fdf31eac3ce2a347073673f5d63263398f1f69d0df4a41253e4b3e
-patch_filename = openssl_3.0.8-3_patch.zip
-patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.8-3/get_patch
-patch_hash = 300da189e106942347d61a4a4295aa2edbcf06184f8d13b4cee0bed9fb936963
-source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.8-3/openssl-3.0.8.tar.gz
-wrapdb_version = 3.0.8-3
+directory = openssl-3.0.10
+source_url = https://www.openssl.org/source/openssl-3.0.10.tar.gz
+source_filename = openssl-3.0.10.tar.gz
+source_hash = 1761d4f5b13a1028b9b6f3d4b8e17feb0cedc9370f6afe61d7193d2cdce83323
+source_fallback_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.10-1/get_source/openssl-3.0.10.tar.gz
+patch_filename = openssl_3.0.10-1_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.10-1/get_patch
+patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.10-1/openssl_3.0.10-1_patch.zip
+patch_hash = 2d142b7e3b1ac092cf67cb4891594c4a2d044aa92624c617a8dcbfe4f056d907
+wrapdb_version = 3.0.10-1
[provide]
-libcrypto = libcrypto_dep
-libssl = libssl_dep
-openssl = openssl_dep
+dependency_names = libcrypto, libssl, openssl
diff --git a/subprojects/sqlite3.wrap b/subprojects/sqlite3.wrap
index 55186ed25e..ce5b8446f2 100644
--- a/subprojects/sqlite3.wrap
+++ b/subprojects/sqlite3.wrap
@@ -1,14 +1,14 @@
[wrap-file]
-directory = sqlite-amalgamation-3530300
-source_url = https://www.sqlite.org/2026/sqlite-amalgamation-3530300.zip
-source_filename = sqlite-amalgamation-3530300.zip
-source_hash = 646421e12aac110282ef8cc68f1a62d4bb15fc7b8f09da0b53e29ee690500431
-source_fallback_url = https://wrapdb.mesonbuild.com/v2/sqlite3_3.53.3-1/get_source/sqlite-amalgamation-3530300.zip
-patch_filename = sqlite3_3.53.3-1_patch.zip
-patch_url = https://wrapdb.mesonbuild.com/v2/sqlite3_3.53.3-1/get_patch
-patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sqlite3_3.53.3-1/sqlite3_3.53.3-1_patch.zip
-patch_hash = acd1a0cca89d7d4b50375002e61eaf67f5a432b6801721fa38cab05a9f1e0385
-wrapdb_version = 3.53.3-1
+directory = sqlite-amalgamation-3530400
+source_url = https://www.sqlite.org/2026/sqlite-amalgamation-3530400.zip
+source_filename = sqlite-amalgamation-3530400.zip
+source_hash = 1e71ddf93849c6a6ecf58b827c0692073d2dd7ee40196158068f7b29f422e87d
+source_fallback_url = https://wrapdb.mesonbuild.com/v2/sqlite3_3.53.4-1/get_source/sqlite-amalgamation-3530400.zip
+patch_filename = sqlite3_3.53.4-1_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/sqlite3_3.53.4-1/get_patch
+patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sqlite3_3.53.4-1/sqlite3_3.53.4-1_patch.zip
+patch_hash = fe8033e209d350fa74e52ce1398253b373e7cfdd6809bc9f304e409e76f108af
+wrapdb_version = 3.53.4-1
[provide]
dependency_names = sqlite3
diff --git a/test/meson.build b/test/meson.build
index 5b736da56f..715df4ca9b 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -218,6 +218,7 @@ if enable_database
executable(
'run_storage',
'run_storage.cxx',
+ 'ShutdownHandler.cxx',
'../src/TagSave.cxx',
include_directories: inc,
dependencies: [
diff --git a/test/run_storage.cxx b/test/run_storage.cxx
index 4961f8470f..8b799d3fb7 100644
--- a/test/run_storage.cxx
+++ b/test/run_storage.cxx
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
+#include "ShutdownHandler.hxx"
#include "cmdline/OptionDef.hxx"
#include "cmdline/OptionParser.hxx"
+#include "event/Loop.hxx"
#include "event/Thread.hxx"
#include "ConfigGlue.hxx"
#include "tag/Tag.hxx"
@@ -47,6 +49,7 @@ static constexpr auto usage_text = R"(Usage: run_storage [OPTIONS] COMMAND URI .
--verbose
Available commands:
+ idle URI
ls URI PATH
stat URI PATH
cat URI PATH
@@ -132,6 +135,24 @@ MakeStorage(EventLoop &event_loop, const char *uri)
return storage;
}
+static int
+Idle(Path config_path, const char *storage_uri)
+{
+ EventLoop event_loop;
+ const ShutdownHandler shutdown_handler{event_loop};
+
+ GlobalInit init{config_path};
+
+ auto storage = MakeStorage(init.GetEventLoop(), storage_uri);
+
+ fprintf(stderr, "Waiting for SIGINT/SIGTERM\n");
+ event_loop.Run();
+
+ /* now let the destructors of #storage and #init shut
+ everything down */
+ return EXIT_SUCCESS;
+}
+
static int
Ls(Storage &storage, const char *path)
{
@@ -266,6 +287,20 @@ try {
const auto c = ParseCommandLine(argc, argv);
SetLogThreshold(c.verbose ? LogLevel::DEBUG : LogLevel::INFO);
+
+ if (StringIsEqual(c.command, "idle")) {
+ /* this command needs to initialize things in a
+ different order, therefore it is handled before
+ #GlobalInit is constructed */
+
+ if (c.args.size() != 1) {
+ fputs(usage_text, stderr);
+ return EXIT_FAILURE;
+ }
+
+ return Idle(c.config_path, c.args[0]);
+ }
+
GlobalInit init{c.config_path};
if (StringIsEqual(c.command, "ls")) {
diff --git a/test/util/TestRingBuffer.cxx b/test/util/TestRingBuffer.cxx
index e5c5f4416f..07ac25d7a5 100644
--- a/test/util/TestRingBuffer.cxx
+++ b/test/util/TestRingBuffer.cxx
@@ -114,3 +114,104 @@ TEST(RingBuffer, ReadFromWriteTo)
EXPECT_EQ(b.WriteAvailable(), 4U);
EXPECT_EQ(b.ReadAvailable(), 0U);
}
+
+TEST(RingBuffer, ReadFramesTo)
+{
+ RingBuffer b{8};
+
+ EXPECT_EQ(b.WriteFrom(std::span{"abcdefgh"sv}), 8U);
+ // "abcdefgh_"
+
+ {
+ /* the destination buffer is not a multiple of the
+ frame size; only whole frames may be read */
+ std::array d;
+ EXPECT_EQ(b.ReadFramesTo(d, 3), 3U);
+ // "___defgh_"
+
+ EXPECT_EQ(ToStringView(d).substr(0, 3), "abc"sv);
+ }
+
+ EXPECT_EQ(b.ReadAvailable(), 5U);
+
+ {
+ /* this time, the amount of available data is not a
+ multiple of the frame size */
+ std::array d;
+ EXPECT_EQ(b.ReadFramesTo(d, 3), 3U);
+ // "______gh_"
+
+ EXPECT_EQ(ToStringView(d).substr(0, 3), "def"sv);
+ }
+
+ EXPECT_EQ(b.ReadAvailable(), 2U);
+
+ {
+ /* not enough data for one frame */
+ std::array d;
+ EXPECT_EQ(b.ReadFramesTo(d, 3), 0U);
+ }
+
+ EXPECT_EQ(b.ReadAvailable(), 2U);
+
+ /* now check the same with a read which wraps around the end
+ of the ring buffer */
+
+ EXPECT_EQ(b.WriteFrom(std::span{"ijklmn"sv}), 6U);
+ // "jklmn_ghi"
+
+ EXPECT_EQ(b.ReadAvailable(), 8U);
+
+ {
+ std::array d;
+ EXPECT_EQ(b.ReadFramesTo(d, 2), 4U);
+ // "_klmn____"
+
+ EXPECT_EQ(ToStringView(d).substr(0, 4), "ghij"sv);
+ }
+
+ EXPECT_EQ(b.ReadAvailable(), 4U);
+
+ {
+ std::array d;
+ EXPECT_EQ(b.ReadFramesTo(d, 2), 4U);
+ // "_________"
+
+ EXPECT_EQ(ToStringView(d), "klmn"sv);
+ }
+
+ EXPECT_EQ(b.ReadAvailable(), 0U);
+}
+
+TEST(RingBuffer, WriteFramesFrom)
+{
+ RingBuffer b{8};
+
+ {
+ /* the source buffer is not a multiple of the frame
+ size; only whole frames may be written */
+ EXPECT_EQ(b.WriteFramesFrom(std::span{"abcde"sv}, 3), 3U);
+ // "abc______"
+
+ EXPECT_EQ(b.ReadAvailable(), 3U);
+ EXPECT_EQ(ToStringView(b.Read()), "abc"sv);
+ }
+
+ {
+ /* this time, the amount of free space is not a
+ multiple of the frame size */
+ EXPECT_EQ(b.WriteFramesFrom(std::span{"defghijk"sv}, 3), 3U);
+ // "abcdef___"
+
+ EXPECT_EQ(b.WriteAvailable(), 2U);
+ EXPECT_EQ(ToStringView(b.Read()), "abcdef"sv);
+ }
+
+ {
+ /* not enough space for one frame */
+ EXPECT_EQ(b.WriteFramesFrom(std::span{"ghi"sv}, 3), 0U);
+
+ EXPECT_EQ(b.WriteAvailable(), 2U);
+ EXPECT_EQ(ToStringView(b.Read()), "abcdef"sv);
+ }
+}