From 3aade670462da2e44a303b80950051627dd3f29d Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 31 May 2018 10:52:36 +0200 Subject: [PATCH] basic udisks2 support To get udisks2 support started, this commit contains the configure.ac option and a "neighbor" plugin which shows block devices. Later, this will allow mounting removable media with a new storage plugin. --- Makefile.am | 10 + configure.ac | 13 + src/CommandLine.cxx | 3 + src/neighbor/Registry.cxx | 4 + src/neighbor/plugins/UdisksNeighborPlugin.cxx | 408 ++++++++++++++++++ src/neighbor/plugins/UdisksNeighborPlugin.hxx | 27 ++ 6 files changed, 465 insertions(+) create mode 100644 src/neighbor/plugins/UdisksNeighborPlugin.cxx create mode 100644 src/neighbor/plugins/UdisksNeighborPlugin.hxx diff --git a/Makefile.am b/Makefile.am index 39fcde0a8..950c3e356 100644 --- a/Makefile.am +++ b/Makefile.am @@ -835,6 +835,7 @@ libneighbor_a_SOURCES = \ src/neighbor/NeighborPlugin.hxx libneighbor_a_CPPFLAGS = $(AM_CPPFLAGS) \ + $(DBUS_CFLAGS) \ $(UPNP_CFLAGS) \ $(SMBCLIENT_CFLAGS) @@ -857,6 +858,15 @@ NEIGHBOR_LIBS += \ $(UPNP_LIBS) endif +if ENABLE_UDISKS +libneighbor_a_SOURCES += \ + $(UDISKS_SOURCES) \ + src/neighbor/plugins/UdisksNeighborPlugin.cxx src/neighbor/plugins/UdisksNeighborPlugin.hxx +NEIGHBOR_LIBS += \ + $(DBUS_LIBS) \ + libodbus.a +endif + endif # database plugins diff --git a/configure.ac b/configure.ac index 1dea3e8f2..741154d26 100644 --- a/configure.ac +++ b/configure.ac @@ -632,6 +632,15 @@ dnl --------------------------------------------------------------------------- MPD_ENABLE_AUTO_PKG(dbus, DBUS, [dbus-1], [D-Bus support], [dbus-1 not found]) +AC_ARG_ENABLE(udisks, + AS_HELP_STRING([--enable-udisks], + [support for removable media via udisks2]),, + [enable_udisks=auto]) + +MPD_DEPENDS([enable_udisks], [found_dbus], [support for removable media via udisks2], [dbus-1 not found]) +MPD_AUTO(udisks, [support for removable media via udisks2], [udisks not available], [found_udisks=yes]) +MPD_DEFINE_CONDITIONAL(enable_udisks, ENABLE_UDISKS, [support for removable media via udisks2]) + dnl --------------------------------------------------------------------------- dnl Metadata Plugins dnl --------------------------------------------------------------------------- @@ -799,6 +808,9 @@ if test x$enable_neighbor_plugins = xauto; then if test x$enable_upnp = xyes; then enable_neighbor_plugins=yes fi + if test x$enable_udisks = xyes; then + enable_neighbor_plugins=yes + fi fi MPD_DEFINE_CONDITIONAL(enable_neighbor_plugins, ENABLE_NEIGHBOR_PLUGINS, @@ -1508,6 +1520,7 @@ results(libmpdclient, [libmpdclient]) results(inotify, [inotify]) results(sqlite, [SQLite]) results(dbus, [DBUS]) +results(udisks, [UDISKS]) printf '\nMetadata support:\n\t' results(id3,[ID3]) diff --git a/src/CommandLine.cxx b/src/CommandLine.cxx index b615cab27..22f579c49 100644 --- a/src/CommandLine.cxx +++ b/src/CommandLine.cxx @@ -226,6 +226,9 @@ static void version(void) #ifdef ENABLE_DBUS " dbus" #endif +#ifdef ENABLE_UDISKS + " udisks" +#endif #ifdef USE_EPOLL " epoll" #endif diff --git a/src/neighbor/Registry.cxx b/src/neighbor/Registry.cxx index fad045f27..99943f750 100644 --- a/src/neighbor/Registry.cxx +++ b/src/neighbor/Registry.cxx @@ -22,6 +22,7 @@ #include "NeighborPlugin.hxx" #include "plugins/SmbclientNeighborPlugin.hxx" #include "plugins/UpnpNeighborPlugin.hxx" +#include "plugins/UdisksNeighborPlugin.hxx" #include @@ -31,6 +32,9 @@ const NeighborPlugin *const neighbor_plugins[] = { #endif #ifdef ENABLE_UPNP &upnp_neighbor_plugin, +#endif +#ifdef ENABLE_UDISKS + &udisks_neighbor_plugin, #endif nullptr }; diff --git a/src/neighbor/plugins/UdisksNeighborPlugin.cxx b/src/neighbor/plugins/UdisksNeighborPlugin.cxx new file mode 100644 index 000000000..602582545 --- /dev/null +++ b/src/neighbor/plugins/UdisksNeighborPlugin.cxx @@ -0,0 +1,408 @@ +/* + * Copyright 2003-2018 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "UdisksNeighborPlugin.hxx" +#include "lib/dbus/Connection.hxx" +#include "lib/dbus/Error.hxx" +#include "lib/dbus/Watch.hxx" +#include "lib/dbus/Message.hxx" +#include "lib/dbus/PendingCall.hxx" +#include "lib/dbus/ReadIter.hxx" +#include "lib/dbus/ObjectManager.hxx" +#include "lib/dbus/UDisks2.hxx" +#include "neighbor/NeighborPlugin.hxx" +#include "neighbor/Explorer.hxx" +#include "neighbor/Listener.hxx" +#include "neighbor/Info.hxx" +#include "thread/Mutex.hxx" +#include "util/Domain.hxx" +#include "util/StringAPI.hxx" +#include "Log.hxx" + +#include +#include +#include +#include + +static constexpr Domain udisks_domain("udisks"); + +struct UdisksObject { + std::string path; + + std::string drive_id, block_id; + + bool is_filesystem = false; + + bool IsValid() const noexcept { + return !drive_id.empty() || !block_id.empty(); + } + + std::string GetUri() const noexcept { + if (!drive_id.empty()) + return "udisks://" + drive_id; + else if (!block_id.empty()) + return "udisks://" + block_id; + else + return {}; + } + + NeighborInfo ToNeighborInfo() const noexcept { + return {GetUri(), path}; + } +}; + +class UdisksNeighborExplorer final + : public NeighborExplorer, ODBus::WatchManagerObserver { + + ODBus::WatchManager dbus_watch; + + ODBus::PendingCall pending_list_call; + + /** + * Protects #by_uri, #by_path. + */ + mutable Mutex mutex; + + using ByUri = std::map; + ByUri by_uri; + std::map by_path; + +public: + UdisksNeighborExplorer(EventLoop &event_loop, + NeighborListener &_listener) noexcept + :NeighborExplorer(_listener), + dbus_watch(event_loop, *this) {} + + auto &GetEventLoop() noexcept { + return dbus_watch.GetEventLoop(); + } + + /* virtual methods from class NeighborExplorer */ + void Open() override; + void Close() noexcept override; + List GetList() const noexcept override; + +private: + /* virtual methods from class ODBus::WatchManagerObserver */ + void OnDBusClosed() noexcept override; + +private: + void Insert(UdisksObject &&o) noexcept; + void Remove(const std::string &path) noexcept; + + void OnListNotify(DBusPendingCall *pending) noexcept; + + static void OnListNotify(DBusPendingCall *pending, + void *user_data) noexcept { + auto &e = *(UdisksNeighborExplorer *)user_data; + e.OnListNotify(pending); + } + + DBusHandlerResult HandleMessage(DBusConnection *dbus_connection, + DBusMessage *message) noexcept; + static DBusHandlerResult HandleMessage(DBusConnection *dbus_connection, + DBusMessage *message, + void *user_data) noexcept; +}; + +void +UdisksNeighborExplorer::Open() +{ + using namespace ODBus; + + dbus_watch.SetConnection(Connection::GetSystem()); + + auto &connection = dbus_watch.GetConnection(); + dbus_connection_set_exit_on_disconnect(connection, false); + + try { + Error error; + dbus_bus_add_match(connection, + "type='signal',sender='" UDISKS2_INTERFACE "'," + "interface='" DBUS_OM_INTERFACE "'," + "path='" UDISKS2_PATH "'", + error); + error.CheckThrow("DBus AddMatch error"); + + dbus_connection_add_filter(connection, + HandleMessage, this, + nullptr); + + auto msg = Message::NewMethodCall(UDISKS2_INTERFACE, + UDISKS2_PATH, + DBUS_OM_INTERFACE, + "GetManagedObjects"); + pending_list_call = PendingCall::SendWithReply(connection, msg.Get()); + pending_list_call.SetNotify(OnListNotify, this); + } catch (...) { + dbus_watch.SetConnection(Connection()); + throw; + } +} + +void +UdisksNeighborExplorer::Close() noexcept +{ + using namespace ODBus; + + if (pending_list_call) { + pending_list_call.Cancel(); + } + + // TODO: remove_match + // TODO: remove_filter + + dbus_watch.SetConnection(Connection()); +} + +template +gcc_pure +static const char * +CheckString(I &&i) noexcept +{ + if (i.GetArgType() != DBUS_TYPE_STRING) + return nullptr; + + return i.GetString(); +} + +template +gcc_pure +static const char * +CheckVariantString(I &&i) noexcept +{ + if (i.GetArgType() != DBUS_TYPE_VARIANT) + return nullptr; + + return CheckString(i.Recurse()); +} + +static void +ParseDriveDictEntry(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept +{ + if (i.GetArgType() != DBUS_TYPE_STRING) + return; + + const char *name = i.GetString(); + i.Next(); + + if (StringIsEqual(name, "Id")) { + const char *value = CheckVariantString(i); + if (value != nullptr && o.drive_id.empty()) + o.drive_id = value; + } +} + +static void +ParseBlockDictEntry(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept +{ + if (i.GetArgType() != DBUS_TYPE_STRING) + return; + + const char *name = i.GetString(); + i.Next(); + + if (StringIsEqual(name, "Id")) { + const char *value = CheckVariantString(i); + if (value != nullptr && o.block_id.empty()) + o.block_id = value; + } +} + +static void +ParseInterface(UdisksObject &o, const char *interface, + ODBus::ReadMessageIter &&i) noexcept +{ + if (StringIsEqual(interface, "org.freedesktop.UDisks2.Drive")) { + for (; i.GetArgType() == DBUS_TYPE_DICT_ENTRY; i.Next()) + ParseDriveDictEntry(o, i.Recurse()); + } else if (StringIsEqual(interface, "org.freedesktop.UDisks2.Block")) { + for (; i.GetArgType() == DBUS_TYPE_DICT_ENTRY; i.Next()) + ParseBlockDictEntry(o, i.Recurse()); + } else if (StringIsEqual(interface, "org.freedesktop.UDisks2.Filesystem")) { + o.is_filesystem = true; + } +} + +static void +ParseInterfaceDictEntry(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept +{ + if (i.GetArgType() != DBUS_TYPE_STRING) + return; + + const char *interface = i.GetString(); + i.Next(); + + if (i.GetArgType() != DBUS_TYPE_ARRAY) + return; + + ParseInterface(o, interface, i.Recurse()); +} + +static bool +ParseObject(UdisksObject &o, ODBus::ReadMessageIter &&i) noexcept +{ + if (i.GetArgType() != DBUS_TYPE_OBJECT_PATH) + return false; + + o.path = i.GetString(); + + i.Next(); + + if (i.GetArgType() != DBUS_TYPE_ARRAY) + return false; + + i.Recurse().ForEach(DBUS_TYPE_DICT_ENTRY, [&o](auto &&j){ + ParseInterfaceDictEntry(o, j.Recurse()); + }); + + return true; +} + +NeighborExplorer::List +UdisksNeighborExplorer::GetList() const noexcept +{ + const std::lock_guard lock(mutex); + + NeighborExplorer::List result; + + for (const auto &i : by_uri) + result.emplace_front(i.second); + return result; +} + +void +UdisksNeighborExplorer::OnDBusClosed() noexcept +{ + // TODO: reconnect +} + +void +UdisksNeighborExplorer::Insert(UdisksObject &&o) noexcept +{ + assert(o.IsValid()); + + const NeighborInfo info = o.ToNeighborInfo(); + + { + const std::lock_guard protect(mutex); + auto i = by_uri.emplace(std::make_pair(o.GetUri(), info)); + if (!i.second) + i.first->second = info; + + by_path.emplace(std::make_pair(o.path, i.first)); + // TODO: do we need to remove a conflicting path? + } + + listener.FoundNeighbor(info); +} + +void +UdisksNeighborExplorer::Remove(const std::string &path) noexcept +{ + std::unique_lock lock(mutex); + + auto i = by_path.find(path); + if (i == by_path.end()) + return; + + const auto info = std::move(i->second->second); + + by_uri.erase(i->second); + by_path.erase(i); + + lock.unlock(); + listener.LostNeighbor(info); +} + +inline void +UdisksNeighborExplorer::OnListNotify(DBusPendingCall *pending) noexcept +{ + assert(pending == pending_list_call.Get()); + + pending_list_call = {}; + + using namespace ODBus; + Message reply = Message::StealReply(*pending); + + try { + reply.CheckThrowError(); + } catch (...) { + LogError(std::current_exception()); + return; + } + + ReadMessageIter i(*reply.Get()); + if (i.GetArgType() != DBUS_TYPE_ARRAY) { + LogError(udisks_domain, "Malformed response"); + return; + } + + i.Recurse().ForEach(DBUS_TYPE_DICT_ENTRY, [this](auto &&j){ + UdisksObject o; + if (ParseObject(o, j.Recurse()) && o.IsValid()) + Insert(std::move(o)); + }); +} + +inline DBusHandlerResult +UdisksNeighborExplorer::HandleMessage(DBusConnection *, DBusMessage *message) noexcept +{ + using namespace ODBus; + + if (dbus_message_is_signal(message, DBUS_OM_INTERFACE, + "InterfacesAdded") && + dbus_message_has_signature(message, DBUS_OM_INTERFACES_ADDED_SIGNATURE)) { + UdisksObject o; + if (ParseObject(o, ReadMessageIter(*message)) && o.IsValid()) + Insert(std::move(o)); + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus_message_is_signal(message, DBUS_OM_INTERFACE, + "InterfacesRemoved") && + dbus_message_has_signature(message, DBUS_OM_INTERFACES_REMOVED_SIGNATURE)) { + Remove(ReadMessageIter(*message).GetString()); + return DBUS_HANDLER_RESULT_HANDLED; + } else + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +DBusHandlerResult +UdisksNeighborExplorer::HandleMessage(DBusConnection *connection, + DBusMessage *message, + void *user_data) noexcept +{ + auto &agent = *(UdisksNeighborExplorer *)user_data; + + return agent.HandleMessage(connection, message); +} + +static std::unique_ptr +udisks_neighbor_create(EventLoop &event_loop, + NeighborListener &listener, + gcc_unused const ConfigBlock &block) +{ + return std::make_unique(event_loop, listener); +} + +const NeighborPlugin udisks_neighbor_plugin = { + "udisks", + udisks_neighbor_create, +}; diff --git a/src/neighbor/plugins/UdisksNeighborPlugin.hxx b/src/neighbor/plugins/UdisksNeighborPlugin.hxx new file mode 100644 index 000000000..f7b122b15 --- /dev/null +++ b/src/neighbor/plugins/UdisksNeighborPlugin.hxx @@ -0,0 +1,27 @@ +/* + * Copyright 2003-2018 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_NEIGHBOR_UDISKS_HXX +#define MPD_NEIGHBOR_UDISKS_HXX + +struct NeighborPlugin; + +extern const NeighborPlugin udisks_neighbor_plugin; + +#endif