diff options
Diffstat (limited to '')
-rw-r--r-- | src/lib/upnp/Action.hxx | 56 | ||||
-rw-r--r-- | src/lib/upnp/Callback.hxx | 46 | ||||
-rw-r--r-- | src/lib/upnp/ClientInit.cxx | 93 | ||||
-rw-r--r-- | src/lib/upnp/ClientInit.hxx | 35 | ||||
-rw-r--r-- | src/lib/upnp/ContentDirectoryService.cxx | 93 | ||||
-rw-r--r-- | src/lib/upnp/ContentDirectoryService.hxx | 135 | ||||
-rw-r--r-- | src/lib/upnp/Device.cxx | 134 | ||||
-rw-r--r-- | src/lib/upnp/Device.hxx | 88 | ||||
-rw-r--r-- | src/lib/upnp/Discovery.cxx | 339 | ||||
-rw-r--r-- | src/lib/upnp/Discovery.hxx | 164 | ||||
-rw-r--r-- | src/lib/upnp/Domain.cxx | 23 | ||||
-rw-r--r-- | src/lib/upnp/Domain.hxx | 27 | ||||
-rw-r--r-- | src/lib/upnp/Init.cxx | 73 | ||||
-rw-r--r-- | src/lib/upnp/Init.hxx | 33 | ||||
-rw-r--r-- | src/lib/upnp/Util.cxx | 166 | ||||
-rw-r--r-- | src/lib/upnp/Util.hxx | 46 | ||||
-rw-r--r-- | src/lib/upnp/WorkQueue.hxx | 206 | ||||
-rw-r--r-- | src/lib/upnp/ixmlwrap.cxx | 44 | ||||
-rw-r--r-- | src/lib/upnp/ixmlwrap.hxx | 35 |
19 files changed, 1836 insertions, 0 deletions
diff --git a/src/lib/upnp/Action.hxx b/src/lib/upnp/Action.hxx new file mode 100644 index 000000000..28c88be92 --- /dev/null +++ b/src/lib/upnp/Action.hxx @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2003-2014 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_UPNP_ACTION_HXX +#define MPD_UPNP_ACTION_HXX + +#include "Compiler.h" + +#include <upnp/upnptools.h> + +static inline constexpr unsigned +CountNameValuePairs() +{ + return 0; +} + +template<typename... Args> +static inline constexpr unsigned +CountNameValuePairs(gcc_unused const char *name, gcc_unused const char *value, + Args... args) +{ + return 1 + CountNameValuePairs(args...); +} + +/** + * A wrapper for UpnpMakeAction() that counts the number of name/value + * pairs and adds the nullptr sentinel. + */ +template<typename... Args> +static inline IXML_Document * +MakeActionHelper(const char *action_name, const char *service_type, + Args... args) +{ + const unsigned n = CountNameValuePairs(args...); + return UpnpMakeAction(action_name, service_type, n, + args..., + nullptr, nullptr); +} + +#endif diff --git a/src/lib/upnp/Callback.hxx b/src/lib/upnp/Callback.hxx new file mode 100644 index 000000000..85daf0a7e --- /dev/null +++ b/src/lib/upnp/Callback.hxx @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2003-2014 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_UPNP_CALLBACK_HXX +#define MPD_UPNP_CALLBACK_HXX + +#include <upnp/upnp.h> + +/** + * A class that is supposed to be used for libupnp asynchronous + * callbacks. + */ +class UpnpCallback { +public: + /** + * Pass this value as "cookie" pointer to libupnp asynchronous + * functions. + */ + void *GetUpnpCookie() { + return this; + } + + static UpnpCallback &FromUpnpCookie(void *cookie) { + return *(UpnpCallback *)cookie; + } + + virtual int Invoke(Upnp_EventType et, void *evp) = 0; +}; + +#endif diff --git a/src/lib/upnp/ClientInit.cxx b/src/lib/upnp/ClientInit.cxx new file mode 100644 index 000000000..77d9cf03d --- /dev/null +++ b/src/lib/upnp/ClientInit.cxx @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2003-2014 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 "ClientInit.hxx" +#include "Init.hxx" +#include "Callback.hxx" +#include "Domain.hxx" +#include "thread/Mutex.hxx" +#include "util/Error.hxx" + +#include <upnp/upnptools.h> + +static Mutex upnp_client_init_mutex; +static unsigned upnp_client_ref; +static UpnpClient_Handle upnp_client_handle; + +static int +UpnpClientCallback(Upnp_EventType et, void *evp, void *cookie) +{ + if (cookie == nullptr) + /* this is the cookie passed to UpnpRegisterClient(); + but can this ever happen? Will libupnp ever invoke + the registered callback without that cookie? */ + return UPNP_E_SUCCESS; + + UpnpCallback &callback = UpnpCallback::FromUpnpCookie(cookie); + return callback.Invoke(et, evp); +} + +static bool +DoInit(Error &error) +{ + auto code = UpnpRegisterClient(UpnpClientCallback, nullptr, + &upnp_client_handle); + if (code != UPNP_E_SUCCESS) { + error.Format(upnp_domain, code, + "UpnpRegisterClient() failed: %s", + UpnpGetErrorMessage(code)); + return false; + } + + return true; +} + +bool +UpnpClientGlobalInit(UpnpClient_Handle &handle, Error &error) +{ + if (!UpnpGlobalInit(error)) + return false; + + upnp_client_init_mutex.lock(); + bool success = upnp_client_ref > 0 || DoInit(error); + upnp_client_init_mutex.unlock(); + + if (success) { + ++upnp_client_ref; + handle = upnp_client_handle; + } else + UpnpGlobalFinish(); + + return success; +} + +void +UpnpClientGlobalFinish() +{ + upnp_client_init_mutex.lock(); + + assert(upnp_client_ref > 0); + if (--upnp_client_ref == 0) + UpnpUnRegisterClient(upnp_client_handle); + + upnp_client_init_mutex.unlock(); + + UpnpGlobalFinish(); +} diff --git a/src/lib/upnp/ClientInit.hxx b/src/lib/upnp/ClientInit.hxx new file mode 100644 index 000000000..645e64ca6 --- /dev/null +++ b/src/lib/upnp/ClientInit.hxx @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2003-2014 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_UPNP_CLIENT_INIT_HXX +#define MPD_UPNP_CLIENT_INIT_HXX + +#include "check.h" + +#include <upnp/upnp.h> + +class Error; + +bool +UpnpClientGlobalInit(UpnpClient_Handle &handle, Error &error); + +void +UpnpClientGlobalFinish(); + +#endif diff --git a/src/lib/upnp/ContentDirectoryService.cxx b/src/lib/upnp/ContentDirectoryService.cxx new file mode 100644 index 000000000..ef8757ec5 --- /dev/null +++ b/src/lib/upnp/ContentDirectoryService.cxx @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2003-2014 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 "ContentDirectoryService.hxx" +#include "Domain.hxx" +#include "Device.hxx" +#include "ixmlwrap.hxx" +#include "Util.hxx" +#include "Action.hxx" +#include "util/Error.hxx" + +ContentDirectoryService::ContentDirectoryService(const UPnPDevice &device, + const UPnPService &service) + :m_actionURL(caturl(device.URLBase, service.controlURL)), + m_serviceType(service.serviceType), + m_deviceId(device.UDN), + m_friendlyName(device.friendlyName), + m_manufacturer(device.manufacturer), + m_modelName(device.modelName), + m_rdreqcnt(200) +{ + if (!m_modelName.compare("MediaTomb")) { + // Readdir by 200 entries is good for most, but MediaTomb likes + // them really big. Actually 1000 is better but I don't dare + m_rdreqcnt = 500; + } +} + +ContentDirectoryService::~ContentDirectoryService() +{ + /* this destructor exists here just so it won't get inlined */ +} + +bool +ContentDirectoryService::getSearchCapabilities(UpnpClient_Handle hdl, + std::list<std::string> &result, + Error &error) const +{ + assert(result.empty()); + + IXML_Document *request = + UpnpMakeAction("GetSearchCapabilities", m_serviceType.c_str(), + 0, + nullptr, nullptr); + if (request == 0) { + error.Set(upnp_domain, "UpnpMakeAction() failed"); + return false; + } + + IXML_Document *response; + auto code = UpnpSendAction(hdl, m_actionURL.c_str(), + m_serviceType.c_str(), + 0 /*devUDN*/, request, &response); + ixmlDocument_free(request); + if (code != UPNP_E_SUCCESS) { + error.Format(upnp_domain, code, + "UpnpSendAction() failed: %s", + UpnpGetErrorMessage(code)); + return false; + } + + const char *s = ixmlwrap::getFirstElementValue(response, "SearchCaps"); + if (s == nullptr || *s == 0) { + ixmlDocument_free(response); + return true; + } + + bool success = true; + if (!csvToStrings(s, result)) { + error.Set(upnp_domain, "Bad response"); + success = false; + } + + ixmlDocument_free(response); + return success; +} diff --git a/src/lib/upnp/ContentDirectoryService.hxx b/src/lib/upnp/ContentDirectoryService.hxx new file mode 100644 index 000000000..0b03df2e7 --- /dev/null +++ b/src/lib/upnp/ContentDirectoryService.hxx @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2003-2014 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 _UPNPDIR_HXX_INCLUDED_ +#define _UPNPDIR_HXX_INCLUDED_ + +#include "Compiler.h" + +#include <upnp/upnp.h> + +#include <string> +#include <list> + +class Error; +class UPnPDevice; +struct UPnPService; +class UPnPDirContent; + +/** + * Content Directory Service class. + * + * This stores identity data from a directory service + * and the device it belongs to, and has methods to query + * the directory, using libupnp for handling the UPnP protocols. + * + * Note: m_rdreqcnt: number of entries requested per directory read. + * 0 means all entries. The device can still return less entries than + * requested, depending on its own limits. In general it's not optimal + * becauses it triggers issues, and is sometimes actually slower, e.g. on + * a D-Link NAS 327 + * + * The value chosen may affect by the UpnpSetMaxContentLength + * (2000*1024) done during initialization, but this should be ample + */ +class ContentDirectoryService { + std::string m_actionURL; + std::string m_serviceType; + std::string m_deviceId; + std::string m_friendlyName; + std::string m_manufacturer; + std::string m_modelName; + + int m_rdreqcnt; // Slice size to use when reading + +public: + /** + * Construct by copying data from device and service objects. + * + * The discovery service does this: use getDirServices() + */ + ContentDirectoryService(const UPnPDevice &device, + const UPnPService &service); + + /** An empty one */ + ContentDirectoryService() = default; + + ~ContentDirectoryService(); + + /** Read a container's children list into dirbuf. + * + * @param objectId the UPnP object Id for the container. Root has Id "0" + * @param[out] dirbuf stores the entries we read. + */ + bool readDir(UpnpClient_Handle handle, + const char *objectId, UPnPDirContent &dirbuf, + Error &error) const; + + bool readDirSlice(UpnpClient_Handle handle, + const char *objectId, unsigned offset, + unsigned count, UPnPDirContent& dirbuf, + unsigned &didread, unsigned &total, + Error &error) const; + + /** Search the content directory service. + * + * @param objectId the UPnP object Id under which the search + * should be done. Not all servers actually support this below + * root. Root has Id "0" + * @param searchstring an UPnP searchcriteria string. Check the + * UPnP document: UPnP-av-ContentDirectory-v1-Service-20020625.pdf + * section 2.5.5. Maybe we'll provide an easier way some day... + * @param[out] dirbuf stores the entries we read. + */ + bool search(UpnpClient_Handle handle, + const char *objectId, const char *searchstring, + UPnPDirContent &dirbuf, + Error &error) const; + + /** Read metadata for a given node. + * + * @param objectId the UPnP object Id. Root has Id "0" + * @param[out] dirbuf stores the entries we read. At most one entry will be + * returned. + */ + bool getMetadata(UpnpClient_Handle handle, + const char *objectId, UPnPDirContent &dirbuf, + Error &error) const; + + /** Retrieve search capabilities + * + * @param[out] result an empty vector: no search, or a single '*' element: + * any tag can be used in a search, or a list of usable tag names. + */ + bool getSearchCapabilities(UpnpClient_Handle handle, + std::list<std::string> &result, + Error &error) const; + + gcc_pure + std::string GetURI() const { + return "upnp://" + m_deviceId + "/" + m_serviceType; + } + + /** Retrieve the "friendly name" for this server, useful for display. */ + const char *getFriendlyName() const { + return m_friendlyName.c_str(); + } +}; + +#endif /* _UPNPDIR_HXX_INCLUDED_ */ diff --git a/src/lib/upnp/Device.cxx b/src/lib/upnp/Device.cxx new file mode 100644 index 000000000..26bffd0f0 --- /dev/null +++ b/src/lib/upnp/Device.cxx @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2003-2014 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 "Device.hxx" +#include "Util.hxx" +#include "lib/expat/ExpatParser.hxx" +#include "util/Error.hxx" + +#include <stdlib.h> + +#include <string.h> + +UPnPDevice::~UPnPDevice() +{ + /* this destructor exists here just so it won't get inlined */ +} + +/** + * An XML parser which constructs an UPnP device object from the + * device descriptor. + */ +class UPnPDeviceParser final : public CommonExpatParser { + UPnPDevice &m_device; + + std::string *value; + + UPnPService m_tservice; + +public: + UPnPDeviceParser(UPnPDevice& device) + :m_device(device), + value(nullptr) {} + +protected: + virtual void StartElement(const XML_Char *name, const XML_Char **) { + value = nullptr; + + switch (name[0]) { + case 'c': + if (strcmp(name, "controlURL") == 0) + value = &m_tservice.controlURL; + break; + case 'd': + if (strcmp(name, "deviceType") == 0) + value = &m_device.deviceType; + break; + case 'f': + if (strcmp(name, "friendlyName") == 0) + value = &m_device.friendlyName; + break; + case 'm': + if (strcmp(name, "manufacturer") == 0) + value = &m_device.manufacturer; + else if (strcmp(name, "modelName") == 0) + value = &m_device.modelName; + break; + case 's': + if (strcmp(name, "serviceType") == 0) + value = &m_tservice.serviceType; + break; + case 'U': + if (strcmp(name, "UDN") == 0) + value = &m_device.UDN; + else if (strcmp(name, "URLBase") == 0) + value = &m_device.URLBase; + break; + } + } + + virtual void EndElement(const XML_Char *name) { + if (value != nullptr) { + trimstring(*value); + value = nullptr; + } else if (!strcmp(name, "service")) { + m_device.services.emplace_back(std::move(m_tservice)); + m_tservice.clear(); + } + } + + virtual void CharacterData(const XML_Char *s, int len) { + if (value != nullptr) + value->append(s, len); + } +}; + +bool +UPnPDevice::Parse(const std::string &url, const char *description, + Error &error) +{ + { + UPnPDeviceParser mparser(*this); + if (!mparser.Parse(description, strlen(description), + true, error)) + return false; + } + + if (URLBase.empty()) { + // The standard says that if the URLBase value is empty, we should use + // the url the description was retrieved from. However this is + // sometimes something like http://host/desc.xml, sometimes something + // like http://host/ + + if (url.size() < 8) { + // ??? + URLBase = url; + } else { + auto hostslash = url.find_first_of("/", 7); + if (hostslash == std::string::npos || hostslash == url.size()-1) { + URLBase = url; + } else { + URLBase = path_getfather(url); + } + } + } + + return true; +} diff --git a/src/lib/upnp/Device.hxx b/src/lib/upnp/Device.hxx new file mode 100644 index 000000000..dd7ecac2d --- /dev/null +++ b/src/lib/upnp/Device.hxx @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2003-2014 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 _UPNPDEV_HXX_INCLUDED_ +#define _UPNPDEV_HXX_INCLUDED_ + +#include <vector> +#include <string> + +class Error; + +/** + * UPnP Description phase: interpreting the device description which we + * downloaded from the URL obtained by the discovery phase. + */ + +/** + * Data holder for a UPnP service, parsed from the XML description + * downloaded after discovery yielded its URL. + */ +struct UPnPService { + // e.g. urn:schemas-upnp-org:service:ConnectionManager:1 + std::string serviceType; + std::string controlURL; // e.g.: /upnp/control/cm + + void clear() + { + serviceType.clear(); + controlURL.clear(); + } +}; + +/** + * Data holder for a UPnP device, parsed from the XML description obtained + * during discovery. + * A device may include several services. To be of interest to us, + * one of them must be a ContentDirectory. + */ +class UPnPDevice { +public: + // e.g. urn:schemas-upnp-org:device:MediaServer:1 + std::string deviceType; + // e.g. MediaTomb + std::string friendlyName; + // Unique device number. This should match the deviceID in the + // discovery message. e.g. uuid:a7bdcd12-e6c1-4c7e-b588-3bbc959eda8d + std::string UDN; + // Base for all relative URLs. e.g. http://192.168.4.4:49152/ + std::string URLBase; + // Manufacturer: e.g. D-Link, PacketVideo ("manufacturer") + std::string manufacturer; + // Model name: e.g. MediaTomb, DNS-327L ("modelName") + std::string modelName; + // Services provided by this device. + std::vector<UPnPService> services; + + UPnPDevice() = default; + UPnPDevice(const UPnPDevice &) = delete; + UPnPDevice(UPnPDevice &&) = default; + UPnPDevice &operator=(UPnPDevice &&) = default; + + ~UPnPDevice(); + + /** Build device from xml description downloaded from discovery + * @param url where the description came from + * @param description the xml device description + */ + bool Parse(const std::string &url, const char *description, + Error &error); +}; + +#endif /* _UPNPDEV_HXX_INCLUDED_ */ diff --git a/src/lib/upnp/Discovery.cxx b/src/lib/upnp/Discovery.cxx new file mode 100644 index 000000000..9ea78c624 --- /dev/null +++ b/src/lib/upnp/Discovery.cxx @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2003-2014 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 "Discovery.hxx" +#include "Domain.hxx" +#include "ContentDirectoryService.hxx" +#include "system/Clock.hxx" +#include "Log.hxx" + +#include <upnp/upnptools.h> + +#include <string.h> + +// The service type string we are looking for. +static constexpr char ContentDirectorySType[] = "urn:schemas-upnp-org:service:ContentDirectory:1"; + +// We don't include a version in comparisons, as we are satisfied with +// version 1 +gcc_pure +static bool +isCDService(const char *st) +{ + constexpr size_t sz = sizeof(ContentDirectorySType) - 3; + return memcmp(ContentDirectorySType, st, sz) == 0; +} + +// The type of device we're asking for in search +static constexpr char MediaServerDType[] = "urn:schemas-upnp-org:device:MediaServer:1"; + +gcc_pure +static bool +isMSDevice(const char *st) +{ + constexpr size_t sz = sizeof(MediaServerDType) - 3; + return memcmp(MediaServerDType, st, sz) == 0; +} + +static void +AnnounceFoundUPnP(UPnPDiscoveryListener &listener, const UPnPDevice &device) +{ + for (const auto &service : device.services) + if (isCDService(service.serviceType.c_str())) + listener.FoundUPnP(ContentDirectoryService(device, + service)); +} + +static void +AnnounceLostUPnP(UPnPDiscoveryListener &listener, const UPnPDevice &device) +{ + for (const auto &service : device.services) + if (isCDService(service.serviceType.c_str())) + listener.LostUPnP(ContentDirectoryService(device, + service)); +} + +inline void +UPnPDeviceDirectory::LockAdd(ContentDirectoryDescriptor &&d) +{ + const ScopeLock protect(mutex); + + for (auto &i : directories) { + if (i.id == d.id) { + i = std::move(d); + return; + } + } + + directories.emplace_back(std::move(d)); + + if (listener != nullptr) + AnnounceFoundUPnP(*listener, directories.back().device); +} + +inline void +UPnPDeviceDirectory::LockRemove(const std::string &id) +{ + const ScopeLock protect(mutex); + + for (auto i = directories.begin(), end = directories.end(); + i != end; ++i) { + if (i->id == id) { + if (listener != nullptr) + AnnounceLostUPnP(*listener, i->device); + + directories.erase(i); + break; + } + } +} + +inline void +UPnPDeviceDirectory::discoExplorer() +{ + for (;;) { + DiscoveredTask *tsk = 0; + if (!discoveredQueue.take(tsk)) { + discoveredQueue.workerExit(); + return; + } + + // Device signals its existence and well-being. Perform the + // UPnP "description" phase by downloading and decoding the + // description document. + char *buf; + // LINE_SIZE is defined by libupnp's upnp.h... + char contentType[LINE_SIZE]; + int code = UpnpDownloadUrlItem(tsk->url.c_str(), &buf, contentType); + if (code != UPNP_E_SUCCESS) { + continue; + } + + // Update or insert the device + ContentDirectoryDescriptor d(std::move(tsk->deviceId), + MonotonicClockS(), tsk->expires); + + { + Error error2; + bool success = d.Parse(tsk->url, buf, error2); + free(buf); + if (!success) { + delete tsk; + LogError(error2); + continue; + } + } + + LockAdd(std::move(d)); + delete tsk; + } +} + +void * +UPnPDeviceDirectory::discoExplorer(void *ctx) +{ + UPnPDeviceDirectory &directory = *(UPnPDeviceDirectory *)ctx; + directory.discoExplorer(); + return (void*)1; +} + +inline int +UPnPDeviceDirectory::OnAlive(Upnp_Discovery *disco) +{ + if (isMSDevice(disco->DeviceType) || + isCDService(disco->ServiceType)) { + DiscoveredTask *tp = new DiscoveredTask(disco); + if (discoveredQueue.put(tp)) + return UPNP_E_FINISH; + } + + return UPNP_E_SUCCESS; +} + +inline int +UPnPDeviceDirectory::OnByeBye(Upnp_Discovery *disco) +{ + if (isMSDevice(disco->DeviceType) || + isCDService(disco->ServiceType)) { + // Device signals it is going off. + LockRemove(disco->DeviceId); + } + + return UPNP_E_SUCCESS; +} + +// This gets called for all libupnp asynchronous events, in a libupnp +// thread context. +// Example: ContentDirectories appearing and disappearing from the network +// We queue a task for our worker thread(s) +int +UPnPDeviceDirectory::Invoke(Upnp_EventType et, void *evp) +{ + switch (et) { + case UPNP_DISCOVERY_SEARCH_RESULT: + case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE: + { + Upnp_Discovery *disco = (Upnp_Discovery *)evp; + return OnAlive(disco); + } + + case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: + { + Upnp_Discovery *disco = (Upnp_Discovery *)evp; + return OnByeBye(disco); + } + + default: + // Ignore other events for now + break; + } + + return UPNP_E_SUCCESS; +} + +bool +UPnPDeviceDirectory::expireDevices(Error &error) +{ + const ScopeLock protect(mutex); + const unsigned now = MonotonicClockS(); + bool didsomething = false; + + for (auto it = directories.begin(); + it != directories.end();) { + if (now > it->expires) { + it = directories.erase(it); + didsomething = true; + } else { + it++; + } + } + + if (didsomething) + return search(error); + + return true; +} + +UPnPDeviceDirectory::UPnPDeviceDirectory(UpnpClient_Handle _handle, + UPnPDiscoveryListener *_listener) + :handle(_handle), + listener(_listener), + discoveredQueue("DiscoveredQueue"), + m_searchTimeout(2), m_lastSearch(0) +{ +} + +UPnPDeviceDirectory::~UPnPDeviceDirectory() +{ + /* this destructor exists here just so it won't get inlined */ +} + +bool +UPnPDeviceDirectory::Start(Error &error) +{ + if (!discoveredQueue.start(1, discoExplorer, this)) { + error.Set(upnp_domain, "Discover work queue start failed"); + return false; + } + + return search(error); +} + +bool +UPnPDeviceDirectory::search(Error &error) +{ + const unsigned now = MonotonicClockS(); + if (now - m_lastSearch < 10) + return true; + m_lastSearch = now; + + // We search both for device and service just in case. + int code = UpnpSearchAsync(handle, m_searchTimeout, + ContentDirectorySType, GetUpnpCookie()); + if (code != UPNP_E_SUCCESS) { + error.Format(upnp_domain, code, + "UpnpSearchAsync() failed: %s", + UpnpGetErrorMessage(code)); + return false; + } + + code = UpnpSearchAsync(handle, m_searchTimeout, + MediaServerDType, GetUpnpCookie()); + if (code != UPNP_E_SUCCESS) { + error.Format(upnp_domain, code, + "UpnpSearchAsync() failed: %s", + UpnpGetErrorMessage(code)); + return false; + } + + return true; +} + +bool +UPnPDeviceDirectory::getDirServices(std::vector<ContentDirectoryService> &out, + Error &error) +{ + // Has locking, do it before our own lock + if (!expireDevices(error)) + return false; + + const ScopeLock protect(mutex); + + for (auto dit = directories.begin(); + dit != directories.end(); dit++) { + for (const auto &service : dit->device.services) { + if (isCDService(service.serviceType.c_str())) { + out.emplace_back(dit->device, service); + } + } + } + + return true; +} + +bool +UPnPDeviceDirectory::getServer(const char *friendlyName, + ContentDirectoryService &server, + Error &error) +{ + // Has locking, do it before our own lock + if (!expireDevices(error)) + return false; + + const ScopeLock protect(mutex); + + for (const auto &i : directories) { + const auto &device = i.device; + + if (device.friendlyName != friendlyName) + continue; + + for (const auto &service : device.services) { + if (isCDService(service.serviceType.c_str())) { + server = ContentDirectoryService(device, + service); + return true; + } + } + } + + error.Set(upnp_domain, "Server not found"); + return false; +} diff --git a/src/lib/upnp/Discovery.hxx b/src/lib/upnp/Discovery.hxx new file mode 100644 index 000000000..767811840 --- /dev/null +++ b/src/lib/upnp/Discovery.hxx @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2003-2014 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 _UPNPPDISC_H_X_INCLUDED_ +#define _UPNPPDISC_H_X_INCLUDED_ + +#include "Callback.hxx" +#include "Device.hxx" +#include "WorkQueue.hxx" +#include "thread/Mutex.hxx" +#include "util/Error.hxx" +#include "Compiler.h" + +#include <upnp/upnp.h> + +#include <list> +#include <vector> +#include <string> + +class ContentDirectoryService; + +class UPnPDiscoveryListener { +public: + virtual void FoundUPnP(const ContentDirectoryService &service) = 0; + virtual void LostUPnP(const ContentDirectoryService &service) = 0; +}; + +/** + * Manage UPnP discovery and maintain a directory of active devices. Singleton. + * + * We are only interested in MediaServers with a ContentDirectory service + * for now, but this could be made more general, by removing the filtering. + */ +class UPnPDeviceDirectory final : UpnpCallback { + /** + * Each appropriate discovery event (executing in a libupnp thread + * context) queues the following task object for processing by the + * discovery thread. + */ + struct DiscoveredTask { + std::string url; + std::string deviceId; + unsigned expires; // Seconds valid + + DiscoveredTask(const Upnp_Discovery *disco) + :url(disco->Location), + deviceId(disco->DeviceId), + expires(disco->Expires) {} + }; + + /** + * Descriptor for one device having a Content Directory + * service found on the network. + */ + class ContentDirectoryDescriptor { + public: + std::string id; + + UPnPDevice device; + + /** + * The MonotonicClockS() time stamp when this device + * expires. + */ + unsigned expires; + + ContentDirectoryDescriptor() = default; + + ContentDirectoryDescriptor(std::string &&_id, + unsigned last, int exp) + :id(std::move(_id)), expires(last + exp + 20) {} + + bool Parse(const std::string &url, const char *description, + Error &_error) { + return device.Parse(url, description, _error); + } + }; + + const UpnpClient_Handle handle; + UPnPDiscoveryListener *const listener; + + Mutex mutex; + std::list<ContentDirectoryDescriptor> directories; + WorkQueue<DiscoveredTask *> discoveredQueue; + + /** + * The UPnP device search timeout, which should actually be + * called delay because it's the base of a random delay that + * the devices apply to avoid responding all at the same time. + */ + int m_searchTimeout; + + /** + * The MonotonicClockS() time stamp of the last search. + */ + unsigned m_lastSearch; + +public: + UPnPDeviceDirectory(UpnpClient_Handle _handle, + UPnPDiscoveryListener *_listener=nullptr); + ~UPnPDeviceDirectory(); + + UPnPDeviceDirectory(const UPnPDeviceDirectory &) = delete; + UPnPDeviceDirectory& operator=(const UPnPDeviceDirectory &) = delete; + + bool Start(Error &error); + + /** Retrieve the directory services currently seen on the network */ + bool getDirServices(std::vector<ContentDirectoryService> &, Error &); + + /** + * Get server by friendly name. + */ + bool getServer(const char *friendlyName, + ContentDirectoryService &server, + Error &error); + +private: + bool search(Error &error); + + /** + * Look at the devices and get rid of those which have not + * been seen for too long. We do this when listing the top + * directory. + */ + bool expireDevices(Error &error); + + void LockAdd(ContentDirectoryDescriptor &&d); + void LockRemove(const std::string &id); + + /** + * Worker routine for the discovery queue. Get messages about + * devices appearing and disappearing, and update the + * directory pool accordingly. + */ + static void *discoExplorer(void *); + void discoExplorer(); + + int OnAlive(Upnp_Discovery *disco); + int OnByeBye(Upnp_Discovery *disco); + int cluCallBack(Upnp_EventType et, void *evp); + + /* virtual methods from class UpnpCallback */ + virtual int Invoke(Upnp_EventType et, void *evp) override; +}; + + +#endif /* _UPNPPDISC_H_X_INCLUDED_ */ diff --git a/src/lib/upnp/Domain.cxx b/src/lib/upnp/Domain.cxx new file mode 100644 index 000000000..010d4c7c2 --- /dev/null +++ b/src/lib/upnp/Domain.cxx @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2003-2014 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 "Domain.hxx" +#include "util/Domain.hxx" + +const Domain upnp_domain("upnp"); diff --git a/src/lib/upnp/Domain.hxx b/src/lib/upnp/Domain.hxx new file mode 100644 index 000000000..ec01ef735 --- /dev/null +++ b/src/lib/upnp/Domain.hxx @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2003-2014 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_UPNP_DOMAIN_HXX +#define MPD_UPNP_DOMAIN_HXX + +class Domain; + +extern const Domain upnp_domain; + +#endif diff --git a/src/lib/upnp/Init.cxx b/src/lib/upnp/Init.cxx new file mode 100644 index 000000000..4fc606de9 --- /dev/null +++ b/src/lib/upnp/Init.cxx @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2003-2014 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 "Init.hxx" +#include "Domain.hxx" +#include "thread/Mutex.hxx" +#include "util/Error.hxx" + +#include <upnp/upnp.h> +#include <upnp/upnptools.h> +#include <upnp/ixml.h> + +static Mutex upnp_init_mutex; +static unsigned upnp_ref; + +static bool +DoInit(Error &error) +{ + auto code = UpnpInit(0, 0); + if (code != UPNP_E_SUCCESS) { + error.Format(upnp_domain, code, + "UpnpInit() failed: %s", + UpnpGetErrorMessage(code)); + return false; + } + + UpnpSetMaxContentLength(2000*1024); + + // Servers sometimes make error (e.g.: minidlna returns bad utf-8) + ixmlRelaxParser(1); + + return true; +} + +bool +UpnpGlobalInit(Error &error) +{ + const ScopeLock protect(upnp_init_mutex); + + if (upnp_ref == 0 && !DoInit(error)) + return false; + + ++upnp_ref; + return true; +} + +void +UpnpGlobalFinish() +{ + const ScopeLock protect(upnp_init_mutex); + + assert(upnp_ref > 0); + + if (--upnp_ref == 0) + UpnpFinish(); +} diff --git a/src/lib/upnp/Init.hxx b/src/lib/upnp/Init.hxx new file mode 100644 index 000000000..b23f8e2ab --- /dev/null +++ b/src/lib/upnp/Init.hxx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2003-2014 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_UPNP_INIT_HXX +#define MPD_UPNP_INIT_HXX + +#include "check.h" + +class Error; + +bool +UpnpGlobalInit(Error &error); + +void +UpnpGlobalFinish(); + +#endif diff --git a/src/lib/upnp/Util.cxx b/src/lib/upnp/Util.cxx new file mode 100644 index 000000000..cf34a47d3 --- /dev/null +++ b/src/lib/upnp/Util.cxx @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2003-2014 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 "Util.hxx" + +#include <upnp/ixml.h> + +#include <assert.h> + +/** Get rid of white space at both ends */ +void +trimstring(std::string &s, const char *ws) +{ + auto pos = s.find_first_not_of(ws); + if (pos == std::string::npos) { + s.clear(); + return; + } + s.replace(0, pos, std::string()); + + pos = s.find_last_not_of(ws); + if (pos != std::string::npos && pos != s.length()-1) + s.replace(pos + 1, std::string::npos, std::string()); +} + +std::string +caturl(const std::string &s1, const std::string &s2) +{ + if (s2.front() == '/') { + /* absolute path: replace the whole URI path in s1 */ + + auto i = s1.find("://"); + if (i == s1.npos) + /* no scheme: override s1 completely */ + return s2; + + /* find the first slash after the host part */ + i = s1.find('/', i + 3); + if (i == s1.npos) + /* there's no URI path - simply append s2 */ + i = s1.length(); + + return s1.substr(0, i) + s2; + } + + std::string out(s1); + if (out.back() != '/') + out.push_back('/'); + + out += s2; + return out; +} + +static void +path_catslash(std::string &s) +{ + if (s.empty() || s.back() != '/') + s += '/'; +} + +std::string +path_getfather(const std::string &s) +{ + std::string father = s; + + // ?? + if (father.empty()) + return "./"; + + if (father.back() == '/') { + // Input ends with /. Strip it, handle special case for root + if (father.length() == 1) + return father; + father.erase(father.length()-1); + } + + auto slp = father.rfind('/'); + if (slp == std::string::npos) + return "./"; + + father.erase(slp); + path_catslash(father); + return father; +} + +std::list<std::string> +stringToTokens(const std::string &str, + const char *delims, bool skipinit) +{ + std::list<std::string> tokens; + + std::string::size_type startPos = 0; + + // Skip initial delims, return empty if this eats all. + if (skipinit && + (startPos = str.find_first_not_of(delims, 0)) == std::string::npos) + return tokens; + + while (startPos < str.size()) { + // Find next delimiter or end of string (end of token) + auto pos = str.find_first_of(delims, startPos); + + // Add token to the vector and adjust start + if (pos == std::string::npos) { + tokens.emplace_back(str, startPos); + break; + } else if (pos == startPos) { + // Dont' push empty tokens after first + if (tokens.empty()) + tokens.emplace_back(); + startPos = ++pos; + } else { + tokens.emplace_back(str, startPos, pos - startPos); + startPos = ++pos; + } + } + + return tokens; +} + +template <class T> +bool +csvToStrings(const char *s, T &tokens) +{ + assert(tokens.empty()); + + std::string current; + + while (true) { + char ch = *s++; + if (ch == 0) { + tokens.emplace_back(std::move(current)); + return true; + } + + if (ch == '\\') { + ch = *s++; + if (ch == 0) + return false; + } else if (ch == ',') { + tokens.emplace_back(std::move(current)); + current.clear(); + continue; + } + + current.push_back(ch); + } +} + +template bool csvToStrings<std::list<std::string>>(const char *, std::list<std::string> &); diff --git a/src/lib/upnp/Util.hxx b/src/lib/upnp/Util.hxx new file mode 100644 index 000000000..58e382faa --- /dev/null +++ b/src/lib/upnp/Util.hxx @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2003-2014 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_UPNP_UTIL_HXX +#define MPD_UPNP_UTIL_HXX + +#include "Compiler.h" + +#include <string> +#include <list> + +std::string +caturl(const std::string& s1, const std::string& s2); + +void +trimstring(std::string &s, const char *ws = " \t\n"); + +std::string +path_getfather(const std::string &s); + +gcc_pure +std::list<std::string> +stringToTokens(const std::string &str, + const char *delims = "/", bool skipinit = true); + +template <class T> +bool +csvToStrings(const char *s, T &tokens); + +#endif /* _UPNPP_H_X_INCLUDED_ */ diff --git a/src/lib/upnp/WorkQueue.hxx b/src/lib/upnp/WorkQueue.hxx new file mode 100644 index 000000000..fe8ce53f9 --- /dev/null +++ b/src/lib/upnp/WorkQueue.hxx @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2003-2014 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 _WORKQUEUE_H_INCLUDED_ +#define _WORKQUEUE_H_INCLUDED_ + +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" + +#include <assert.h> +#include <pthread.h> + +#include <string> +#include <queue> + +#define LOGINFO(X) +#define LOGERR(X) + +/** + * A WorkQueue manages the synchronisation around a queue of work items, + * where a number of client threads queue tasks and a number of worker + * threads take and execute them. The goal is to introduce some level + * of parallelism between the successive steps of a previously single + * threaded pipeline. For example data extraction / data preparation / index + * update, but this could have other uses. + * + * There is no individual task status return. In case of fatal error, + * the client or worker sets an end condition on the queue. A second + * queue could conceivably be used for returning individual task + * status. + */ +template <class T> +class WorkQueue { + // Configuration + const std::string name; + + // Status + // Worker threads having called exit + unsigned n_workers_exited; + bool ok; + + unsigned n_threads; + pthread_t *threads; + + // Synchronization + std::queue<T> queue; + Cond client_cond; + Cond worker_cond; + Mutex mutex; + +public: + /** Create a WorkQueue + * @param name for message printing + * @param hi number of tasks on queue before clients blocks. Default 0 + * meaning no limit. hi == -1 means that the queue is disabled. + * @param lo minimum count of tasks before worker starts. Default 1. + */ + WorkQueue(const char *_name) + :name(_name), + n_workers_exited(0), + ok(false), + n_threads(0), threads(nullptr) + { + } + + ~WorkQueue() { + setTerminateAndWait(); + } + + /** Start the worker threads. + * + * @param nworkers number of threads copies to start. + * @param start_routine thread function. It should loop + * taking (QueueWorker::take()) and executing tasks. + * @param arg initial parameter to thread function. + * @return true if ok. + */ + bool start(unsigned nworkers, void *(*workproc)(void *), void *arg) + { + const ScopeLock protect(mutex); + + assert(nworkers > 0); + assert(!ok); + assert(n_threads == 0); + assert(threads == nullptr); + + n_threads = nworkers; + threads = new pthread_t[n_threads]; + + for (unsigned i = 0; i < nworkers; i++) { + int err; + if ((err = pthread_create(&threads[i], 0, workproc, arg))) { + LOGERR(("WorkQueue:%s: pthread_create failed, err %d\n", + name.c_str(), err)); + return false; + } + } + + ok = true; + return true; + } + + /** Add item to work queue, called from client. + * + * Sleeps if there are already too many. + */ + template<typename U> + bool put(U &&u) + { + const ScopeLock protect(mutex); + + queue.emplace(std::forward<U>(u)); + + // Just wake one worker, there is only one new task. + worker_cond.signal(); + + return true; + } + + + /** Tell the workers to exit, and wait for them. + */ + void setTerminateAndWait() + { + const ScopeLock protect(mutex); + + // Wait for all worker threads to have called workerExit() + ok = false; + while (n_workers_exited < n_threads) { + worker_cond.broadcast(); + client_cond.wait(mutex); + } + + // Perform the thread joins and compute overall status + // Workers return (void*)1 if ok + for (unsigned i = 0; i < n_threads; ++i) { + void *status; + pthread_join(threads[i], &status); + } + + delete[] threads; + threads = nullptr; + n_threads = 0; + + // Reset to start state. + n_workers_exited = 0; + } + + /** Take task from queue. Called from worker. + * + * Sleeps if there are not enough. Signal if we go to sleep on empty + * queue: client may be waiting for our going idle. + */ + bool take(T &tp) + { + const ScopeLock protect(mutex); + + if (!ok) + return false; + + while (queue.empty()) { + worker_cond.wait(mutex); + if (!ok) + return false; + } + + tp = std::move(queue.front()); + queue.pop(); + return true; + } + + /** Advertise exit and abort queue. Called from worker + * + * This would happen after an unrecoverable error, or when + * the queue is terminated by the client. Workers never exit normally, + * except when the queue is shut down (at which point ok is set to + * false by the shutdown code anyway). The thread must return/exit + * immediately after calling this. + */ + void workerExit() + { + const ScopeLock protect(mutex); + + n_workers_exited++; + ok = false; + client_cond.broadcast(); + } +}; + +#endif /* _WORKQUEUE_H_INCLUDED_ */ diff --git a/src/lib/upnp/ixmlwrap.cxx b/src/lib/upnp/ixmlwrap.cxx new file mode 100644 index 000000000..6a2829cf9 --- /dev/null +++ b/src/lib/upnp/ixmlwrap.cxx @@ -0,0 +1,44 @@ +/* Copyright (C) 2013 J.F.Dockes + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include "ixmlwrap.hxx" + +namespace ixmlwrap { + +const char * +getFirstElementValue(IXML_Document *doc, const char *name) +{ + const char *ret = nullptr; + IXML_NodeList *nodes = + ixmlDocument_getElementsByTagName(doc, name); + + if (nodes) { + IXML_Node *first = ixmlNodeList_item(nodes, 0); + if (first) { + IXML_Node *dnode = ixmlNode_getFirstChild(first); + if (dnode) { + ret = ixmlNode_getNodeValue(dnode); + } + } + + ixmlNodeList_free(nodes); + } + + return ret; +} + +} diff --git a/src/lib/upnp/ixmlwrap.hxx b/src/lib/upnp/ixmlwrap.hxx new file mode 100644 index 000000000..0d519a323 --- /dev/null +++ b/src/lib/upnp/ixmlwrap.hxx @@ -0,0 +1,35 @@ +/* Copyright (C) 2013 J.F.Dockes + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#ifndef _IXMLWRAP_H_INCLUDED_ +#define _IXMLWRAP_H_INCLUDED_ + +#include <upnp/ixml.h> + +#include <string> + +namespace ixmlwrap { + /** + * Retrieve the text content for the first element of given + * name. Returns nullptr if the element does not + * contain a text node + */ + const char *getFirstElementValue(IXML_Document *doc, + const char *name); + +}; + +#endif /* _IXMLWRAP_H_INCLUDED_ */ |