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); + } +}