Commit f8a39300 authored by Hugo Beauzée-Luyssen's avatar Hugo Beauzée-Luyssen

Add a IDeviceLister interface.

This will be used on OSes that make it painfull to list devices, be it
because of permissions or lack of APIs or anything else.
parent 371da0c8
......@@ -55,6 +55,7 @@ libmedialibrary_la_SOURCES = \
src/discoverer/DiscovererWorker.cpp \
src/discoverer/FsDiscoverer.cpp \
src/factory/FileSystem.cpp \
src/factory/DeviceListerFactory.cpp \
src/filesystem/common/CommonFile.cpp \
src/logging/IostreamLogger.cpp \
src/logging/Logger.cpp \
......@@ -107,11 +108,13 @@ noinst_HEADERS = \
src/discoverer/DiscovererWorker.h \
src/discoverer/FsDiscoverer.h \
src/factory/FileSystem.h \
src/factory/DeviceListerFactory.h \
src/File.h \
src/filesystem/common/CommonFile.h \
src/filesystem/unix/Device.h \
src/filesystem/unix/Directory.h \
src/filesystem/unix/File.h \
src/filesystem/unix/DeviceLister.h \
src/filesystem/win32/Directory.h \
src/filesystem/win32/File.h \
src/Folder.h \
......@@ -155,7 +158,9 @@ libmedialibrary_la_SOURCES += \
src/filesystem/unix/File.cpp
if HAVE_LINUX
libmedialibrary_la_SOURCES += \
src/filesystem/unix/Device.cpp
src/filesystem/unix/Device.cpp \
src/filesystem/unix/DeviceLister.cpp \
$(NULL)
endif
if HAVE_DARWIN
libmedialibrary_macos_la_SOURCES = \
......
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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.
*****************************************************************************/
#pragma once
#include <tuple>
#include <vector>
namespace medialibrary
{
class IDeviceListerCb
{
public:
virtual ~IDeviceListerCb() = default;
/**
* @brief onDevicePlugged Shall be invoked when a known device gets plugged
* @param uuid The device UUID
* @param mountpoint The device new mountpoint
*/
virtual void onDevicePlugged( const std::string& uuid, const std::string& mountpoint ) = 0;
/**
* @brief onDeviceUnplugged Shall be invoked when a known device gets unplugged
* @param uuid The device UUID
*/
virtual void onDeviceUnplugged( const std::string& uuid ) = 0;
};
class IDeviceLister
{
public:
virtual ~IDeviceLister() = default;
/**
* @brief devices Returns a tuple containing:
* - The device UUID
* - The device mountpoint
* - A 'removable' state, being true if the device can be removed, false otherwise.
* @return
*/
virtual std::vector<std::tuple<std::string, std::string, bool>> devices() const = 0;
};
}
......@@ -220,6 +220,20 @@ class IMediaLibrary
virtual void resumeBackgroundOperations() = 0;
virtual void reload() = 0;
virtual void reload( const std::string& entryPoint ) = 0;
/**
* @brief setDeviceLister Sets a device lister.
* This is meant for OSes with complicated/impossible to achieve device listing (due to
* missing APIs, permissions problems...)
* @param lister A device lister
* @return In case of successfull registration, this will return a IDeviceListerCb, which
* can be used to signal changes related to the devices available. This callback is owned by
* the medialibrary, and must *NOT* be released by the application.
* In case of failure, nullptr will be returned.
*
* This must be called *before* initialize()
*/
virtual IDeviceListerCb* setDeviceLister( DeviceListerPtr lister ) = 0;
};
}
......
......@@ -44,6 +44,8 @@ class ILogger;
class IArtist;
class IPlaylist;
class IMediaLibraryCb;
class IDeviceLister;
class IDeviceListerCb;
using AlbumPtr = std::shared_ptr<IAlbum>;
using AlbumTrackPtr = std::shared_ptr<IAlbumTrack>;
......@@ -59,6 +61,7 @@ using PlaylistPtr = std::shared_ptr<IPlaylist>;
using ShowEpisodePtr = std::shared_ptr<IShowEpisode>;
using ShowPtr = std::shared_ptr<IShow>;
using VideoTrackPtr = std::shared_ptr<IVideoTrack>;
using DeviceListerPtr = std::shared_ptr<IDeviceLister>;
}
......@@ -59,6 +59,8 @@
#include "metadata_services/vlc/VLCThumbnailer.h"
#include "metadata_services/MetadataParser.h"
// FileSystem
#include "factory/DeviceListerFactory.h"
#include "factory/FileSystem.h"
namespace medialibrary
......@@ -195,8 +197,14 @@ bool MediaLibrary::validateSearchPattern( const std::string& pattern )
bool MediaLibrary::initialize( const std::string& dbPath, const std::string& thumbnailPath, IMediaLibraryCb* mlCallback )
{
if ( m_deviceLister == nullptr )
{
m_deviceLister = factory::createDeviceLister();
if ( m_deviceLister == nullptr )
return false;
}
if ( m_fsFactory == nullptr )
m_fsFactory.reset( new factory::FileSystemFactory );
m_fsFactory.reset( new factory::FileSystemFactory( m_deviceLister ) );
Folder::setFileSystemFactory( m_fsFactory );
if ( mkdir( thumbnailPath.c_str(), S_IRWXU ) != 0 )
{
......@@ -582,6 +590,12 @@ std::shared_ptr<ModificationNotifier> MediaLibrary::getNotifier() const
return m_modificationNotifier;
}
IDeviceListerCb* MediaLibrary::setDeviceLister( DeviceListerPtr lister )
{
m_deviceLister = lister;
return static_cast<IDeviceListerCb*>( this );
}
void MediaLibrary::discover( const std::string &entryPoint )
{
if ( m_discoverer != nullptr )
......@@ -621,4 +635,12 @@ void MediaLibrary::setLogger( ILogger* logger )
Log::SetLogger( logger );
}
void MediaLibrary::onDevicePlugged( const std::string&, const std::string& )
{
}
void MediaLibrary::onDeviceUnplugged( const std::string& )
{
}
}
......@@ -27,6 +27,8 @@
#include "logging/Logger.h"
#include "Settings.h"
#include <IDeviceLister.h>
namespace medialibrary
{
......@@ -55,7 +57,7 @@ class IFile;
class IDirectory;
}
class MediaLibrary : public IMediaLibrary
class MediaLibrary : public IMediaLibrary, public IDeviceListerCb
{
public:
MediaLibrary();
......@@ -125,6 +127,8 @@ class MediaLibrary : public IMediaLibrary
IMediaLibraryCb* getCb() const;
std::shared_ptr<ModificationNotifier> getNotifier() const;
virtual IDeviceListerCb* setDeviceLister( DeviceListerPtr lister ) override;
public:
static const uint32_t DbModelVersion;
......@@ -141,11 +145,17 @@ class MediaLibrary : public IMediaLibrary
void registerEntityHooks();
static bool validateSearchPattern( const std::string& pattern );
// Mark IDeviceListerCb callbacks as private. They must be invoked through the interface.
private:
virtual void onDevicePlugged(const std::string& uuid, const std::string& mountpoint) override;
virtual void onDeviceUnplugged(const std::string& uuid) override;
protected:
std::unique_ptr<SqliteConnection> m_dbConnection;
std::shared_ptr<factory::IFileSystem> m_fsFactory;
std::string m_thumbnailPath;
IMediaLibraryCb* m_callback;
DeviceListerPtr m_deviceLister;
// Keep the parser as last field.
// The parser holds a (raw) pointer to the media library. When MediaLibrary's destructor gets called
......
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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 "DeviceListerFactory.h"
#if defined(__linux__) && !defined(__ANDROID__)
#include "filesystem/unix/DeviceLister.h"
#endif
medialibrary::DeviceListerPtr medialibrary::factory::createDeviceLister()
{
#if defined(__linux__) && !defined(__ANDROID__)
return std::make_shared<fs::DeviceLister>();
#endif
return nullptr;
}
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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.
*****************************************************************************/
#pragma once
#include "medialibrary/Types.h"
namespace medialibrary
{
namespace factory
{
DeviceListerPtr createDeviceLister();
}
}
......@@ -42,6 +42,11 @@ namespace medialibrary
namespace factory
{
FileSystemFactory::FileSystemFactory( DeviceListerPtr lister )
{
fs::Device::setDeviceLister( lister );
}
std::shared_ptr<fs::IDirectory> FileSystemFactory::createDirectory( const std::string& path )
{
std::lock_guard<std::mutex> lock( m_mutex );
......@@ -68,7 +73,6 @@ std::shared_ptr<fs::IDevice> FileSystemFactory::createDevice( const std::string&
void FileSystemFactory::refresh()
{
fs::Device::populateCache();
std::lock_guard<std::mutex> lock( m_mutex );
m_dirs.clear();
}
......
......@@ -23,6 +23,7 @@
#pragma once
#include "factory/IFileSystem.h"
#include "medialibrary/Types.h"
#include <mutex>
#include <string>
......@@ -36,6 +37,7 @@ namespace factory
class FileSystemFactory : public IFileSystem
{
public:
FileSystemFactory( DeviceListerPtr lister );
virtual std::shared_ptr<fs::IDirectory> createDirectory( const std::string& path ) override;
virtual std::shared_ptr<fs::IDevice> createDevice( const std::string& uuid ) override;
virtual void refresh() override;
......
......@@ -22,17 +22,8 @@
#include "Device.h"
#include "Directory.h"
#include "utils/Filename.h"
#include "logging/Logger.h"
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <dirent.h>
#include <limits.h>
#include <mntent.h>
#include <unistd.h>
#include <sys/types.h>
#include "IDeviceLister.h"
namespace
{
......@@ -50,9 +41,8 @@ namespace medialibrary
namespace fs
{
Device::DeviceMap Device::Devices;
Device::MountpointMap Device::Mountpoints;
Device::DeviceCacheMap Device::DeviceCache;
Cache<Device::DeviceCacheMap> Device::DeviceCache;
DeviceListerPtr Device::DeviceLister;
Device::Device( const std::string& uuid, const std::string& mountpoint, bool isRemovable )
: m_uuid( uuid )
......@@ -86,8 +76,9 @@ const std::string&Device::mountpoint() const
std::shared_ptr<IDevice> Device::fromPath( const std::string& path )
{
auto lock = DeviceCache.lock();
std::shared_ptr<IDevice> res;
for ( const auto& p : DeviceCache )
for ( const auto& p : DeviceCache.get() )
{
if ( path.find( p.second->mountpoint() ) == 0 )
{
......@@ -100,197 +91,40 @@ std::shared_ptr<IDevice> Device::fromPath( const std::string& path )
std::shared_ptr<IDevice> Device::fromUuid( const std::string& uuid )
{
auto it = DeviceCache.find( uuid );
if ( it != end( DeviceCache ) )
auto lock = DeviceCache.lock();
auto it = DeviceCache.get().find( uuid );
if ( it != end( DeviceCache.get() ) )
return it->second;
return nullptr;
}
bool Device::populateCache()
void Device::setDeviceLister( DeviceListerPtr lister )
{
Devices = listDevices();
Mountpoints = listMountpoints();
DeviceCache = populateDeviceCache();
return true;
auto lock = DeviceCache.lock();
DeviceLister = lister;
refreshDeviceCacheLocked();
}
Device::DeviceMap Device::listDevices()
void Device::refreshDeviceCache()
{
static const std::vector<std::string> deviceBlacklist = { "loop", "dm-" };
const std::string devPath = "/dev/disk/by-uuid/";
// Don't use fs::Directory to iterate, as it resolves the symbolic links automatically.
// We need the link name & what it points to.
std::unique_ptr<DIR, int(*)(DIR*)> dir( opendir( devPath.c_str() ), &closedir );
if ( dir == nullptr )
{
std::stringstream err;
err << "Failed to open /dev/disk/by-uuid: " << strerror(errno);
throw std::runtime_error( err.str() );
}
DeviceMap res;
dirent* result = nullptr;
while ( ( result = readdir( dir.get() ) ) != nullptr )
{
if ( strcmp( result->d_name, "." ) == 0 ||
strcmp( result->d_name, ".." ) == 0 )
{
continue;
}
std::string path = devPath + result->d_name;
char linkPath[PATH_MAX] = {};
if ( readlink( path.c_str(), linkPath, PATH_MAX ) < 0 )
{
std::stringstream err;
err << "Failed to resolve uuid -> device link: "
<< result->d_name << " (" << strerror(errno) << ')';
throw std::runtime_error( err.str() );
}
auto deviceName = utils::file::fileName( linkPath );
if ( std::find_if( begin( deviceBlacklist ), end( deviceBlacklist ), [&deviceName]( const std::string& pattern ) {
return deviceName.length() >= pattern.length() && deviceName.find( pattern ) == 0;
}) != end( deviceBlacklist ) )
continue;
auto uuid = result->d_name;
LOG_INFO( "Discovered device ", deviceName, " -> {", uuid, '}' );
res[deviceName] = uuid;
}
return res;
}
Device::MountpointMap Device::listMountpoints()
{
static const std::vector<std::string> allowedFsType = { "vfat", "exfat", "sdcardfs", "fuse",
"ntfs", "fat32", "ext3", "ext4", "esdfs" };
MountpointMap res;
errno = 0;
mntent* s;
#ifndef __ANDROID__
char buff[512];
mntent smnt;
FILE* f = setmntent("/etc/mtab", "r");
if ( f == nullptr )
throw std::runtime_error( "Failed to read /etc/mtab" );
std::unique_ptr<FILE, int(*)(FILE*)> fPtr( f, &endmntent );
while ( getmntent_r( f, &smnt, buff, sizeof(buff) ) != nullptr )
{
s = &smnt;
#else
FILE* f = fopen( "/proc/mounts", "r" );
std::unique_ptr<FILE, int(*)(FILE*)> fPtr( f, &fclose );
while ( ( s = getmntent( f ) ) )
{
#endif
if ( std::find( begin( allowedFsType ), end( allowedFsType ), s->mnt_type ) == end( allowedFsType ) )
continue;
auto deviceName = s->mnt_fsname;
LOG_INFO( "Discovered mountpoint ", deviceName, " mounted on ", s->mnt_dir, " (", s->mnt_type, ')' );
res[deviceName] = s->mnt_dir;
errno = 0;
}
if ( errno != 0 )
{
LOG_ERROR( "Failed to read mountpoints: ", strerror( errno ) );
}
return res;
auto lock = DeviceCache.lock();
refreshDeviceCacheLocked();
}
Device::DeviceCacheMap Device::populateDeviceCache()
void Device::refreshDeviceCacheLocked()
{
Device::DeviceCacheMap res;
for ( const auto& p : Mountpoints )
{
const auto& devicePath = p.first;
auto deviceName = utils::file::fileName( devicePath );
const auto& mountpoint = p.second;
auto it = Devices.find( deviceName );
std::string uuid;
if ( it != end( Devices ) )
uuid = it->second;
else
{
#ifndef __ANDROID__
deviceName = deviceFromDeviceMapper( devicePath );
it = Devices.find( deviceName );
if ( it != end( Devices ) )
uuid = it->second;
else
#endif
{
LOG_ERROR( "Failed to resolve mountpoint ", mountpoint, " to any known device" );
continue;
}
}
auto removable = isRemovable( deviceName, mountpoint );
LOG_INFO( "Adding device to cache: {", uuid, "} mounted on ", mountpoint, " Removable: ", removable );
res[uuid] = std::make_shared<DeviceBuilder>( uuid, mountpoint, removable );
}
return res;
}
std::string Device::deviceFromDeviceMapper( const std::string& devicePath )
{
if ( devicePath.find( "/dev/mapper" ) != 0 )
return {};
char linkPath[PATH_MAX];
if ( readlink( devicePath.c_str(), linkPath, PATH_MAX ) < 0 )
{
std::stringstream err;
err << "Failed to resolve device -> mapper link: "
<< devicePath << " (" << strerror(errno) << ')';
throw std::runtime_error( err.str() );
}
const auto dmName = utils::file::fileName( linkPath );
std::string dmSlavePath = "/sys/block/" + dmName + "/slaves";
std::unique_ptr<DIR, int(*)(DIR*)> dir( opendir( dmSlavePath.c_str() ), &closedir );
std::string res;
if ( dir == nullptr )
return {};
dirent* result;
while ( ( result = readdir( dir.get() ) ) != nullptr )
{
if ( strcmp( result->d_name, "." ) == 0 ||
strcmp( result->d_name, ".." ) == 0 )
{
continue;
}
if ( res.empty() == true )
res = result->d_name;
else
LOG_WARN( "More than one slave for device mapper ", linkPath );
}
LOG_INFO( "Device mapper ", dmName, " maps to ", res );
return res;
}
bool Device::isRemovable( const std::string& deviceName, const std::string& mountpoint )
{
#ifndef TIZEN
(void)mountpoint;
std::stringstream removableFilePath;
removableFilePath << "/sys/block/" << deviceName << "/removable";
std::unique_ptr<FILE, int(*)(FILE*)> removableFile( fopen( removableFilePath.str().c_str(), "r" ), &fclose );
// Assume the file isn't removable by default
if ( removableFile != nullptr )
{
char buff;
if ( fread(&buff, sizeof(buff), 1, removableFile.get() ) != 1 )
return buff == '1';
return false;
}
#else
(void)deviceName;
static const std::vector<std::string> SDMountpoints = { "/opt/storage/sdcard" };
auto it = std::find_if( begin( SDMountpoints ), end( SDMountpoints ), [mountpoint]( const std::string& pattern ) {
return mountpoint.length() >= pattern.length() && mountpoint.find( pattern ) == 0;
});
if ( it != end( SDMountpoints ) )
if ( DeviceCache.isCached() == false )
DeviceCache = DeviceCacheMap{};
DeviceCache.get().clear();
auto devices = DeviceLister->devices();
for ( const auto& d : devices )
{
LOG_INFO( "Considering mountpoint ", mountpoint, " a removable SDCard" );
return true;
const auto& uuid = std::get<0>( d );
const auto& mountpoint = std::get<1>( d );
const auto removable = std::get<2>( d );
DeviceCache.get().emplace( uuid, std::make_shared<DeviceBuilder>( uuid, mountpoint, removable ) );
}
#endif
return false;
}
}
......
......@@ -23,6 +23,9 @@
#pragma once
#include "filesystem/IDevice.h"
#include "medialibrary/Types.h"
#include "utils/Cache.h"
#include <memory>
#include <unordered_map>
......@@ -34,10 +37,6 @@ namespace fs
class Device : public IDevice
{
// Device name / UUID map
using DeviceMap = std::unordered_map<std::string, std::string>;
// Device path / Mountpoints map
using MountpointMap = std::unordered_map<std::string, std::string>;
// UUID -> Device instance map
using DeviceCacheMap = std::unordered_map<std::string, std::shared_ptr<IDevice>>;
......@@ -52,22 +51,19 @@ public:
///
static std::shared_ptr<IDevice> fromPath( const std::string& path );
static std::shared_ptr<IDevice> fromUuid( const std::string& uuid );
static bool populateCache();
static void setDeviceLister( DeviceListerPtr lister );
static void refreshDeviceCache();
protected:
Device( const std::string& uuid, const std::string& mountpoint, bool isRemovable );
private:
static DeviceMap listDevices();
static MountpointMap listMountpoints();
static DeviceCacheMap populateDeviceCache();
static std::string deviceFromDeviceMapper( const std::string& devicePath );
static bool isRemovable( const std::string& deviceName, const std::string& mountpoint );
static void refreshDeviceCacheLocked();
private:
static DeviceMap Devices;
static MountpointMap Mountpoints;
static DeviceCacheMap DeviceCache;
static Cache<DeviceCacheMap> DeviceCache;
static DeviceListerPtr DeviceLister;
private:
std::string m_uuid;
......
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "DeviceLister.h"
#include "logging/Logger.h"
#include "utils/Filename.h"
#include <dirent.h>
#include <mntent.h>
#include <sys/types.h>
#include <vector>
#include <memory>
#include <cerrno>
#include <sstream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <limits.h>
#include <unistd.h>
namespace medialibrary
{
namespace fs
{
DeviceLister::DeviceMap DeviceLister::listDevices() const
{
static const std::vector<std::string> deviceBlacklist = { "loop", "dm-" };
const std::string devPath = "/dev/disk/by-uuid/";
// Don't use fs::Directory to iterate, as it resolves the symbolic links automatically.
// We need the link name & what it points to.
std::unique_ptr<DIR, int(*)(DIR*)> dir( opendir( devPath.c_str() ), &closedir );
if ( dir == nullptr )
{
std::stringstream err;
err << "Failed to open /dev/disk/by-uuid: " << strerror(errno);
throw std::runtime_error( err.str() );
}
DeviceMap res;
dirent* result = nullptr;
while ( ( result = readdir( dir.get() ) ) != nullptr )
{
if ( strcmp( result->d_name, "." ) == 0 ||
strcmp( result->d_name, ".." ) == 0 )
{
continue;
}
std::string path = devPath + result->d_name;
char linkPath[PATH_MAX] = {};
if ( readlink( path.c_str(), linkPath, PATH_MAX ) < 0 )
{
std::stringstream err;
err << "Failed to resolve uuid -> device link: "
<< result->d_name << " (" << strerror(errno) << ')';
throw std::runtime_error( err.str() );
}
auto deviceName = utils::file::fileName( linkPath );
if ( std::find_if( begin( deviceBlacklist ), end( deviceBlacklist ), [&deviceName]( const std::string& pattern ) {
return deviceName.length() >= pattern.length() && deviceName.find( pattern ) == 0;
}) != end( deviceBlacklist ) )
continue;
auto uuid = result->d_name;
LOG_INFO( "Discovered device "