From e5f23678ca1bdaaff3dba81af89d5d3fe9be594a Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Wed, 22 May 2019 18:24:45 +0200 Subject: [PATCH 1/6] Log: use GetFullMessage() to print exceptions Print all nested exceptions on a single line to avoid confusion. --- src/Log.cxx | 44 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/src/Log.cxx b/src/Log.cxx index 622f79b69..6e47512c4 100644 --- a/src/Log.cxx +++ b/src/Log.cxx @@ -19,8 +19,7 @@ #include "LogV.hxx" #include "util/Domain.hxx" - -#include +#include "util/Exception.hxx" #include #include @@ -94,31 +93,13 @@ FormatError(const Domain &domain, const char *fmt, ...) noexcept void LogError(const std::exception &e) noexcept { - Log(exception_domain, LogLevel::ERROR, e.what()); - - try { - std::rethrow_if_nested(e); - } catch (const std::exception &nested) { - LogError(nested, "nested"); - } catch (...) { - Log(exception_domain, LogLevel::ERROR, - "Unrecognized nested exception"); - } + LogError(exception_domain, GetFullMessage(e).c_str()); } void LogError(const std::exception &e, const char *msg) noexcept { - FormatError(exception_domain, "%s: %s", msg, e.what()); - - try { - std::rethrow_if_nested(e); - } catch (const std::exception &nested) { - LogError(nested); - } catch (...) { - Log(exception_domain, LogLevel::ERROR, - "Unrecognized nested exception"); - } + FormatError(exception_domain, "%s: %s", msg, GetFullMessage(e).c_str()); } void @@ -136,27 +117,14 @@ FormatError(const std::exception &e, const char *fmt, ...) noexcept void LogError(const std::exception_ptr &ep) noexcept { - try { - std::rethrow_exception(ep); - } catch (const std::exception &e) { - LogError(e); - } catch (...) { - Log(exception_domain, LogLevel::ERROR, - "Unrecognized exception"); - } + LogError(exception_domain, GetFullMessage(ep).c_str()); } void LogError(const std::exception_ptr &ep, const char *msg) noexcept { - try { - std::rethrow_exception(ep); - } catch (const std::exception &e) { - LogError(e, msg); - } catch (...) { - FormatError(exception_domain, - "%s: Unrecognized exception", msg); - } + FormatError(exception_domain, "%s: %s", msg, + GetFullMessage(ep).c_str()); } void From 36e6079c57787a3599de8be80e508eb497d0de1e Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Wed, 22 May 2019 18:38:25 +0200 Subject: [PATCH 2/6] Log: make LogLevel the first parameter Prepare for templated functions. --- src/Log.cxx | 22 +++++++++---------- src/Log.hxx | 16 +++++++------- src/LogBackend.cxx | 4 ++-- src/LogBackend.hxx | 2 +- src/LogV.hxx | 4 ++-- .../plugins/FluidsynthDecoderPlugin.cxx | 4 ++-- src/lib/ffmpeg/LogCallback.cxx | 2 +- src/system/FatalError.cxx | 2 +- test/test_translate_song.cxx | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Log.cxx b/src/Log.cxx index 6e47512c4..befe238c9 100644 --- a/src/Log.cxx +++ b/src/Log.cxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2018 The Music Player Daemon Project + * Copyright 2003-2019 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -28,20 +28,20 @@ static constexpr Domain exception_domain("exception"); void -LogFormatV(const Domain &domain, LogLevel level, +LogFormatV(LogLevel level, const Domain &domain, const char *fmt, va_list ap) noexcept { char msg[1024]; vsnprintf(msg, sizeof(msg), fmt, ap); - Log(domain, level, msg); + Log(level, domain, msg); } void -LogFormat(const Domain &domain, LogLevel level, const char *fmt, ...) noexcept +LogFormat(LogLevel level, const Domain &domain, const char *fmt, ...) noexcept { va_list ap; va_start(ap, fmt); - LogFormatV(domain, level, fmt, ap); + LogFormatV(level, domain, fmt, ap); va_end(ap); } @@ -50,7 +50,7 @@ FormatDebug(const Domain &domain, const char *fmt, ...) noexcept { va_list ap; va_start(ap, fmt); - LogFormatV(domain, LogLevel::DEBUG, fmt, ap); + LogFormatV(LogLevel::DEBUG, domain, fmt, ap); va_end(ap); } @@ -59,7 +59,7 @@ FormatInfo(const Domain &domain, const char *fmt, ...) noexcept { va_list ap; va_start(ap, fmt); - LogFormatV(domain, LogLevel::INFO, fmt, ap); + LogFormatV(LogLevel::INFO, domain, fmt, ap); va_end(ap); } @@ -68,7 +68,7 @@ FormatDefault(const Domain &domain, const char *fmt, ...) noexcept { va_list ap; va_start(ap, fmt); - LogFormatV(domain, LogLevel::DEFAULT, fmt, ap); + LogFormatV(LogLevel::DEFAULT, domain, fmt, ap); va_end(ap); } @@ -77,7 +77,7 @@ FormatWarning(const Domain &domain, const char *fmt, ...) noexcept { va_list ap; va_start(ap, fmt); - LogFormatV(domain, LogLevel::WARNING, fmt, ap); + LogFormatV(LogLevel::WARNING, domain, fmt, ap); va_end(ap); } @@ -86,7 +86,7 @@ FormatError(const Domain &domain, const char *fmt, ...) noexcept { va_list ap; va_start(ap, fmt); - LogFormatV(domain, LogLevel::ERROR, fmt, ap); + LogFormatV(LogLevel::ERROR, domain, fmt, ap); va_end(ap); } @@ -142,7 +142,7 @@ FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept void LogErrno(const Domain &domain, int e, const char *msg) noexcept { - LogFormat(domain, LogLevel::ERROR, "%s: %s", msg, strerror(e)); + LogFormat(LogLevel::ERROR, domain, "%s: %s", msg, strerror(e)); } void diff --git a/src/Log.hxx b/src/Log.hxx index 2e8c0c6ee..7454d96ea 100644 --- a/src/Log.hxx +++ b/src/Log.hxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2018 The Music Player Daemon Project + * Copyright 2003-2019 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -28,16 +28,16 @@ class Domain; void -Log(const Domain &domain, LogLevel level, const char *msg) noexcept; +Log(LogLevel level, const Domain &domain, const char *msg) noexcept; gcc_printf(3,4) void -LogFormat(const Domain &domain, LogLevel level, const char *fmt, ...) noexcept; +LogFormat(LogLevel level, const Domain &domain, const char *fmt, ...) noexcept; static inline void LogDebug(const Domain &domain, const char *msg) noexcept { - Log(domain, LogLevel::DEBUG, msg); + Log(LogLevel::DEBUG, domain, msg); } gcc_printf(2,3) @@ -47,7 +47,7 @@ FormatDebug(const Domain &domain, const char *fmt, ...) noexcept; static inline void LogInfo(const Domain &domain, const char *msg) noexcept { - Log(domain, LogLevel::INFO, msg); + Log(LogLevel::INFO, domain, msg); } gcc_printf(2,3) @@ -57,7 +57,7 @@ FormatInfo(const Domain &domain, const char *fmt, ...) noexcept; static inline void LogDefault(const Domain &domain, const char *msg) noexcept { - Log(domain, LogLevel::DEFAULT, msg); + Log(LogLevel::DEFAULT, domain, msg); } gcc_printf(2,3) @@ -67,7 +67,7 @@ FormatDefault(const Domain &domain, const char *fmt, ...) noexcept; static inline void LogWarning(const Domain &domain, const char *msg) noexcept { - Log(domain, LogLevel::WARNING, msg); + Log(LogLevel::WARNING, domain, msg); } gcc_printf(2,3) @@ -77,7 +77,7 @@ FormatWarning(const Domain &domain, const char *fmt, ...) noexcept; static inline void LogError(const Domain &domain, const char *msg) noexcept { - Log(domain, LogLevel::ERROR, msg); + Log(LogLevel::ERROR, domain, msg); } void diff --git a/src/LogBackend.cxx b/src/LogBackend.cxx index 874da1af0..f7760ee23 100644 --- a/src/LogBackend.cxx +++ b/src/LogBackend.cxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2018 The Music Player Daemon Project + * Copyright 2003-2019 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -176,7 +176,7 @@ FileLog(const Domain &domain, const char *message) noexcept #endif /* !ANDROID */ void -Log(const Domain &domain, LogLevel level, const char *msg) noexcept +Log(LogLevel level, const Domain &domain, const char *msg) noexcept { #ifdef ANDROID __android_log_print(ToAndroidLogLevel(level), "MPD", diff --git a/src/LogBackend.hxx b/src/LogBackend.hxx index 76d7b1910..b90354755 100644 --- a/src/LogBackend.hxx +++ b/src/LogBackend.hxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2018 The Music Player Daemon Project + * Copyright 2003-2019 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify diff --git a/src/LogV.hxx b/src/LogV.hxx index 23e27d931..7e55d3d24 100644 --- a/src/LogV.hxx +++ b/src/LogV.hxx @@ -1,5 +1,5 @@ /* - * Copyright 2003-2018 The Music Player Daemon Project + * Copyright 2003-2019 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -25,7 +25,7 @@ #include void -LogFormatV(const Domain &domain, LogLevel level, +LogFormatV(LogLevel level, const Domain &domain, const char *fmt, va_list ap) noexcept; #endif /* LOG_H */ diff --git a/src/decoder/plugins/FluidsynthDecoderPlugin.cxx b/src/decoder/plugins/FluidsynthDecoderPlugin.cxx index ad09c6f3d..905aa55be 100644 --- a/src/decoder/plugins/FluidsynthDecoderPlugin.cxx +++ b/src/decoder/plugins/FluidsynthDecoderPlugin.cxx @@ -70,8 +70,8 @@ fluidsynth_mpd_log_function(int level, char *message, void *) { - Log(fluidsynth_domain, - fluidsynth_level_to_mpd(fluid_log_level(level)), + Log(fluidsynth_level_to_mpd(fluid_log_level(level)), + fluidsynth_domain, message); } diff --git a/src/lib/ffmpeg/LogCallback.cxx b/src/lib/ffmpeg/LogCallback.cxx index f236b0eea..0dcdb5e6c 100644 --- a/src/lib/ffmpeg/LogCallback.cxx +++ b/src/lib/ffmpeg/LogCallback.cxx @@ -62,6 +62,6 @@ FfmpegLogCallback(gcc_unused void *ptr, int level, const char *fmt, va_list vl) ffmpeg_domain.GetName(), cls->item_name(ptr)); const Domain d(domain); - LogFormatV(d, FfmpegImportLogLevel(level), fmt, vl); + LogFormatV(FfmpegImportLogLevel(level), d, fmt, vl); } } diff --git a/src/system/FatalError.cxx b/src/system/FatalError.cxx index b846dc830..dc056ca3c 100644 --- a/src/system/FatalError.cxx +++ b/src/system/FatalError.cxx @@ -54,7 +54,7 @@ FormatFatalError(const char *fmt, ...) { va_list ap; va_start(ap, fmt); - LogFormatV(fatal_error_domain, LogLevel::ERROR, fmt, ap); + LogFormatV(LogLevel::ERROR, fatal_error_domain, fmt, ap); va_end(ap); Abort(); diff --git a/test/test_translate_song.cxx b/test/test_translate_song.cxx index 8c2b41ebb..6afcc3ca4 100644 --- a/test/test_translate_song.cxx +++ b/test/test_translate_song.cxx @@ -25,7 +25,7 @@ #include void -Log(const Domain &domain, gcc_unused LogLevel level, const char *msg) noexcept +Log(LogLevel, const Domain &domain, const char *msg) noexcept { fprintf(stderr, "[%s] %s\n", domain.GetName(), msg); } From e7c5a42821ebe7767f76aabebf1247fddd3c6376 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 23 May 2019 12:23:28 +0200 Subject: [PATCH 3/6] Log: add Log() and LogFormat() overloads with std::exception_ptr Make LogError()/FormatError() wrappers for those. Now we can log exceptions with a lower level. --- src/Log.cxx | 26 ++++++++++---------- src/Log.hxx | 69 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/Log.cxx b/src/Log.cxx index befe238c9..3c29ef9a3 100644 --- a/src/Log.cxx +++ b/src/Log.cxx @@ -91,19 +91,19 @@ FormatError(const Domain &domain, const char *fmt, ...) noexcept } void -LogError(const std::exception &e) noexcept +Log(LogLevel level, const std::exception &e) noexcept { - LogError(exception_domain, GetFullMessage(e).c_str()); + Log(level, exception_domain, GetFullMessage(e).c_str()); } void -LogError(const std::exception &e, const char *msg) noexcept +Log(LogLevel level, const std::exception &e, const char *msg) noexcept { - FormatError(exception_domain, "%s: %s", msg, GetFullMessage(e).c_str()); + LogFormat(level, exception_domain, "%s: %s", msg, GetFullMessage(e).c_str()); } void -FormatError(const std::exception &e, const char *fmt, ...) noexcept +LogFormat(LogLevel level, const std::exception &e, const char *fmt, ...) noexcept { char msg[1024]; va_list ap; @@ -111,24 +111,24 @@ FormatError(const std::exception &e, const char *fmt, ...) noexcept vsnprintf(msg, sizeof(msg), fmt, ap); va_end(ap); - LogError(e, msg); + Log(level, e, msg); } void -LogError(const std::exception_ptr &ep) noexcept +Log(LogLevel level, const std::exception_ptr &ep) noexcept { - LogError(exception_domain, GetFullMessage(ep).c_str()); + Log(level, exception_domain, GetFullMessage(ep).c_str()); } void -LogError(const std::exception_ptr &ep, const char *msg) noexcept +Log(LogLevel level, const std::exception_ptr &ep, const char *msg) noexcept { - FormatError(exception_domain, "%s: %s", msg, - GetFullMessage(ep).c_str()); + LogFormat(level, exception_domain, "%s: %s", msg, + GetFullMessage(ep).c_str()); } void -FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept +LogFormat(LogLevel level, const std::exception_ptr &ep, const char *fmt, ...) noexcept { char msg[1024]; va_list ap; @@ -136,7 +136,7 @@ FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept vsnprintf(msg, sizeof(msg), fmt, ap); va_end(ap); - LogError(ep, msg); + Log(level, ep, msg); } void diff --git a/src/Log.hxx b/src/Log.hxx index 7454d96ea..f116ca439 100644 --- a/src/Log.hxx +++ b/src/Log.hxx @@ -34,6 +34,28 @@ gcc_printf(3,4) void LogFormat(LogLevel level, const Domain &domain, const char *fmt, ...) noexcept; +void +Log(LogLevel level, const std::exception &e) noexcept; + +void +Log(LogLevel level, const std::exception &e, const char *msg) noexcept; + +gcc_printf(3,4) +void +LogFormat(LogLevel level, const std::exception &e, + const char *fmt, ...) noexcept; + +void +Log(LogLevel level, const std::exception_ptr &ep) noexcept; + +void +Log(LogLevel level, const std::exception_ptr &ep, const char *msg) noexcept; + +gcc_printf(3,4) +void +LogFormat(LogLevel level, const std::exception_ptr &ep, + const char *fmt, ...) noexcept; + static inline void LogDebug(const Domain &domain, const char *msg) noexcept { @@ -80,25 +102,44 @@ LogError(const Domain &domain, const char *msg) noexcept Log(LogLevel::ERROR, domain, msg); } -void -LogError(const std::exception &e) noexcept; +inline void +LogError(const std::exception &e) noexcept +{ + Log(LogLevel::ERROR, e); +} -void -LogError(const std::exception &e, const char *msg) noexcept; +inline void +LogError(const std::exception &e, const char *msg) noexcept +{ + Log(LogLevel::ERROR, e, msg); +} -gcc_printf(2,3) -void -FormatError(const std::exception &e, const char *fmt, ...) noexcept; +template +inline void +FormatError(const std::exception &e, const char *fmt, Args&&... args) noexcept +{ + LogFormat(LogLevel::ERROR, e, fmt, std::forward(args)...); +} -void -LogError(const std::exception_ptr &ep) noexcept; +inline void +LogError(const std::exception_ptr &ep) noexcept +{ + Log(LogLevel::ERROR, ep); +} -void -LogError(const std::exception_ptr &ep, const char *msg) noexcept; +inline void +LogError(const std::exception_ptr &ep, const char *msg) noexcept +{ + Log(LogLevel::ERROR, ep, msg); +} -gcc_printf(2,3) -void -FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept; +template +inline void +FormatError(const std::exception_ptr &ep, + const char *fmt, Args&&... args) noexcept +{ + LogFormat(LogLevel::ERROR, ep, fmt, std::forward(args)...); +} gcc_printf(2,3) void From 5ece9685c2dc0441bb0bf1aef3b3e5e548a8ee0c Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Mon, 6 Jul 2020 20:58:33 +0200 Subject: [PATCH 4/6] PluginUnavailable: backport class PluginUnconfigured from master Stop bothering people about the Tidal/Qobuz plugins. --- src/PluginUnavailable.hxx | 13 ++++++++++++- src/input/Init.cxx | 5 +++++ src/input/plugins/QobuzInputPlugin.cxx | 8 ++++---- src/input/plugins/TidalInputPlugin.cxx | 6 +++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/PluginUnavailable.hxx b/src/PluginUnavailable.hxx index 379bd87bf..a29361ad4 100644 --- a/src/PluginUnavailable.hxx +++ b/src/PluginUnavailable.hxx @@ -27,9 +27,20 @@ * that this plugin is unavailable. It will be disabled, and MPD can * continue initialization. */ -class PluginUnavailable final : public std::runtime_error { +class PluginUnavailable : public std::runtime_error { public: using std::runtime_error::runtime_error; }; +/** + * Like #PluginUnavailable, but denotes that the plugin is not + * available because it was not explicitly enabled in the + * configuration. The message may describe the necessary steps to + * enable it. + */ +class PluginUnconfigured : public PluginUnavailable { +public: + using PluginUnavailable::PluginUnavailable; +}; + #endif diff --git a/src/input/Init.cxx b/src/input/Init.cxx index 3b714bf5c..e944ff697 100644 --- a/src/input/Init.cxx +++ b/src/input/Init.cxx @@ -58,6 +58,11 @@ input_stream_global_init(const ConfigData &config, EventLoop &event_loop) if (plugin->init != nullptr) plugin->init(event_loop, *block); input_plugins_enabled[i] = true; + } catch (const PluginUnconfigured &e) { + LogFormat(LogLevel::INFO, e, + "Input plugin '%s' is not configured", + plugin->name); + continue; } catch (const PluginUnavailable &e) { FormatError(e, "Input plugin '%s' is unavailable", diff --git a/src/input/plugins/QobuzInputPlugin.cxx b/src/input/plugins/QobuzInputPlugin.cxx index 31f60cb24..372a38752 100644 --- a/src/input/plugins/QobuzInputPlugin.cxx +++ b/src/input/plugins/QobuzInputPlugin.cxx @@ -133,11 +133,11 @@ InitQobuzInput(EventLoop &event_loop, const ConfigBlock &block) const char *app_id = block.GetBlockValue("app_id"); if (app_id == nullptr) - throw PluginUnavailable("No Qobuz app_id configured"); + throw PluginUnconfigured("No Qobuz app_id configured"); const char *app_secret = block.GetBlockValue("app_secret"); if (app_secret == nullptr) - throw PluginUnavailable("No Qobuz app_secret configured"); + throw PluginUnconfigured("No Qobuz app_secret configured"); const char *device_manufacturer_id = block.GetBlockValue("device_manufacturer_id", "df691fdc-fa36-11e7-9718-635337d7df8f"); @@ -145,11 +145,11 @@ InitQobuzInput(EventLoop &event_loop, const ConfigBlock &block) const char *username = block.GetBlockValue("username"); const char *email = block.GetBlockValue("email"); if (username == nullptr && email == nullptr) - throw PluginUnavailable("No Qobuz username configured"); + throw PluginUnconfigured("No Qobuz username configured"); const char *password = block.GetBlockValue("password"); if (password == nullptr) - throw PluginUnavailable("No Qobuz password configured"); + throw PluginUnconfigured("No Qobuz password configured"); const char *format_id = block.GetBlockValue("format_id", "5"); diff --git a/src/input/plugins/TidalInputPlugin.cxx b/src/input/plugins/TidalInputPlugin.cxx index 3592262b2..426244af9 100644 --- a/src/input/plugins/TidalInputPlugin.cxx +++ b/src/input/plugins/TidalInputPlugin.cxx @@ -170,15 +170,15 @@ InitTidalInput(EventLoop &event_loop, const ConfigBlock &block) const char *token = block.GetBlockValue("token"); if (token == nullptr) - throw PluginUnavailable("No Tidal application token configured"); + throw PluginUnconfigured("No Tidal application token configured"); const char *username = block.GetBlockValue("username"); if (username == nullptr) - throw PluginUnavailable("No Tidal username configured"); + throw PluginUnconfigured("No Tidal username configured"); const char *password = block.GetBlockValue("password"); if (password == nullptr) - throw PluginUnavailable("No Tidal password configured"); + throw PluginUnconfigured("No Tidal password configured"); FormatWarning(tidal_domain, "The Tidal input plugin is deprecated because Tidal has changed the protocol and doesn't share documentation"); From 00789de7d42429f0fdd8f5282d54ec5e41d567d6 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Mon, 6 Jul 2020 21:35:31 +0200 Subject: [PATCH 5/6] db/upnp/Object: root nodes are allowed to omit parent_id and name This fixes compatibility with Plex DLNA. Closes https://github.com/MusicPlayerDaemon/MPD/issues/851 --- NEWS | 1 + src/db/plugins/upnp/Object.hxx | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 054059bfb..258723782 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ ver 0.21.25 (not yet released) - fix crash when using "rangeid" while playing * database - simple: automatically scan new mounts + - upnp: fix compatibility with Plex DLNA * storage - fix disappearing mounts after mounting twice - udisks: fix reading ".mpdignore" diff --git a/src/db/plugins/upnp/Object.hxx b/src/db/plugins/upnp/Object.hxx index e17cdfbdd..e27fcfc2d 100644 --- a/src/db/plugins/upnp/Object.hxx +++ b/src/db/plugins/upnp/Object.hxx @@ -89,9 +89,18 @@ public: tag.Clear(); } + gcc_pure + bool IsRoot() const noexcept { + return type == Type::CONTAINER && id == "0"; + } + gcc_pure bool Check() const noexcept { - return !id.empty() && !parent_id.empty() && !name.empty() && + return !id.empty() && + /* root nodes don't need a parent id and a + name */ + (IsRoot() || (!parent_id.empty() && + !name.empty())) && (type != UPnPDirObject::Type::ITEM || item_class != UPnPDirObject::ItemClass::UNKNOWN); } From c67372f8af0419da2012bae743d96e272594b7f4 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Mon, 6 Jul 2020 21:41:53 +0200 Subject: [PATCH 6/6] release v0.21.25 --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 258723782..e831815b5 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -ver 0.21.25 (not yet released) +ver 0.21.25 (2020/07/06) * protocol: - fix crash when using "rangeid" while playing * database