diff options
Diffstat (limited to 'xbmc/platform/linux')
73 files changed, 8875 insertions, 0 deletions
diff --git a/xbmc/platform/linux/AppParamParserLinux.cpp b/xbmc/platform/linux/AppParamParserLinux.cpp new file mode 100644 index 0000000..3a1adf9 --- /dev/null +++ b/xbmc/platform/linux/AppParamParserLinux.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "AppParamParserLinux.h" + +#include "CompileInfo.h" +#include "application/AppParams.h" +#include "utils/StringUtils.h" + +#include <algorithm> +#include <array> +#include <iostream> +#include <vector> + +namespace +{ +std::vector<std::string> availableWindowSystems = CCompileInfo::GetAvailableWindowSystems(); +std::array<std::string, 1> availableLogTargets = {"console"}; + +constexpr const char* windowingText = + R"""( +Selected window system not available: {} + Available window systems: {} +)"""; + +constexpr const char* loggingText = + R"""( +Selected logging target not available: {} + Available log targest: {} +)"""; + +constexpr const char* helpText = + R"""( +Linux Specific Arguments: + --windowing=<system> Select which windowing method to use. + Available window systems are: {} + --logging=<target> Select which log target to use (log file will always be used in conjunction). + Available log targets are: {} +)"""; + + +} // namespace + +CAppParamParserLinux::CAppParamParserLinux() : CAppParamParser() +{ +} + +CAppParamParserLinux::~CAppParamParserLinux() = default; + +void CAppParamParserLinux::ParseArg(const std::string& arg) +{ + CAppParamParser::ParseArg(arg); + + if (arg.substr(0, 12) == "--windowing=") + { + if (std::find(availableWindowSystems.begin(), availableWindowSystems.end(), arg.substr(12)) != + availableWindowSystems.end()) + GetAppParams()->SetWindowing(arg.substr(12)); + else + { + std::cout << StringUtils::Format(windowingText, arg.substr(12), + StringUtils::Join(availableWindowSystems, ", ")); + exit(0); + } + } + else if (arg.substr(0, 10) == "--logging=") + { + if (std::find(availableLogTargets.begin(), availableLogTargets.end(), arg.substr(10)) != + availableLogTargets.end()) + { + GetAppParams()->SetLogTarget(arg.substr(10)); + } + else + { + std::cout << StringUtils::Format(loggingText, arg.substr(10), + StringUtils::Join(availableLogTargets, ", ")); + exit(0); + } + } +} + +void CAppParamParserLinux::DisplayHelp() +{ + CAppParamParser::DisplayHelp(); + + std::cout << StringUtils::Format(helpText, StringUtils::Join(availableWindowSystems, ", "), + StringUtils::Join(availableLogTargets, ", ")); +} diff --git a/xbmc/platform/linux/AppParamParserLinux.h b/xbmc/platform/linux/AppParamParserLinux.h new file mode 100644 index 0000000..ea866ca --- /dev/null +++ b/xbmc/platform/linux/AppParamParserLinux.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2005-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "application/AppParamParser.h" + +class CAppParamParserLinux : public CAppParamParser +{ +public: + CAppParamParserLinux(); + ~CAppParamParserLinux(); + +protected: + void ParseArg(const std::string& arg) override; + void DisplayHelp() override; +}; diff --git a/xbmc/platform/linux/CMakeLists.txt b/xbmc/platform/linux/CMakeLists.txt new file mode 100644 index 0000000..31f4cef --- /dev/null +++ b/xbmc/platform/linux/CMakeLists.txt @@ -0,0 +1,32 @@ +set(SOURCES AppParamParserLinux.cpp + CPUInfoLinux.cpp + MemUtils.cpp + OptionalsReg.cpp + PlatformLinux.cpp + SysfsPath.cpp + TimeUtils.cpp) + +set(HEADERS AppParamParserLinux.h + CPUInfoLinux.h + OptionalsReg.h + PlatformLinux.h + SysfsPath.h + TimeUtils.h) + +if(ALSA_FOUND) + list(APPEND SOURCES FDEventMonitor.cpp) + list(APPEND HEADERS FDEventMonitor.h) +endif() + +if(DBUS_FOUND) + list(APPEND SOURCES DBusMessage.cpp + DBusUtil.cpp) + list(APPEND HEADERS DBusMessage.h + DBusUtil.h) +endif() + +if(ADDONS_CONFIGURE_AT_STARTUP) + add_compile_definitions(ADDONS_CONFIGURE_AT_STARTUP) +endif() + +core_add_library(linuxsupport) diff --git a/xbmc/platform/linux/CPUInfoLinux.cpp b/xbmc/platform/linux/CPUInfoLinux.cpp new file mode 100644 index 0000000..ef34bbd --- /dev/null +++ b/xbmc/platform/linux/CPUInfoLinux.cpp @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CPUInfoLinux.h" + +#include "utils/StringUtils.h" +#include "utils/Temperature.h" + +#include "platform/linux/SysfsPath.h" + +#include <exception> +#include <fstream> +#include <regex> +#include <sstream> +#include <vector> + +#if (defined(__arm__) && defined(HAS_NEON)) || defined(__aarch64__) +#include <asm/hwcap.h> +#include <sys/auxv.h> +#elif defined(__i386__) || defined(__x86_64__) +#include <cpuid.h> +#endif + +#include <unistd.h> + +namespace +{ +enum CpuStates +{ + STATE_USER, + STATE_NICE, + STATE_SYSTEM, + STATE_IDLE, + STATE_IOWAIT, + STATE_IRQ, + STATE_SOFTIRQ, + STATE_STEAL, + STATE_GUEST, + STATE_GUEST_NICE, + STATE_MAX +}; + +struct CpuData +{ +public: + std::size_t GetActiveTime() const + { + return state[STATE_USER] + state[STATE_NICE] + state[STATE_SYSTEM] + state[STATE_IRQ] + + state[STATE_SOFTIRQ] + state[STATE_STEAL] + state[STATE_GUEST] + state[STATE_GUEST_NICE]; + } + + std::size_t GetIdleTime() const { return state[STATE_IDLE] + state[STATE_IOWAIT]; } + + std::size_t GetTotalTime() const { return GetActiveTime() + GetIdleTime(); } + + std::string cpu; + std::size_t state[STATE_MAX]; +}; +} // namespace + +std::shared_ptr<CCPUInfo> CCPUInfo::GetCPUInfo() +{ + return std::make_shared<CCPUInfoLinux>(); +} + +CCPUInfoLinux::CCPUInfoLinux() +{ + CSysfsPath machinePath{"/sys/bus/soc/devices/soc0/machine"}; + if (machinePath.Exists()) + m_cpuHardware = machinePath.Get<std::string>().value_or(""); + + CSysfsPath familyPath{"/sys/bus/soc/devices/soc0/family"}; + if (familyPath.Exists()) + m_cpuSoC = familyPath.Get<std::string>().value_or(""); + + CSysfsPath socPath{"/sys/bus/soc/devices/soc0/soc_id"}; + if (socPath.Exists()) + m_cpuSoC += " " + socPath.Get<std::string>().value_or(""); + + CSysfsPath revisionPath{"/sys/bus/soc/devices/soc0/revision"}; + if (revisionPath.Exists()) + m_cpuRevision = revisionPath.Get<std::string>().value_or(""); + + CSysfsPath serialPath{"/sys/bus/soc/devices/soc0/serial_number"}; + if (serialPath.Exists()) + m_cpuSerial = serialPath.Get<std::string>().value_or(""); + + const std::string freqStr{"/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"}; + CSysfsPath freqPath{freqStr}; + if (freqPath.Exists()) + m_freqPath = freqStr; + + const std::array<std::string, 4> modules = { + "coretemp", + "k10temp", + "scpi_sensors", + "imx_thermal_zone", + }; + + for (int i = 0; i < 20; i++) + { + CSysfsPath path{"/sys/class/hwmon/hwmon" + std::to_string(i) + "/name"}; + if (!path.Exists()) + continue; + + auto name = path.Get<std::string>(); + + if (!name.has_value()) + continue; + + for (const auto& module : modules) + { + if (module == name) + { + std::string tempStr{"/sys/class/hwmon/hwmon" + std::to_string(i) + "/temp1_input"}; + CSysfsPath tempPath{tempStr}; + if (!tempPath.Exists()) + continue; + + m_tempPath = tempStr; + break; + } + } + + if (!m_tempPath.empty()) + break; + } + + m_cpuCount = sysconf(_SC_NPROCESSORS_ONLN); + + for (int core = 0; core < m_cpuCount; core++) + { + CoreInfo coreInfo; + coreInfo.m_id = core; + m_cores.emplace_back(coreInfo); + } + +#if defined(__i386__) || defined(__x86_64__) + unsigned int eax; + unsigned int ebx; + unsigned int ecx; + unsigned int edx; + + m_cpuVendor.clear(); + + if (__get_cpuid(CPUID_INFOTYPE_MANUFACTURER, &eax, &ebx, &ecx, &edx)) + { + m_cpuVendor.append(reinterpret_cast<const char*>(&ebx), 4); + m_cpuVendor.append(reinterpret_cast<const char*>(&edx), 4); + m_cpuVendor.append(reinterpret_cast<const char*>(&ecx), 4); + } + + if (__get_cpuid(CPUID_INFOTYPE_EXTENDED_IMPLEMENTED, &eax, &ebx, &ecx, &edx)) + { + if (eax >= CPUID_INFOTYPE_PROCESSOR_3) + { + m_cpuModel.clear(); + + if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_1, &eax, &ebx, &ecx, &edx)) + { + m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4); + } + + if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_2, &eax, &ebx, &ecx, &edx)) + { + m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4); + } + + if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_3, &eax, &ebx, &ecx, &edx)) + { + m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4); + m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4); + } + } + } + + if (__get_cpuid(CPUID_INFOTYPE_STANDARD, &eax, &eax, &ecx, &edx)) + { + if (edx & CPUID_00000001_EDX_MMX) + m_cpuFeatures |= CPU_FEATURE_MMX; + + if (edx & CPUID_00000001_EDX_SSE) + m_cpuFeatures |= CPU_FEATURE_SSE; + + if (edx & CPUID_00000001_EDX_SSE2) + m_cpuFeatures |= CPU_FEATURE_SSE2; + + if (ecx & CPUID_00000001_ECX_SSE3) + m_cpuFeatures |= CPU_FEATURE_SSE3; + + if (ecx & CPUID_00000001_ECX_SSSE3) + m_cpuFeatures |= CPU_FEATURE_SSSE3; + + if (ecx & CPUID_00000001_ECX_SSE4) + m_cpuFeatures |= CPU_FEATURE_SSE4; + + if (ecx & CPUID_00000001_ECX_SSE42) + m_cpuFeatures |= CPU_FEATURE_SSE42; + } + + if (__get_cpuid(CPUID_INFOTYPE_EXTENDED_IMPLEMENTED, &eax, &eax, &ecx, &edx)) + { + if (eax >= CPUID_INFOTYPE_EXTENDED) + { + if (edx & CPUID_80000001_EDX_MMX) + m_cpuFeatures |= CPU_FEATURE_MMX; + + if (edx & CPUID_80000001_EDX_MMX2) + m_cpuFeatures |= CPU_FEATURE_MMX2; + + if (edx & CPUID_80000001_EDX_3DNOW) + m_cpuFeatures |= CPU_FEATURE_3DNOW; + + if (edx & CPUID_80000001_EDX_3DNOWEXT) + m_cpuFeatures |= CPU_FEATURE_3DNOWEXT; + } + } +#else + std::ifstream cpuinfo("/proc/cpuinfo"); + std::regex re(".*: (.*)$"); + + for (std::string line; std::getline(cpuinfo, line);) + { + std::smatch match; + + if (std::regex_match(line, match, re)) + { + if (match.size() == 2) + { + std::ssub_match value = match[1]; + + if (line.find("model name") != std::string::npos) + { + if (m_cpuModel.empty()) + m_cpuModel = value.str(); + } + + if (line.find("BogoMIPS") != std::string::npos) + { + if (m_cpuBogoMips.empty()) + m_cpuBogoMips = value.str(); + } + + if (line.find("Hardware") != std::string::npos) + { + if (m_cpuHardware.empty()) + m_cpuHardware = value.str(); + } + + if (line.find("Serial") != std::string::npos) + { + if (m_cpuSerial.empty()) + m_cpuSerial = value.str(); + } + + if (line.find("Revision") != std::string::npos) + { + if (m_cpuRevision.empty()) + m_cpuRevision = value.str(); + } + } + } + } +#endif + + m_cpuModel = m_cpuModel.substr(0, m_cpuModel.find(char(0))); // remove extra null terminations + +#if defined(HAS_NEON) && defined(__arm__) + if (getauxval(AT_HWCAP) & HWCAP_NEON) + m_cpuFeatures |= CPU_FEATURE_NEON; +#endif + +#if defined(HAS_NEON) && defined(__aarch64__) + if (getauxval(AT_HWCAP) & HWCAP_ASIMD) + m_cpuFeatures |= CPU_FEATURE_NEON; +#endif + + // Set MMX2 when SSE is present as SSE is a superset of MMX2 and Intel doesn't set the MMX2 cap + if (m_cpuFeatures & CPU_FEATURE_SSE) + m_cpuFeatures |= CPU_FEATURE_MMX2; +} + +int CCPUInfoLinux::GetUsedPercentage() +{ + if (!m_nextUsedReadTime.IsTimePast()) + return m_lastUsedPercentage; + + std::vector<CpuData> cpuData; + + std::ifstream infile("/proc/stat"); + + for (std::string line; std::getline(infile, line);) + { + if (line.find("cpu") != std::string::npos) + { + std::istringstream ss(line); + CpuData info; + + ss >> info.cpu; + + for (int i = 0; i < STATE_MAX; i++) + { + ss >> info.state[i]; + } + + cpuData.emplace_back(info); + } + } + + auto activeTime = cpuData.front().GetActiveTime() - m_activeTime; + auto idleTime = cpuData.front().GetIdleTime() - m_idleTime; + auto totalTime = cpuData.front().GetTotalTime() - m_totalTime; + + m_activeTime += activeTime; + m_idleTime += idleTime; + m_totalTime += totalTime; + + m_lastUsedPercentage = activeTime * 100.0f / totalTime; + m_nextUsedReadTime.Set(MINIMUM_TIME_BETWEEN_READS); + + cpuData.erase(cpuData.begin()); + + for (std::size_t core = 0; core < cpuData.size(); core++) + { + auto activeTime = cpuData[core].GetActiveTime() - m_cores[core].m_activeTime; + auto idleTime = cpuData[core].GetIdleTime() - m_cores[core].m_idleTime; + auto totalTime = cpuData[core].GetTotalTime() - m_cores[core].m_totalTime; + + m_cores[core].m_usagePercent = activeTime * 100.0 / totalTime; + + m_cores[core].m_activeTime += activeTime; + m_cores[core].m_idleTime += idleTime; + m_cores[core].m_totalTime += totalTime; + } + + return static_cast<int>(m_lastUsedPercentage); +} + +float CCPUInfoLinux::GetCPUFrequency() +{ + if (m_freqPath.empty()) + return 0; + + auto freq = CSysfsPath(m_freqPath).Get<float>(); + return freq.has_value() ? *freq / 1000.0f : 0.0f; +} + +bool CCPUInfoLinux::GetTemperature(CTemperature& temperature) +{ + if (CheckUserTemperatureCommand(temperature)) + return true; + + if (m_tempPath.empty()) + return false; + + auto temp = CSysfsPath(m_tempPath).Get<double>(); + if (!temp.has_value()) + return false; + + double value = *temp / 1000.0; + + temperature = CTemperature::CreateFromCelsius(value); + temperature.SetValid(true); + + return true; +} diff --git a/xbmc/platform/linux/CPUInfoLinux.h b/xbmc/platform/linux/CPUInfoLinux.h new file mode 100644 index 0000000..ed79afc --- /dev/null +++ b/xbmc/platform/linux/CPUInfoLinux.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/Temperature.h" + +#include "platform/posix/CPUInfoPosix.h" + +#include <string> + +class CCPUInfoLinux : public CCPUInfoPosix +{ +public: + CCPUInfoLinux(); + ~CCPUInfoLinux() = default; + + int GetUsedPercentage() override; + float GetCPUFrequency() override; + bool GetTemperature(CTemperature& temperature) override; + +private: + std::string m_freqPath; + std::string m_tempPath; +}; diff --git a/xbmc/platform/linux/DBusMessage.cpp b/xbmc/platform/linux/DBusMessage.cpp new file mode 100644 index 0000000..ab09c3e --- /dev/null +++ b/xbmc/platform/linux/DBusMessage.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "DBusMessage.h" + +#include "DBusUtil.h" +#include "settings/AdvancedSettings.h" +#include "utils/log.h" + +CDBusMessage::CDBusMessage(const char *destination, const char *object, const char *interface, const char *method) +{ + m_message.reset(dbus_message_new_method_call(destination, object, interface, method)); + if (!m_message) + { + // Fails only due to memory allocation failure + throw std::runtime_error("dbus_message_new_method_call"); + } + m_haveArgs = false; + + CLog::Log(LOGDEBUG, LOGDBUS, "DBus: Creating message to {} on {} with interface {} and method {}", + destination, object, interface, method); +} + +CDBusMessage::CDBusMessage(const std::string& destination, const std::string& object, const std::string& interface, const std::string& method) +: CDBusMessage(destination.c_str(), object.c_str(), interface.c_str(), method.c_str()) +{ +} + +void DBusMessageDeleter::operator()(DBusMessage* message) const +{ + dbus_message_unref(message); +} + +void CDBusMessage::AppendObjectPath(const char *object) +{ + AppendWithType(DBUS_TYPE_OBJECT_PATH, &object); +} + +template<> +void CDBusMessage::AppendArgument<bool>(const bool arg) +{ + // dbus_bool_t width might not match C++ bool width + dbus_bool_t convArg = (arg == true); + AppendWithType(DBUS_TYPE_BOOLEAN, &convArg); +} + +template<> +void CDBusMessage::AppendArgument<std::string>(const std::string arg) +{ + AppendArgument(arg.c_str()); +} + +void CDBusMessage::AppendArgument(const char **arrayString, unsigned int length) +{ + PrepareArgument(); + DBusMessageIter sub; + if (!dbus_message_iter_open_container(&m_args, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &sub)) + { + throw std::runtime_error("dbus_message_iter_open_container"); + } + + for (unsigned int i = 0; i < length; i++) + { + if (!dbus_message_iter_append_basic(&sub, DBUS_TYPE_STRING, &arrayString[i])) + { + throw std::runtime_error("dbus_message_iter_append_basic"); + } + } + + if (!dbus_message_iter_close_container(&m_args, &sub)) + { + throw std::runtime_error("dbus_message_iter_close_container"); + } +} + +void CDBusMessage::AppendWithType(int type, const void* value) +{ + PrepareArgument(); + if (!dbus_message_iter_append_basic(&m_args, type, value)) + { + throw std::runtime_error("dbus_message_iter_append_basic"); + } +} + +DBusMessageIter * CDBusMessage::GetArgumentIter() { + PrepareArgument(); + return &m_args; +} + +DBusMessage *CDBusMessage::SendSystem() +{ + return Send(DBUS_BUS_SYSTEM); +} + +DBusMessage *CDBusMessage::SendSession() +{ + return Send(DBUS_BUS_SESSION); +} + +DBusMessage *CDBusMessage::SendSystem(CDBusError& error) +{ + return Send(DBUS_BUS_SYSTEM, error); +} + +DBusMessage *CDBusMessage::SendSession(CDBusError& error) +{ + return Send(DBUS_BUS_SESSION, error); +} + +bool CDBusMessage::SendAsyncSystem() +{ + return SendAsync(DBUS_BUS_SYSTEM); +} + +bool CDBusMessage::SendAsyncSession() +{ + return SendAsync(DBUS_BUS_SESSION); +} + +DBusMessage *CDBusMessage::Send(DBusBusType type) +{ + CDBusError error; + CDBusConnection con; + if (!con.Connect(type)) + return nullptr; + + DBusMessage *returnMessage = Send(con, error); + + if (error) + error.Log(); + + return returnMessage; +} + +DBusMessage *CDBusMessage::Send(DBusBusType type, CDBusError& error) +{ + CDBusConnection con; + if (!con.Connect(type, error)) + return nullptr; + + DBusMessage *returnMessage = Send(con, error); + + return returnMessage; +} + +bool CDBusMessage::SendAsync(DBusBusType type) +{ + CDBusConnection con; + if (!con.Connect(type)) + return false; + + return dbus_connection_send(con, m_message.get(), nullptr); +} + +DBusMessage *CDBusMessage::Send(DBusConnection *con, CDBusError& error) +{ + m_reply.reset(dbus_connection_send_with_reply_and_block(con, m_message.get(), -1, error)); + return m_reply.get(); +} + +void CDBusMessage::PrepareArgument() +{ + if (!m_haveArgs) + dbus_message_iter_init_append(m_message.get(), &m_args); + + m_haveArgs = true; +} + +bool CDBusMessage::InitializeReplyIter(DBusMessageIter* iter) +{ + if (!m_reply) + { + throw std::logic_error("Cannot get reply arguments of message that does not have reply"); + } + if (!dbus_message_iter_init(m_reply.get(), iter)) + { + CLog::Log(LOGWARNING, "Tried to obtain reply arguments from message that has zero arguments"); + return false; + } + return true; +} + +bool CDBusMessage::CheckTypeAndGetValue(DBusMessageIter* iter, int expectType, void* dest) +{ + const int haveType = dbus_message_iter_get_arg_type(iter); + if (haveType != expectType) + { + CLog::Log(LOGDEBUG, "DBus argument type mismatch: expected {}, got {}", expectType, haveType); + return false; + } + + dbus_message_iter_get_basic(iter, dest); + return true; +} diff --git a/xbmc/platform/linux/DBusMessage.h b/xbmc/platform/linux/DBusMessage.h new file mode 100644 index 0000000..80c7cd6 --- /dev/null +++ b/xbmc/platform/linux/DBusMessage.h @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <cstdint> +#include <memory> +#include <string> +#include <type_traits> + +#include <dbus/dbus.h> + +class CDBusError; + +template<typename T> +struct ToDBusType; +template<> struct ToDBusType<bool> { static constexpr int TYPE = DBUS_TYPE_BOOLEAN; }; +template<> struct ToDBusType<char*> { static constexpr int TYPE = DBUS_TYPE_STRING; }; +template<> struct ToDBusType<const char*> { static constexpr int TYPE = DBUS_TYPE_STRING; }; +template<> struct ToDBusType<std::uint8_t> { static constexpr int TYPE = DBUS_TYPE_BYTE; }; +template<> struct ToDBusType<std::int16_t> { static constexpr int TYPE = DBUS_TYPE_INT16; }; +template<> struct ToDBusType<std::uint16_t> { static constexpr int TYPE = DBUS_TYPE_UINT16; }; +template<> struct ToDBusType<std::int32_t> { static constexpr int TYPE = DBUS_TYPE_INT32; }; +template<> struct ToDBusType<std::uint32_t> { static constexpr int TYPE = DBUS_TYPE_UINT32; }; +template<> struct ToDBusType<std::int64_t> { static constexpr int TYPE = DBUS_TYPE_INT64; }; +template<> struct ToDBusType<std::uint64_t> { static constexpr int TYPE = DBUS_TYPE_UINT64; }; +template<> struct ToDBusType<double> { static constexpr int TYPE = DBUS_TYPE_DOUBLE; }; + +template<typename T> +using ToDBusTypeFromPointer = ToDBusType<typename std::remove_pointer<T>::type>; + +struct DBusMessageDeleter +{ + void operator()(DBusMessage* message) const; +}; +using DBusMessagePtr = std::unique_ptr<DBusMessage, DBusMessageDeleter>; + +class CDBusMessage +{ +public: + CDBusMessage(const char *destination, const char *object, const char *interface, const char *method); + CDBusMessage(std::string const& destination, std::string const& object, std::string const& interface, std::string const& method); + + void AppendObjectPath(const char *object); + + template<typename T> + void AppendArgument(const T arg) + { + AppendWithType(ToDBusType<T>::TYPE, &arg); + } + void AppendArgument(const char **arrayString, unsigned int length); + + template<typename TFirst> + void AppendArguments(const TFirst first) + { + AppendArgument(first); + // Recursion end + } + template<typename TFirst, typename... TArgs> + void AppendArguments(const TFirst first, const TArgs... args) + { + AppendArgument(first); + AppendArguments(args...); + } + + DBusMessageIter * GetArgumentIter(); + + /** + * Retrieve simple arguments from DBus reply message + * + * You MUST use the correct fixed-width integer typedefs (e.g. std::uint16_t) + * corresponding to the DBus types for the variables or you will get potentially + * differing behavior between architectures since the DBus argument type detection + * is based on the width of the type. + * + * Complex arguments (arrays, structs) are not supported. + * + * Returned pointers for strings are only valid until the instance of this class + * is deleted. + * + * \throw std::logic_error if the message has no reply + * \return whether all arguments could be retrieved (false on argument type + * mismatch or when more arguments were to be retrieved than there are + * in the message) + */ + template<typename... TArgs> + bool GetReplyArguments(TArgs*... args) + { + DBusMessageIter iter; + if (!InitializeReplyIter(&iter)) + { + return false; + } + return GetReplyArgumentsWithIter(&iter, args...); + } + + DBusMessage *SendSystem(); + DBusMessage *SendSession(); + DBusMessage *SendSystem(CDBusError& error); + DBusMessage *SendSession(CDBusError& error); + + bool SendAsyncSystem(); + bool SendAsyncSession(); + + DBusMessage *Send(DBusBusType type); + DBusMessage *Send(DBusBusType type, CDBusError& error); + DBusMessage *Send(DBusConnection *con, CDBusError& error); +private: + void AppendWithType(int type, const void* value); + bool SendAsync(DBusBusType type); + + void PrepareArgument(); + + bool InitializeReplyIter(DBusMessageIter* iter); + bool CheckTypeAndGetValue(DBusMessageIter* iter, int expectType, void* dest); + template<typename TFirst> + bool GetReplyArgumentsWithIter(DBusMessageIter* iter, TFirst* first) + { + // Recursion end + return CheckTypeAndGetValue(iter, ToDBusTypeFromPointer<TFirst>::TYPE, first); + } + template<typename TFirst, typename... TArgs> + bool GetReplyArgumentsWithIter(DBusMessageIter* iter, TFirst* first, TArgs*... args) + { + if (!CheckTypeAndGetValue(iter, ToDBusTypeFromPointer<TFirst>::TYPE, first)) + { + return false; + } + // Ignore return value, if we try to read past the end of the message this + // will be catched by the type check (past-end type is DBUS_TYPE_INVALID) + dbus_message_iter_next(iter); + return GetReplyArgumentsWithIter(iter, args...); + } + + DBusMessagePtr m_message; + DBusMessagePtr m_reply = nullptr; + DBusMessageIter m_args; + bool m_haveArgs; +}; + +template<> +void CDBusMessage::AppendArgument<bool>(const bool arg); +template<> +void CDBusMessage::AppendArgument<std::string>(const std::string arg); diff --git a/xbmc/platform/linux/DBusUtil.cpp b/xbmc/platform/linux/DBusUtil.cpp new file mode 100644 index 0000000..2efb75c --- /dev/null +++ b/xbmc/platform/linux/DBusUtil.cpp @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "DBusUtil.h" + +#include "utils/log.h" + +CVariant CDBusUtil::GetVariant(const char *destination, const char *object, const char *interface, const char *property) +{ +//dbus-send --system --print-reply --dest=destination object org.freedesktop.DBus.Properties.Get string:interface string:property + CDBusMessage message(destination, object, "org.freedesktop.DBus.Properties", "Get"); + CVariant result; + + message.AppendArgument(interface); + message.AppendArgument(property); + DBusMessage *reply = message.SendSystem(); + + if (reply) + { + DBusMessageIter iter; + + if (dbus_message_iter_init(reply, &iter)) + { + if (!dbus_message_has_signature(reply, "v")) + CLog::Log(LOGERROR, "DBus: wrong signature on Get - should be \"v\" but was {}", + dbus_message_iter_get_signature(&iter)); + else + result = ParseVariant(&iter); + } + } + + return result; +} + +CVariant CDBusUtil::GetAll(const char *destination, const char *object, const char *interface) +{ + CDBusMessage message(destination, object, "org.freedesktop.DBus.Properties", "GetAll"); + CVariant properties; + message.AppendArgument(interface); + DBusMessage *reply = message.SendSystem(); + if (reply) + { + DBusMessageIter iter; + if (dbus_message_iter_init(reply, &iter)) + { + if (!dbus_message_has_signature(reply, "a{sv}")) + CLog::Log(LOGERROR, "DBus: wrong signature on GetAll - should be \"a{sv}\" but was {}", + dbus_message_iter_get_signature(&iter)); + else + { + do + { + DBusMessageIter sub; + dbus_message_iter_recurse(&iter, &sub); + do + { + DBusMessageIter dict; + dbus_message_iter_recurse(&sub, &dict); + do + { + const char * key = NULL; + + dbus_message_iter_get_basic(&dict, &key); + if (!dbus_message_iter_next(&dict)) + break; + + CVariant value = ParseVariant(&dict); + + if (!value.isNull()) + properties[key] = value; + + } while (dbus_message_iter_next(&dict)); + + } while (dbus_message_iter_next(&sub)); + + } while (dbus_message_iter_next(&iter)); + } + } + } + + return properties; +} + +CVariant CDBusUtil::ParseVariant(DBusMessageIter *itr) +{ + DBusMessageIter variant; + dbus_message_iter_recurse(itr, &variant); + + return ParseType(&variant); +} + +CVariant CDBusUtil::ParseType(DBusMessageIter *itr) +{ + CVariant value; + const char * string = NULL; + dbus_int32_t int32 = 0; + dbus_uint32_t uint32 = 0; + dbus_int64_t int64 = 0; + dbus_uint64_t uint64 = 0; + dbus_bool_t boolean = false; + double doublev = 0; + + int type = dbus_message_iter_get_arg_type(itr); + switch (type) + { + case DBUS_TYPE_OBJECT_PATH: + case DBUS_TYPE_STRING: + dbus_message_iter_get_basic(itr, &string); + value = string; + break; + case DBUS_TYPE_UINT32: + dbus_message_iter_get_basic(itr, &uint32); + value = (uint64_t)uint32; + break; + case DBUS_TYPE_BYTE: + case DBUS_TYPE_INT32: + dbus_message_iter_get_basic(itr, &int32); + value = (int64_t)int32; + break; + case DBUS_TYPE_UINT64: + dbus_message_iter_get_basic(itr, &uint64); + value = (uint64_t)uint64; + break; + case DBUS_TYPE_INT64: + dbus_message_iter_get_basic(itr, &int64); + value = (int64_t)int64; + break; + case DBUS_TYPE_BOOLEAN: + dbus_message_iter_get_basic(itr, &boolean); + value = (bool)boolean; + break; + case DBUS_TYPE_DOUBLE: + dbus_message_iter_get_basic(itr, &doublev); + value = doublev; + break; + case DBUS_TYPE_ARRAY: + DBusMessageIter array; + dbus_message_iter_recurse(itr, &array); + + value = CVariant::VariantTypeArray; + + do + { + CVariant item = ParseType(&array); + if (!item.isNull()) + value.push_back(item); + } while (dbus_message_iter_next(&array)); + break; + } + + return value; +} + +bool CDBusUtil::TryMethodCall(DBusBusType bus, const char* destination, const char* object, const char* interface, const char* method) +{ + CDBusMessage message(destination, object, interface, method); + CDBusError error; + message.Send(bus, error); + if (error) + { + error.Log(LOGDEBUG, std::string("DBus method call to ") + interface + "." + method + " at " + object + " of " + destination + " failed"); + } + return !error; +} + +bool CDBusUtil::TryMethodCall(DBusBusType bus, std::string const& destination, std::string const& object, std::string const& interface, std::string const& method) +{ + return TryMethodCall(bus, destination.c_str(), object.c_str(), interface.c_str(), method.c_str()); +} + +CDBusConnection::CDBusConnection() = default; + +bool CDBusConnection::Connect(DBusBusType bus, bool openPrivate) +{ + CDBusError error; + Connect(bus, error, openPrivate); + if (error) + { + error.Log(LOGWARNING, "DBus connection failed"); + return false; + } + + return true; +} + +bool CDBusConnection::Connect(DBusBusType bus, CDBusError& error, bool openPrivate) +{ + if (m_connection) + { + throw std::logic_error("Cannot reopen connected DBus connection"); + } + + m_connection.get_deleter().closeBeforeUnref = openPrivate; + + if (openPrivate) + { + m_connection.reset(dbus_bus_get_private(bus, error)); + } + else + { + m_connection.reset(dbus_bus_get(bus, error)); + } + + return !!m_connection; +} + +CDBusConnection::operator DBusConnection*() +{ + return m_connection.get(); +} + +void CDBusConnection::DBusConnectionDeleter::operator()(DBusConnection* connection) const +{ + if (closeBeforeUnref) + { + dbus_connection_close(connection); + } + dbus_connection_unref(connection); +} + +void CDBusConnection::Destroy() +{ + m_connection.reset(); +} + + +CDBusError::CDBusError() +{ + dbus_error_init(&m_error); +} + +CDBusError::~CDBusError() +{ + Reset(); +} + +void CDBusError::Reset() +{ + dbus_error_free(&m_error); +} + +CDBusError::operator DBusError*() +{ + return &m_error; +} + +bool CDBusError::IsSet() const +{ + return dbus_error_is_set(&m_error); +} + +CDBusError::operator bool() +{ + return IsSet(); +} + +CDBusError::operator bool() const +{ + return IsSet(); +} + +std::string CDBusError::Name() const +{ + if (!IsSet()) + { + throw std::logic_error("Cannot retrieve name of unset DBus error"); + } + return m_error.name; +} + +std::string CDBusError::Message() const +{ + if (!IsSet()) + { + throw std::logic_error("Cannot retrieve message of unset DBus error"); + } + return m_error.message; +} + +void CDBusError::Log(std::string const& message) const +{ + Log(LOGERROR, message); +} + +void CDBusError::Log(int level, const std::string& message) const +{ + if (!IsSet()) + { + throw std::logic_error("Cannot log unset DBus error"); + } + CLog::Log(level, "{}: {} - {}", message, m_error.name, m_error.message); +} diff --git a/xbmc/platform/linux/DBusUtil.h b/xbmc/platform/linux/DBusUtil.h new file mode 100644 index 0000000..57349ac --- /dev/null +++ b/xbmc/platform/linux/DBusUtil.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DBusMessage.h" +#include "utils/Variant.h" + +#include <memory> +#include <string> + +#include <dbus/dbus.h> + +class CDBusUtil +{ +public: + static CVariant GetAll(const char *destination, const char *object, const char *interface); + static CVariant GetVariant(const char *destination, const char *object, const char *interface, const char *property); + /** + * Try to call a DBus method and return whether the call succeeded + */ + static bool TryMethodCall(DBusBusType bus, const char* destination, const char* object, const char* interface, const char* method); + /** + * Try to call a DBus method and return whether the call succeeded + */ + static bool TryMethodCall(DBusBusType bus, std::string const& destination, std::string const& object, std::string const& interface, std::string const& method); + +private: + static CVariant ParseType(DBusMessageIter *itr); + static CVariant ParseVariant(DBusMessageIter *itr); +}; + +class CDBusConnection +{ +public: + CDBusConnection(); + bool Connect(DBusBusType bus, bool openPrivate = false); + bool Connect(DBusBusType bus, CDBusError& error, bool openPrivate = false); + void Destroy(); + operator DBusConnection*(); + +private: + CDBusConnection(CDBusConnection const& other) = delete; + CDBusConnection& operator=(CDBusConnection const& other) = delete; + + struct DBusConnectionDeleter + { + DBusConnectionDeleter() {} + bool closeBeforeUnref = false; + void operator()(DBusConnection* connection) const; + }; + std::unique_ptr<DBusConnection, DBusConnectionDeleter> m_connection; +}; + +class CDBusError +{ +public: + CDBusError(); + ~CDBusError(); + operator DBusError*(); + bool IsSet() const; + /** + * Reset this error wrapper + * + * If there was an error, it was handled and this error wrapper should be used + * again in a new call, it must be reset before that call. + */ + void Reset(); + // Keep because operator DBusError* would be used for if-statements on + // non-const CDBusError instead + operator bool(); + operator bool() const; + std::string Name() const; + std::string Message() const; + void Log(std::string const& message = "DBus error") const; + void Log(int level, std::string const& message = "DBus error") const; + +private: + CDBusError(CDBusError const& other) = delete; + CDBusError& operator=(CDBusError const& other) = delete; + + DBusError m_error; +}; diff --git a/xbmc/platform/linux/FDEventMonitor.cpp b/xbmc/platform/linux/FDEventMonitor.cpp new file mode 100644 index 0000000..9cd27c1 --- /dev/null +++ b/xbmc/platform/linux/FDEventMonitor.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FDEventMonitor.h" + +#include "threads/SingleLock.h" +#include "utils/log.h" + +#include <errno.h> +#include <mutex> + +#include <poll.h> +#include <sys/eventfd.h> +#include <unistd.h> + +#include "PlatformDefs.h" + +CFDEventMonitor::CFDEventMonitor() : + CThread("FDEventMonitor") +{ +} + +CFDEventMonitor::~CFDEventMonitor() +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + InterruptPoll(); + + if (m_wakeupfd >= 0) + { + /* sets m_bStop */ + StopThread(false); + + /* wake up the poll() call */ + eventfd_write(m_wakeupfd, 1); + + /* Wait for the thread to stop */ + { + CSingleExit exit(m_mutex); + StopThread(true); + } + + close(m_wakeupfd); + } +} + +void CFDEventMonitor::AddFD(const MonitoredFD& monitoredFD, int& id) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + InterruptPoll(); + + AddFDLocked(monitoredFD, id); + + StartMonitoring(); +} + +void CFDEventMonitor::AddFDs(const std::vector<MonitoredFD>& monitoredFDs, + std::vector<int>& ids) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + InterruptPoll(); + + for (unsigned int i = 0; i < monitoredFDs.size(); ++i) + { + int id; + AddFDLocked(monitoredFDs[i], id); + ids.push_back(id); + } + + StartMonitoring(); +} + +void CFDEventMonitor::RemoveFD(int id) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + InterruptPoll(); + + if (m_monitoredFDs.erase(id) != 1) + { + CLog::Log(LOGERROR, "CFDEventMonitor::RemoveFD - Tried to remove non-existing monitoredFD {}", + id); + } + + UpdatePollDescs(); +} + +void CFDEventMonitor::RemoveFDs(const std::vector<int>& ids) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + InterruptPoll(); + + for (unsigned int i = 0; i < ids.size(); ++i) + { + if (m_monitoredFDs.erase(ids[i]) != 1) + { + CLog::Log(LOGERROR, + "CFDEventMonitor::RemoveFDs - Tried to remove non-existing monitoredFD {} while " + "removing {} FDs", + ids[i], (unsigned)ids.size()); + } + } + + UpdatePollDescs(); +} + +void CFDEventMonitor::Process() +{ + eventfd_t dummy; + + while (!m_bStop) + { + std::unique_lock<CCriticalSection> lock(m_mutex); + std::unique_lock<CCriticalSection> pollLock(m_pollMutex); + + /* + * Leave the main mutex here to allow another thread to + * lock it while we are in poll(). + * By then calling InterruptPoll() the other thread can + * wake up poll and wait for the processing to pause at + * the above lock(m_mutex). + */ + lock.unlock(); + + int err = poll(&m_pollDescs[0], m_pollDescs.size(), -1); + + if (err < 0 && errno != EINTR) + { + CLog::Log(LOGERROR, "CFDEventMonitor::Process - poll() failed, error {}, stopping monitoring", + errno); + StopThread(false); + } + + // Something woke us up - either there is data available or we are being + // paused/stopped via m_wakeupfd. + + for (unsigned int i = 0; i < m_pollDescs.size(); ++i) + { + struct pollfd& pollDesc = m_pollDescs[i]; + int id = m_monitoredFDbyPollDescs[i]; + const MonitoredFD& monitoredFD = m_monitoredFDs[id]; + + if (pollDesc.revents) + { + if (monitoredFD.callback) + { + monitoredFD.callback(id, pollDesc.fd, pollDesc.revents, + monitoredFD.callbackData); + } + + if (pollDesc.revents & (POLLERR | POLLHUP | POLLNVAL)) + { + CLog::Log(LOGERROR, + "CFDEventMonitor::Process - polled fd {} got revents 0x{:x}, removing it", + pollDesc.fd, pollDesc.revents); + + /* Probably would be nice to inform our caller that their FD was + * dropped, but oh well... */ + m_monitoredFDs.erase(id); + UpdatePollDescs(); + } + + pollDesc.revents = 0; + } + } + + /* flush wakeup fd */ + eventfd_read(m_wakeupfd, &dummy); + + } +} + +void CFDEventMonitor::AddFDLocked(const MonitoredFD& monitoredFD, int& id) +{ + id = m_nextID; + + while (m_monitoredFDs.count(id)) + { + ++id; + } + m_nextID = id + 1; + + m_monitoredFDs[id] = monitoredFD; + + AddPollDesc(id, monitoredFD.fd, monitoredFD.events); +} + +void CFDEventMonitor::AddPollDesc(int id, int fd, short events) +{ + struct pollfd newPollFD; + newPollFD.fd = fd; + newPollFD.events = events; + newPollFD.revents = 0; + + m_pollDescs.push_back(newPollFD); + m_monitoredFDbyPollDescs.push_back(id); +} + +void CFDEventMonitor::UpdatePollDescs() +{ + m_monitoredFDbyPollDescs.clear(); + m_pollDescs.clear(); + + for (const auto& it : m_monitoredFDs) + { + AddPollDesc(it.first, it.second.fd, it.second.events); + } +} + +void CFDEventMonitor::StartMonitoring() +{ + if (!IsRunning()) + { + /* Start the monitoring thread */ + + m_wakeupfd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (m_wakeupfd < 0) + { + CLog::Log(LOGERROR, "CFDEventMonitor::StartMonitoring - Failed to create eventfd, error {}", + errno); + return; + } + + /* Add wakeup fd to the fd list */ + int id; + AddFDLocked(MonitoredFD(m_wakeupfd, POLLIN, NULL, NULL), id); + + Create(false); + } +} + +void CFDEventMonitor::InterruptPoll() +{ + if (m_wakeupfd >= 0) + { + eventfd_write(m_wakeupfd, 1); + /* wait for the poll() result handling (if any) to end */ + std::unique_lock<CCriticalSection> pollLock(m_pollMutex); + } +} diff --git a/xbmc/platform/linux/FDEventMonitor.h b/xbmc/platform/linux/FDEventMonitor.h new file mode 100644 index 0000000..c0b8395 --- /dev/null +++ b/xbmc/platform/linux/FDEventMonitor.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "platform/Platform.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <map> +#include <vector> + +#include <sys/epoll.h> + +/** + * Monitor a file descriptor with callback on poll() events. + */ +class CFDEventMonitor : public IPlatformService, private CThread +{ +public: + + typedef void (*EventCallback)(int id, int fd, short revents, void *data); + + struct MonitoredFD + { + int fd = -1; /**< File descriptor to be monitored */ + short events = 0; /**< Events to be monitored (see poll(2)) */ + + EventCallback callback = nullptr; /** Callback to be called on events */ + void *callbackData = nullptr; /** data parameter for EventCallback */ + + MonitoredFD(int fd_, short events_, EventCallback callback_, void *callbackData_) : + fd(fd_), events(events_), callback(callback_), callbackData(callbackData_) {} + MonitoredFD() = default; + }; + + CFDEventMonitor(); + ~CFDEventMonitor() override; + + void AddFD(const MonitoredFD& monitoredFD, int& id); + void AddFDs(const std::vector<MonitoredFD>& monitoredFDs, std::vector<int>& ids); + + void RemoveFD(int id); + void RemoveFDs(const std::vector<int>& ids); + +protected: + void Process() override; + +private: + void AddFDLocked(const MonitoredFD& monitoredFD, int& id); + + void AddPollDesc(int id, int fd, short events); + void UpdatePollDescs(); + + void StartMonitoring(); + void InterruptPoll(); + + std::map<int, MonitoredFD> m_monitoredFDs; + + /* these are kept synchronized */ + std::vector<int> m_monitoredFDbyPollDescs; + std::vector<struct pollfd> m_pollDescs; + + int m_nextID = 0; + int m_wakeupfd = -1; + + CCriticalSection m_mutex; + CCriticalSection m_pollMutex; +}; diff --git a/xbmc/platform/linux/MemUtils.cpp b/xbmc/platform/linux/MemUtils.cpp new file mode 100644 index 0000000..09dd22f --- /dev/null +++ b/xbmc/platform/linux/MemUtils.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "utils/MemUtils.h" + +#include <cstdlib> +#include <fstream> + +namespace KODI +{ +namespace MEMORY +{ + +void* AlignedMalloc(size_t s, size_t alignTo) +{ + void* p = nullptr; + int res = posix_memalign(&p, alignTo, s); + if (res == EINVAL) + { + throw std::runtime_error("Failed to align memory, alignment is not a multiple of 2"); + } + else if (res == ENOMEM) + { + throw std::runtime_error("Failed to align memory, insufficient memory available"); + } + return p; +} + +void AlignedFree(void* p) +{ + if (!p) + return; + + free(p); +} + +void GetMemoryStatus(MemoryStatus* buffer) +{ + if (!buffer) + return; + + std::ifstream file("/proc/meminfo"); + + if (!file.is_open()) + return; + + uint64_t buffers; + uint64_t cached; + uint64_t free; + uint64_t total; + uint64_t reclaimable; + + std::string token; + + while (file >> token) + { + if (token == "Buffers:") + file >> buffers; + if (token == "Cached:") + file >> cached; + if (token == "MemFree:") + file >> free; + if (token == "MemTotal:") + file >> total; + if (token == "SReclaimable:") + file >> reclaimable; + } + + buffer->totalPhys = total * 1024; + buffer->availPhys = (free + cached + reclaimable + buffers) * 1024; +} + +} +} diff --git a/xbmc/platform/linux/OptionalsReg.cpp b/xbmc/platform/linux/OptionalsReg.cpp new file mode 100644 index 0000000..82fb176 --- /dev/null +++ b/xbmc/platform/linux/OptionalsReg.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "OptionalsReg.h" + + +//----------------------------------------------------------------------------- +// ALSA +//----------------------------------------------------------------------------- + +#ifdef HAS_ALSA +#include "cores/AudioEngine/Sinks/AESinkALSA.h" +bool OPTIONALS::ALSARegister() +{ + CAESinkALSA::Register(); + return true; +} +#else +bool OPTIONALS::ALSARegister() +{ + return false; +} +#endif + +//----------------------------------------------------------------------------- +// PulseAudio +//----------------------------------------------------------------------------- + +#ifdef HAS_PULSEAUDIO +#include "cores/AudioEngine/Sinks/AESinkPULSE.h" +bool OPTIONALS::PulseAudioRegister() +{ + bool ret = CAESinkPULSE::Register(); + return ret; +} +#else +bool OPTIONALS::PulseAudioRegister() +{ + return false; +} +#endif + +//----------------------------------------------------------------------------- +// Pipewire +//----------------------------------------------------------------------------- + +#ifdef HAS_PIPEWIRE +#include "cores/AudioEngine/Sinks/pipewire/AESinkPipewire.h" +bool OPTIONALS::PipewireRegister() +{ + bool ret = AE::SINK::CAESinkPipewire::Register(); + return ret; +} +#else +bool OPTIONALS::PipewireRegister() +{ + return false; +} +#endif + +//----------------------------------------------------------------------------- +// sndio +//----------------------------------------------------------------------------- + +#ifdef HAS_SNDIO +#include "cores/AudioEngine/Sinks/AESinkSNDIO.h" +bool OPTIONALS::SndioRegister() +{ + CAESinkSNDIO::Register(); + return true; +} +#else +bool OPTIONALS::SndioRegister() +{ + return false; +} +#endif + +//----------------------------------------------------------------------------- +// Lirc +//----------------------------------------------------------------------------- + +#ifdef HAS_LIRC +#include "platform/linux/input/LIRC.h" +#include "ServiceBroker.h" +class OPTIONALS::CLircContainer +{ +public: + CLircContainer() + { + m_lirc.Start(); + } +protected: + CLirc m_lirc; +}; +#else +class OPTIONALS::CLircContainer +{ +}; +#endif + +OPTIONALS::CLircContainer* OPTIONALS::LircRegister() +{ + return new CLircContainer(); +} +void OPTIONALS::delete_CLircContainer::operator()(CLircContainer *p) const +{ + delete p; +} diff --git a/xbmc/platform/linux/OptionalsReg.h b/xbmc/platform/linux/OptionalsReg.h new file mode 100644 index 0000000..75fdb6a --- /dev/null +++ b/xbmc/platform/linux/OptionalsReg.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +//----------------------------------------------------------------------------- +// ALSA +//----------------------------------------------------------------------------- + +namespace OPTIONALS +{ +bool ALSARegister(); +} + +//----------------------------------------------------------------------------- +// PulseAudio +//----------------------------------------------------------------------------- + +namespace OPTIONALS +{ +bool PulseAudioRegister(); +} + +//----------------------------------------------------------------------------- +// Pipewire +//----------------------------------------------------------------------------- + +namespace OPTIONALS +{ +bool PipewireRegister(); +} + +//----------------------------------------------------------------------------- +// sndio +//----------------------------------------------------------------------------- + +namespace OPTIONALS +{ +bool SndioRegister(); +} + +//----------------------------------------------------------------------------- +// Lirc +//----------------------------------------------------------------------------- + +namespace OPTIONALS +{ +class CLircContainer; +CLircContainer* LircRegister(); +struct delete_CLircContainer +{ + void operator()(CLircContainer *p) const; +}; +} diff --git a/xbmc/platform/linux/PlatformLinux.cpp b/xbmc/platform/linux/PlatformLinux.cpp new file mode 100644 index 0000000..a7af098 --- /dev/null +++ b/xbmc/platform/linux/PlatformLinux.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlatformLinux.h" + +#if defined(HAS_ALSA) +#include "cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h" +#include "cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h" +#endif + +#include "utils/StringUtils.h" + +#if defined(HAS_ALSA) +#include "platform/linux/FDEventMonitor.h" +#endif + +#include "platform/linux/powermanagement/LinuxPowerSyscall.h" + +// clang-format off +#if defined(HAS_GLES) +#if defined(HAVE_WAYLAND) +#include "windowing/wayland/WinSystemWaylandEGLContextGLES.h" +#endif +#if defined(HAVE_X11) +#include "windowing/X11/WinSystemX11GLESContext.h" +#endif +#if defined(HAVE_GBM) +#include "windowing/gbm/WinSystemGbmGLESContext.h" +#endif +#endif + +#if defined(HAS_GL) +#if defined(HAVE_WAYLAND) +#include "windowing/wayland/WinSystemWaylandEGLContextGL.h" +#endif +#if defined(HAVE_X11) +#include "windowing/X11/WinSystemX11GLContext.h" +#endif +#if defined(HAVE_GBM) +#include "windowing/gbm/WinSystemGbmGLContext.h" +#endif +#endif +// clang-format on + +#include <cstdlib> + +CPlatform* CPlatform::CreateInstance() +{ + return new CPlatformLinux(); +} + +bool CPlatformLinux::InitStageOne() +{ + if (!CPlatformPosix::InitStageOne()) + return false; + + setenv("OS", "Linux", true); // for python scripts that check the OS + +#if defined(HAS_GLES) +#if defined(HAVE_WAYLAND) + KODI::WINDOWING::WAYLAND::CWinSystemWaylandEGLContextGLES::Register(); +#endif +#if defined(HAVE_X11) + KODI::WINDOWING::X11::CWinSystemX11GLESContext::Register(); +#endif +#if defined(HAVE_GBM) + KODI::WINDOWING::GBM::CWinSystemGbmGLESContext::Register(); +#endif +#endif + +#if defined(HAS_GL) +#if defined(HAVE_WAYLAND) + KODI::WINDOWING::WAYLAND::CWinSystemWaylandEGLContextGL::Register(); +#endif +#if defined(HAVE_X11) + KODI::WINDOWING::X11::CWinSystemX11GLContext::Register(); +#endif +#if defined(HAVE_GBM) + KODI::WINDOWING::GBM::CWinSystemGbmGLContext::Register(); +#endif +#endif + + CLinuxPowerSyscall::Register(); + + std::string envSink; + if (getenv("KODI_AE_SINK")) + envSink = getenv("KODI_AE_SINK"); + + if (StringUtils::EqualsNoCase(envSink, "ALSA")) + { + OPTIONALS::ALSARegister(); + } + else if (StringUtils::EqualsNoCase(envSink, "PULSE")) + { + OPTIONALS::PulseAudioRegister(); + } + else if (StringUtils::EqualsNoCase(envSink, "PIPEWIRE")) + { + OPTIONALS::PipewireRegister(); + } + else if (StringUtils::EqualsNoCase(envSink, "SNDIO")) + { + OPTIONALS::SndioRegister(); + } + else if (StringUtils::EqualsNoCase(envSink, "ALSA+PULSE")) + { + OPTIONALS::ALSARegister(); + OPTIONALS::PulseAudioRegister(); + } + else + { + if (!OPTIONALS::PulseAudioRegister()) + { + if (!OPTIONALS::PipewireRegister()) + { + if (!OPTIONALS::ALSARegister()) + { + OPTIONALS::SndioRegister(); + } + } + } + } + + m_lirc.reset(OPTIONALS::LircRegister()); + +#if defined(HAS_ALSA) + RegisterComponent(std::make_shared<CFDEventMonitor>()); +#if defined(HAVE_LIBUDEV) + RegisterComponent(std::make_shared<CALSADeviceMonitor>()); +#endif +#if !defined(HAVE_X11) + RegisterComponent(std::make_shared<CALSAHControlMonitor>()); +#endif +#endif // HAS_ALSA + return true; +} + +void CPlatformLinux::DeinitStageOne() +{ +#if defined(HAS_ALSA) +#if !defined(HAVE_X11) + DeregisterComponent(typeid(CALSAHControlMonitor)); +#endif +#if defined(HAVE_LIBUDEV) + DeregisterComponent(typeid(CALSADeviceMonitor)); +#endif + DeregisterComponent(typeid(CFDEventMonitor)); +#endif // HAS_ALSA +} + +bool CPlatformLinux::IsConfigureAddonsAtStartupEnabled() +{ +#if defined(ADDONS_CONFIGURE_AT_STARTUP) + return true; +#else + return false; +#endif +} diff --git a/xbmc/platform/linux/PlatformLinux.h b/xbmc/platform/linux/PlatformLinux.h new file mode 100644 index 0000000..b74deb6 --- /dev/null +++ b/xbmc/platform/linux/PlatformLinux.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "platform/linux/OptionalsReg.h" +#include "platform/posix/PlatformPosix.h" + +#include <memory> + +class CPlatformLinux : public CPlatformPosix +{ +public: + CPlatformLinux() = default; + + ~CPlatformLinux() override = default; + + bool InitStageOne() override; + void DeinitStageOne() override; + + bool IsConfigureAddonsAtStartupEnabled() override; + +private: + std::unique_ptr<OPTIONALS::CLircContainer, OPTIONALS::delete_CLircContainer> m_lirc; +}; diff --git a/xbmc/platform/linux/SysfsPath.cpp b/xbmc/platform/linux/SysfsPath.cpp new file mode 100644 index 0000000..af4f9cb --- /dev/null +++ b/xbmc/platform/linux/SysfsPath.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SysfsPath.h" + +#include <exception> + +bool CSysfsPath::Exists() +{ + std::ifstream file(m_path); + + if (!file.is_open()) + return false; + + return true; +} + +template<> +std::optional<std::string> CSysfsPath::Get() +{ + try + { + std::ifstream file(m_path); + + std::string value; + + std::getline(file, value); + + if (file.bad()) + { + CLog::LogF(LOGERROR, "error reading from '{}'", m_path); + return std::nullopt; + } + + return value; + } + catch (const std::exception& e) + { + CLog::LogF(LOGERROR, "exception reading from '{}': {}", m_path, e.what()); + return std::nullopt; + } +} diff --git a/xbmc/platform/linux/SysfsPath.h b/xbmc/platform/linux/SysfsPath.h new file mode 100644 index 0000000..3c19703 --- /dev/null +++ b/xbmc/platform/linux/SysfsPath.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/log.h" + +#include <exception> +#include <fstream> +#include <optional> +#include <string> + +class CSysfsPath +{ +public: + CSysfsPath() = default; + CSysfsPath(const std::string& path) : m_path(path) {} + ~CSysfsPath() = default; + + bool Exists(); + + template<typename T> + std::optional<T> Get() + { + try + { + std::ifstream file(m_path); + + T value; + + file >> value; + + if (file.bad()) + { + CLog::LogF(LOGERROR, "error reading from '{}'", m_path); + return std::nullopt; + } + + return value; + } + catch (const std::exception& e) + { + CLog::LogF(LOGERROR, "exception reading from '{}': {}", m_path, e.what()); + return std::nullopt; + } + } + +private: + std::string m_path; +}; + +template<> +std::optional<std::string> CSysfsPath::Get<std::string>(); diff --git a/xbmc/platform/linux/TimeUtils.cpp b/xbmc/platform/linux/TimeUtils.cpp new file mode 100644 index 0000000..55cce92 --- /dev/null +++ b/xbmc/platform/linux/TimeUtils.cpp @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "TimeUtils.h" + +std::int64_t KODI::LINUX::TimespecDifference(timespec const& start, timespec const& end) +{ + return (end.tv_sec - start.tv_sec) * UINT64_C(1000000000) + (end.tv_nsec - start.tv_nsec); +}
\ No newline at end of file diff --git a/xbmc/platform/linux/TimeUtils.h b/xbmc/platform/linux/TimeUtils.h new file mode 100644 index 0000000..abbeb13 --- /dev/null +++ b/xbmc/platform/linux/TimeUtils.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <cstdint> +#include <time.h> + +namespace KODI +{ +namespace LINUX +{ + +/** + * Calculate difference between two timespecs in nanoseconds + * \param start earlier time + * \param end later time + * \return (end - start) in nanoseconds + */ +std::int64_t TimespecDifference(timespec const& start, timespec const& end); + +} +} diff --git a/xbmc/platform/linux/input/CMakeLists.txt b/xbmc/platform/linux/input/CMakeLists.txt new file mode 100644 index 0000000..ebb2ccd --- /dev/null +++ b/xbmc/platform/linux/input/CMakeLists.txt @@ -0,0 +1,27 @@ +set(SOURCES "") +set(HEADERS "") + +if(LIRCCLIENT_FOUND) + list(APPEND SOURCES LIRC.cpp) + list(APPEND HEADERS LIRC.h) +endif() + +if("gbm" IN_LIST CORE_PLATFORM_NAME_LC) + if(LIBINPUT_FOUND) + list(APPEND SOURCES LibInputHandler.cpp + LibInputKeyboard.cpp + LibInputPointer.cpp + LibInputSettings.cpp + LibInputTouch.cpp) + + list(APPEND HEADERS LibInputHandler.h + LibInputKeyboard.h + LibInputPointer.h + LibInputSettings.h + LibInputTouch.h) + endif() +endif() + +if(SOURCES) + core_add_library(input_linux) +endif() diff --git a/xbmc/platform/linux/input/LIRC.cpp b/xbmc/platform/linux/input/LIRC.cpp new file mode 100644 index 0000000..6900f5c --- /dev/null +++ b/xbmc/platform/linux/input/LIRC.cpp @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2007-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LIRC.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SingleLock.h" +#include "utils/log.h" + +#include <mutex> + +#include <fcntl.h> +#include <lirc/lirc_client.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <unistd.h> + +#include "PlatformDefs.h" + +using namespace std::chrono_literals; + +CLirc::CLirc() : CThread("Lirc") +{ +} + +CLirc::~CLirc() +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_fd > 0) + shutdown(m_fd, SHUT_RDWR); + } + StopThread(); +} + +void CLirc::Start() +{ + Create(); + SetPriority(ThreadPriority::LOWEST); +} + +void CLirc::Process() +{ + auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + throw std::runtime_error("CSettingsComponent needs to exist before starting CLirc"); + + auto settings = settingsComponent->GetSettings(); + if (!settings) + throw std::runtime_error("CSettings needs to exist before starting CLirc"); + + while (!settings->IsLoaded()) + CThread::Sleep(1000ms); + + m_profileId = settingsComponent->GetProfileManager()->GetCurrentProfileId(); + m_irTranslator.Load("Lircmap.xml"); + + // make sure work-around (CheckDaemon) uses the same socket path as lirc_init + const char* socket_path = getenv("LIRC_SOCKET_PATH"); + socket_path = socket_path ? socket_path : "/var/run/lirc/lircd"; + setenv("LIRC_SOCKET_PATH", socket_path, 0); + + while (!m_bStop) + { + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + // lirc_client is buggy because it does not close socket, if connect fails + // work around by checking if daemon is running before calling lirc_init + if (!CheckDaemon()) + { + CSingleExit lock(m_critSection); + CThread::Sleep(1000ms); + continue; + } + + m_fd = lirc_init(const_cast<char*>("kodi"), 0); + if (m_fd <= 0) + { + CSingleExit lock(m_critSection); + CThread::Sleep(1000ms); + continue; + } + } + + char *code; + while (!m_bStop) + { + int ret = lirc_nextcode(&code); + if (ret < 0) + { + lirc_deinit(); + CThread::Sleep(1000ms); + break; + } + if (code != nullptr) + { + int profileId = settingsComponent->GetProfileManager()->GetCurrentProfileId(); + if (m_profileId != profileId) + { + m_profileId = profileId; + m_irTranslator.Load("Lircmap.xml"); + } + ProcessCode(code); + free(code); + } + } + } + + lirc_deinit(); +} + +void CLirc::ProcessCode(char *buf) +{ + // Parse the result. Sample line: + // 000000037ff07bdd 00 OK mceusb + char scanCode[128]; + char buttonName[128]; + char repeatStr[4]; + char deviceName[128]; + sscanf(buf, "%s %s %s %s", &scanCode[0], &repeatStr[0], &buttonName[0], &deviceName[0]); + + // Some template LIRC configuration have button names in apostrophes or quotes. + // If we got a quoted button name, strip 'em + unsigned int buttonNameLen = strlen(buttonName); + if (buttonNameLen > 2 && + ((buttonName[0] == '\'' && buttonName[buttonNameLen-1] == '\'') || + ((buttonName[0] == '"' && buttonName[buttonNameLen-1] == '"')))) + { + memmove(buttonName, buttonName + 1, buttonNameLen - 2); + buttonName[buttonNameLen - 2] = '\0'; + } + + int button = m_irTranslator.TranslateButton(deviceName, buttonName); + + char *end = nullptr; + long repeat = strtol(repeatStr, &end, 16); + if (!end || *end != 0) + CLog::Log(LOGERROR, "LIRC: invalid non-numeric character in expression {}", repeatStr); + + if (repeat == 0) + { + CLog::Log(LOGDEBUG, "LIRC: - NEW {} {} {} {} ({})", &scanCode[0], &repeatStr[0], &buttonName[0], + &deviceName[0], buttonName); + m_firstClickTime = std::chrono::steady_clock::now(); + + XBMC_Event newEvent = {}; + newEvent.type = XBMC_BUTTON; + newEvent.keybutton.button = button; + newEvent.keybutton.holdtime = 0; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + return; + } + else if (repeat > CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_remoteDelay) + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_BUTTON; + newEvent.keybutton.button = button; + + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_firstClickTime); + + newEvent.keybutton.holdtime = duration.count(); + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } +} + +bool CLirc::CheckDaemon() +{ + const char* socket_path = getenv("LIRC_SOCKET_PATH"); + + struct sockaddr_un addr_un; + if (strlen(socket_path) + 1 > sizeof(addr_un.sun_path)) + { + return false; + } + + addr_un.sun_family = AF_UNIX; + strcpy(addr_un.sun_path, socket_path); + + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) + { + return false; + } + + if (connect(fd, reinterpret_cast<struct sockaddr*>(&addr_un), sizeof(addr_un)) == -1) + { + close(fd); + return false; + } + + close(fd); + return true; +} diff --git a/xbmc/platform/linux/input/LIRC.h b/xbmc/platform/linux/input/LIRC.h new file mode 100644 index 0000000..f08638d --- /dev/null +++ b/xbmc/platform/linux/input/LIRC.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2007-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/IRTranslator.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <chrono> +#include <string> + +class CLirc : CThread +{ +public: + CLirc(); + ~CLirc() override; + void Start(); + +protected: + void Process() override; + void ProcessCode(char *buf); + bool CheckDaemon(); + + int m_fd = -1; + std::chrono::time_point<std::chrono::steady_clock> m_firstClickTime; + CCriticalSection m_critSection; + CIRTranslator m_irTranslator; + int m_profileId; +}; diff --git a/xbmc/platform/linux/input/LibInputHandler.cpp b/xbmc/platform/linux/input/LibInputHandler.cpp new file mode 100644 index 0000000..a8c39f5 --- /dev/null +++ b/xbmc/platform/linux/input/LibInputHandler.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LibInputHandler.h" + +#include "LibInputKeyboard.h" +#include "LibInputPointer.h" +#include "LibInputSettings.h" +#include "LibInputTouch.h" +#include "ServiceBroker.h" +#include "interfaces/AnnouncementManager.h" +#include "utils/log.h" + +#include <algorithm> +#include <string.h> + +#include <fcntl.h> +#include <linux/input.h> +#include <sys/epoll.h> +#include <sys/ioctl.h> +#include <unistd.h> + +static int open_restricted(const char *path, int flags, void __attribute__((unused)) *user_data) +{ + int fd = open(path, flags); + + if (fd < 0) + { + CLog::Log(LOGERROR, "{} - failed to open {} ({})", __FUNCTION__, path, strerror(errno)); + return -errno; + } + + auto ret = ioctl(fd, EVIOCGRAB, (void*)1); + if (ret < 0) + { + CLog::Log(LOGDEBUG, "{} - grab requested, but failed for {} ({})", __FUNCTION__, path, + strerror(errno)); + } + + return fd; +} + +static void close_restricted(int fd, void __attribute__((unused)) *user_data) +{ + close(fd); +} + +static const struct libinput_interface m_interface = +{ + open_restricted, + close_restricted, +}; + +static void LogHandler(libinput __attribute__((unused)) *libinput, libinput_log_priority priority, const char *format, va_list args) +{ + if (priority == LIBINPUT_LOG_PRIORITY_DEBUG) + { + char buf[512]; + int n = vsnprintf(buf, sizeof(buf), format, args); + if (n > 0) + CLog::Log(LOGDEBUG, "libinput: {}", buf); + } +} + +CLibInputHandler::CLibInputHandler() : CThread("libinput") +{ + m_udev = udev_new(); + if (!m_udev) + { + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to get udev context for libinput", + __FUNCTION__); + return; + } + + m_li = libinput_udev_create_context(&m_interface, nullptr, m_udev); + if (!m_li) + { + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to get libinput context", __FUNCTION__); + return; + } + + libinput_log_set_handler(m_li, LogHandler); + libinput_log_set_priority(m_li, LIBINPUT_LOG_PRIORITY_DEBUG); + + auto ret = libinput_udev_assign_seat(m_li, "seat0"); + if (ret < 0) + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to assign seat", __FUNCTION__); + + m_liFd = libinput_get_fd(m_li); + + m_keyboard.reset(new CLibInputKeyboard()); + m_pointer.reset(new CLibInputPointer()); + m_touch.reset(new CLibInputTouch()); + m_settings.reset(new CLibInputSettings(this)); + + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); +} + +CLibInputHandler::~CLibInputHandler() +{ + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + StopThread(); + + libinput_unref(m_li); + udev_unref(m_udev); +} + +void CLibInputHandler::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if (flag & (ANNOUNCEMENT::System)) + { + if (message == "OnSleep") + libinput_suspend(m_li); + else if (message == "OnWake") + { + auto ret = libinput_resume(m_li); + if (ret < 0) + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to resume monitoring", __FUNCTION__); + } + } +} + +bool CLibInputHandler::SetKeymap(const std::string& layout) +{ + return m_keyboard->SetKeymap(layout); +} + +void CLibInputHandler::Start() +{ + Create(); + SetPriority(ThreadPriority::LOWEST); +} + +void CLibInputHandler::Process() +{ + int epollFd = epoll_create1(EPOLL_CLOEXEC); + if (epollFd < 0) + { + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to create epoll file descriptor: {}", + __FUNCTION__, strerror(-errno)); + return; + } + + epoll_event event; + event.events = EPOLLIN; + event.data.fd = m_liFd; + + auto ret = epoll_ctl(epollFd, EPOLL_CTL_ADD, m_liFd, &event); + if (ret < 0) + { + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to add file descriptor to epoll: {}", + __FUNCTION__, strerror(-errno)); + close(epollFd); + return; + } + + while (!m_bStop) + { + epoll_wait(epollFd, &event, 1, 200); + + ret = libinput_dispatch(m_li); + if (ret < 0) + { + CLog::Log(LOGERROR, "CLibInputHandler::{} - libinput_dispatch failed: {}", __FUNCTION__, + strerror(-errno)); + close(epollFd); + return; + } + + libinput_event *ev; + while ((ev = libinput_get_event(m_li)) != nullptr) + { + ProcessEvent(ev); + libinput_event_destroy(ev); + } + } + + ret = close(epollFd); + if (ret < 0) + { + CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to close epoll file descriptor: {}", + __FUNCTION__, strerror(-errno)); + return; + } +} + +void CLibInputHandler::ProcessEvent(libinput_event *ev) +{ + libinput_event_type type = libinput_event_get_type(ev); + libinput_device *dev = libinput_event_get_device(ev); + + switch (type) + { + case LIBINPUT_EVENT_DEVICE_ADDED: + DeviceAdded(dev); + break; + case LIBINPUT_EVENT_DEVICE_REMOVED: + DeviceRemoved(dev); + break; + case LIBINPUT_EVENT_POINTER_BUTTON: + m_pointer->ProcessButton(libinput_event_get_pointer_event(ev)); + break; + case LIBINPUT_EVENT_POINTER_MOTION: + m_pointer->ProcessMotion(libinput_event_get_pointer_event(ev)); + break; + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: + m_pointer->ProcessMotionAbsolute(libinput_event_get_pointer_event(ev)); + break; + case LIBINPUT_EVENT_POINTER_AXIS: + m_pointer->ProcessAxis(libinput_event_get_pointer_event(ev)); + break; + case LIBINPUT_EVENT_KEYBOARD_KEY: + m_keyboard->ProcessKey(libinput_event_get_keyboard_event(ev)); + m_keyboard->UpdateLeds(dev); + break; + case LIBINPUT_EVENT_TOUCH_DOWN: + m_touch->ProcessTouchDown(libinput_event_get_touch_event(ev)); + break; + case LIBINPUT_EVENT_TOUCH_MOTION: + m_touch->ProcessTouchMotion(libinput_event_get_touch_event(ev)); + break; + case LIBINPUT_EVENT_TOUCH_UP: + m_touch->ProcessTouchUp(libinput_event_get_touch_event(ev)); + break; + case LIBINPUT_EVENT_TOUCH_CANCEL: + m_touch->ProcessTouchCancel(libinput_event_get_touch_event(ev)); + break; + case LIBINPUT_EVENT_TOUCH_FRAME: + m_touch->ProcessTouchFrame(libinput_event_get_touch_event(ev)); + break; + + default: + break; + } +} + +void CLibInputHandler::DeviceAdded(libinput_device *dev) +{ + const char *sysname = libinput_device_get_sysname(dev); + const char *name = libinput_device_get_name(dev); + + if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_TOUCH)) + { + CLog::Log(LOGDEBUG, "CLibInputHandler::{} - touch type device added: {} ({})", __FUNCTION__, + name, sysname); + m_devices.push_back(libinput_device_ref(dev)); + } + + if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_POINTER)) + { + CLog::Log(LOGDEBUG, "CLibInputHandler::{} - pointer type device added: {} ({})", __FUNCTION__, + name, sysname); + m_devices.push_back(libinput_device_ref(dev)); + } + + if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_KEYBOARD)) + { + CLog::Log(LOGDEBUG, "CLibInputHandler::{} - keyboard type device added: {} ({})", __FUNCTION__, + name, sysname); + m_devices.push_back(libinput_device_ref(dev)); + m_keyboard->GetRepeat(dev); + } +} + +void CLibInputHandler::DeviceRemoved(libinput_device *dev) +{ + const char *sysname = libinput_device_get_sysname(dev); + const char *name = libinput_device_get_name(dev); + + if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_TOUCH)) + { + CLog::Log(LOGDEBUG, "CLibInputHandler::{} - touch type device removed: {} ({})", __FUNCTION__, + name, sysname); + auto device = std::find(m_devices.begin(), m_devices.end(), libinput_device_unref(dev)); + m_devices.erase(device); + } + + if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_POINTER)) + { + CLog::Log(LOGDEBUG, "CLibInputHandler::{} - pointer type device removed: {} ({})", __FUNCTION__, + name, sysname); + auto device = std::find(m_devices.begin(), m_devices.end(), libinput_device_unref(dev)); + m_devices.erase(device); + } + + if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_KEYBOARD)) + { + CLog::Log(LOGDEBUG, "CLibInputHandler::{} - keyboard type device removed: {} ({})", + __FUNCTION__, name, sysname); + auto device = std::find(m_devices.begin(), m_devices.end(), libinput_device_unref(dev)); + m_devices.erase(device); + } +} diff --git a/xbmc/platform/linux/input/LibInputHandler.h b/xbmc/platform/linux/input/LibInputHandler.h new file mode 100644 index 0000000..659d086 --- /dev/null +++ b/xbmc/platform/linux/input/LibInputHandler.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/IAnnouncer.h" +#include "threads/Thread.h" + +#include <memory> +#include <vector> + +#include <libinput.h> +#include <libudev.h> + +class CLibInputKeyboard; +class CLibInputPointer; +class CLibInputSettings; +class CLibInputTouch; + +class CLibInputHandler : CThread, public ANNOUNCEMENT::IAnnouncer +{ +public: + CLibInputHandler(); + ~CLibInputHandler() override; + + void Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) override; + + void Start(); + + bool SetKeymap(const std::string& layout); + +private: + void Process() override; + void ProcessEvent(libinput_event *ev); + void DeviceAdded(libinput_device *dev); + void DeviceRemoved(libinput_device *dev); + + udev *m_udev; + libinput *m_li; + int m_liFd; + + std::unique_ptr<CLibInputKeyboard> m_keyboard; + std::unique_ptr<CLibInputPointer> m_pointer; + std::unique_ptr<CLibInputSettings> m_settings; + std::unique_ptr<CLibInputTouch> m_touch; + std::vector<libinput_device*> m_devices; +}; + diff --git a/xbmc/platform/linux/input/LibInputKeyboard.cpp b/xbmc/platform/linux/input/LibInputKeyboard.cpp new file mode 100644 index 0000000..657588a --- /dev/null +++ b/xbmc/platform/linux/input/LibInputKeyboard.cpp @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LibInputKeyboard.h" + +#include "LibInputSettings.h" +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/log.h" + +#include <algorithm> +#include <map> +#include <string.h> + +#include <fcntl.h> +#include <linux/input.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <xkbcommon/xkbcommon-keysyms.h> +#include <xkbcommon/xkbcommon-names.h> + +namespace +{ +constexpr int REPEAT_DELAY = 400; +constexpr int REPEAT_RATE = 80; + +static const std::map<xkb_keysym_t, XBMCKey> xkbMap = +{ + // Function keys before start of ASCII printable character range + { XKB_KEY_BackSpace, XBMCK_BACKSPACE }, + { XKB_KEY_Tab, XBMCK_TAB }, + { XKB_KEY_Clear, XBMCK_CLEAR }, + { XKB_KEY_Return, XBMCK_RETURN }, + { XKB_KEY_Pause, XBMCK_PAUSE }, + { XKB_KEY_Escape, XBMCK_ESCAPE }, + + // ASCII printable range - not included here + + // Function keys after end of ASCII printable character range + { XKB_KEY_Delete, XBMCK_DELETE }, + + // Multimedia keys + { XKB_KEY_XF86Back, XBMCK_BROWSER_BACK }, + { XKB_KEY_XF86Forward, XBMCK_BROWSER_FORWARD }, + { XKB_KEY_XF86Refresh, XBMCK_BROWSER_REFRESH }, + { XKB_KEY_XF86Stop, XBMCK_BROWSER_STOP }, + { XKB_KEY_XF86Search, XBMCK_BROWSER_SEARCH }, + // XKB_KEY_XF86Favorites could be XBMCK_BROWSER_FAVORITES or XBMCK_FAVORITES, + // XBMCK_FAVORITES was chosen here because it is more general + { XKB_KEY_XF86HomePage, XBMCK_BROWSER_HOME }, + { XKB_KEY_XF86AudioMute, XBMCK_VOLUME_MUTE }, + { XKB_KEY_XF86AudioLowerVolume, XBMCK_VOLUME_DOWN }, + { XKB_KEY_XF86AudioRaiseVolume, XBMCK_VOLUME_UP }, + { XKB_KEY_XF86AudioNext, XBMCK_MEDIA_NEXT_TRACK }, + { XKB_KEY_XF86AudioPrev, XBMCK_MEDIA_PREV_TRACK }, + { XKB_KEY_XF86AudioStop, XBMCK_MEDIA_STOP }, + { XKB_KEY_XF86AudioPause, XBMCK_MEDIA_PLAY_PAUSE }, + { XKB_KEY_XF86Mail, XBMCK_LAUNCH_MAIL }, + { XKB_KEY_XF86Select, XBMCK_LAUNCH_MEDIA_SELECT }, + { XKB_KEY_XF86Launch0, XBMCK_LAUNCH_APP1 }, + { XKB_KEY_XF86Launch1, XBMCK_LAUNCH_APP2 }, + { XKB_KEY_XF86WWW, XBMCK_LAUNCH_FILE_BROWSER }, + { XKB_KEY_XF86AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER }, + { XKB_KEY_XF86AudioRewind, XBMCK_MEDIA_REWIND }, + { XKB_KEY_XF86AudioForward, XBMCK_MEDIA_FASTFORWARD }, + + // Numeric keypad + { XKB_KEY_KP_0, XBMCK_KP0 }, + { XKB_KEY_KP_1, XBMCK_KP1 }, + { XKB_KEY_KP_2, XBMCK_KP2 }, + { XKB_KEY_KP_3, XBMCK_KP3 }, + { XKB_KEY_KP_4, XBMCK_KP4 }, + { XKB_KEY_KP_5, XBMCK_KP5 }, + { XKB_KEY_KP_6, XBMCK_KP6 }, + { XKB_KEY_KP_7, XBMCK_KP7 }, + { XKB_KEY_KP_8, XBMCK_KP8 }, + { XKB_KEY_KP_9, XBMCK_KP9 }, + { XKB_KEY_KP_Decimal, XBMCK_KP_PERIOD }, + { XKB_KEY_KP_Divide, XBMCK_KP_DIVIDE }, + { XKB_KEY_KP_Multiply, XBMCK_KP_MULTIPLY }, + { XKB_KEY_KP_Subtract, XBMCK_KP_MINUS }, + { XKB_KEY_KP_Add, XBMCK_KP_PLUS }, + { XKB_KEY_KP_Enter, XBMCK_KP_ENTER }, + { XKB_KEY_KP_Equal, XBMCK_KP_EQUALS }, + + // Arrows + Home/End pad + { XKB_KEY_Up, XBMCK_UP }, + { XKB_KEY_Down, XBMCK_DOWN }, + { XKB_KEY_Right, XBMCK_RIGHT }, + { XKB_KEY_Left, XBMCK_LEFT }, + { XKB_KEY_Insert, XBMCK_INSERT }, + { XKB_KEY_Home, XBMCK_HOME }, + { XKB_KEY_End, XBMCK_END }, + { XKB_KEY_Page_Up, XBMCK_PAGEUP }, + { XKB_KEY_Page_Down, XBMCK_PAGEDOWN }, + + // Key state modifier keys + { XKB_KEY_Num_Lock, XBMCK_NUMLOCK }, + { XKB_KEY_Caps_Lock, XBMCK_CAPSLOCK }, + { XKB_KEY_Scroll_Lock, XBMCK_SCROLLOCK }, + { XKB_KEY_Shift_R, XBMCK_RSHIFT }, + { XKB_KEY_Shift_L, XBMCK_LSHIFT }, + { XKB_KEY_Control_R, XBMCK_RCTRL }, + { XKB_KEY_Control_L, XBMCK_LCTRL }, + { XKB_KEY_Alt_R, XBMCK_RALT }, + { XKB_KEY_Alt_L, XBMCK_LALT }, + { XKB_KEY_Meta_R, XBMCK_RMETA }, + { XKB_KEY_Meta_L, XBMCK_LMETA }, + { XKB_KEY_Super_R, XBMCK_RSUPER }, + { XKB_KEY_Super_L, XBMCK_LSUPER }, + // XKB does not have XBMCK_MODE/"Alt Gr" - probably equal to XKB_KEY_Alt_R + { XKB_KEY_Multi_key, XBMCK_COMPOSE }, + + // Miscellaneous function keys + { XKB_KEY_Help, XBMCK_HELP }, + { XKB_KEY_Print, XBMCK_PRINT }, + { XKB_KEY_Sys_Req, XBMCK_SYSREQ}, + { XKB_KEY_Break, XBMCK_BREAK }, + { XKB_KEY_Menu, XBMCK_MENU }, + { XKB_KEY_XF86PowerOff, XBMCK_POWER }, + { XKB_KEY_EcuSign, XBMCK_EURO }, + { XKB_KEY_Undo, XBMCK_UNDO }, + { XKB_KEY_XF86Sleep, XBMCK_SLEEP }, + // Unmapped: XBMCK_GUIDE, XBMCK_SETTINGS, XBMCK_INFO + { XKB_KEY_XF86Red, XBMCK_RED }, + { XKB_KEY_XF86Green, XBMCK_GREEN }, + { XKB_KEY_XF86Yellow, XBMCK_YELLOW }, + { XKB_KEY_XF86Blue, XBMCK_BLUE }, + // Unmapped: XBMCK_ZOOM, XBMCK_TEXT + { XKB_KEY_XF86Favorites, XBMCK_FAVORITES }, + { XKB_KEY_XF86HomePage, XBMCK_HOMEPAGE }, + // Unmapped: XBMCK_CONFIG, XBMCK_EPG + + // Media keys + { XKB_KEY_XF86Eject, XBMCK_EJECT }, + { XKB_KEY_Cancel, XBMCK_STOP }, + { XKB_KEY_XF86AudioRecord, XBMCK_RECORD }, + // XBMCK_REWIND clashes with XBMCK_MEDIA_REWIND + { XKB_KEY_XF86Phone, XBMCK_PHONE }, + { XKB_KEY_XF86AudioPlay, XBMCK_PLAY }, + { XKB_KEY_XF86AudioRandomPlay, XBMCK_SHUFFLE } + // XBMCK_FASTFORWARD clashes with XBMCK_MEDIA_FASTFORWARD +}; +} // namespace + +CLibInputKeyboard::CLibInputKeyboard() + : m_repeatTimer(std::bind(&CLibInputKeyboard::KeyRepeatTimeout, this)) +{ + m_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (!m_ctx) + { + CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to create xkb context", __FUNCTION__); + return; + } + + std::string layout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CLibInputSettings::SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT); + + if (!SetKeymap(layout)) + { + CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed set default keymap", __FUNCTION__); + return; + } +} + +CLibInputKeyboard::~CLibInputKeyboard() +{ + xkb_state_unref(m_state); + xkb_keymap_unref(m_keymap); + xkb_context_unref(m_ctx); +} + +bool CLibInputKeyboard::SetKeymap(const std::string& layout) +{ + xkb_state_unref(m_state); + xkb_keymap_unref(m_keymap); + + xkb_rule_names names; + + names.rules = nullptr; + names.model = nullptr; + names.layout = layout.c_str(); + names.variant = nullptr; + names.options = nullptr; + + m_keymap = xkb_keymap_new_from_names(m_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (!m_keymap) + { + CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to compile keymap", __FUNCTION__); + return false; + } + + m_state = xkb_state_new(m_keymap); + if (!m_state) + { + CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to create xkb state", __FUNCTION__); + return false; + } + + m_modindex[0] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_CTRL); + m_modindex[1] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_ALT); + m_modindex[2] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_SHIFT); + m_modindex[3] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_LOGO); + + m_ledindex[0] = xkb_keymap_led_get_index(m_keymap, XKB_LED_NAME_NUM); + m_ledindex[1] = xkb_keymap_led_get_index(m_keymap, XKB_LED_NAME_CAPS); + m_ledindex[2] = xkb_keymap_led_get_index(m_keymap, XKB_LED_NAME_SCROLL); + + m_leds = 0; + + return true; +} + +void CLibInputKeyboard::ProcessKey(libinput_event_keyboard *e) +{ + if (!m_ctx || !m_keymap || !m_state) + return; + + XBMC_Event event = {}; + + const uint32_t xkbkey = libinput_event_keyboard_get_key(e) + 8; + const bool pressed = libinput_event_keyboard_get_key_state(e) == LIBINPUT_KEY_STATE_PRESSED; + + event.type = pressed ? XBMC_KEYDOWN : XBMC_KEYUP; + xkb_state_update_key(m_state, xkbkey, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); + + const xkb_keysym_t keysym = xkb_state_key_get_one_sym(m_state, xkbkey); + + int mod = XBMCKMOD_NONE; + + xkb_state_component modtype = xkb_state_component(XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); + if (xkb_state_mod_index_is_active(m_state, m_modindex[0], modtype) && ((keysym != XBMCK_LCTRL) || !pressed)) + mod |= XBMCKMOD_CTRL; + if (xkb_state_mod_index_is_active(m_state, m_modindex[0], modtype) && ((keysym != XBMCK_RCTRL) || !pressed)) + mod |= XBMCKMOD_CTRL; + if (xkb_state_mod_index_is_active(m_state, m_modindex[1], modtype) && ((keysym != XBMCK_LALT) || !pressed)) + mod |= XBMCKMOD_ALT; + if (xkb_state_mod_index_is_active(m_state, m_modindex[1], modtype) && ((keysym != XBMCK_RALT) || !pressed)) + mod |= XBMCKMOD_ALT; + if (xkb_state_mod_index_is_active(m_state, m_modindex[2], modtype) && ((keysym != XBMCK_LSHIFT) || !pressed)) + mod |= XBMCKMOD_SHIFT; + if (xkb_state_mod_index_is_active(m_state, m_modindex[2], modtype) && ((keysym != XBMCK_RSHIFT) || !pressed)) + mod |= XBMCKMOD_SHIFT; + if (xkb_state_mod_index_is_active(m_state, m_modindex[3], modtype) && ((keysym != XBMCK_LMETA) || !pressed)) + mod |= XBMCKMOD_META; + if (xkb_state_mod_index_is_active(m_state, m_modindex[3], modtype) && ((keysym != XBMCK_RMETA) || !pressed)) + mod |= XBMCKMOD_META; + + m_leds = 0; + + if (xkb_state_led_index_is_active(m_state, m_ledindex[0]) && ((keysym != XBMCK_NUMLOCK) || !pressed)) + { + m_leds |= LIBINPUT_LED_NUM_LOCK; + mod |= XBMCKMOD_NUM; + } + if (xkb_state_led_index_is_active(m_state, m_ledindex[1]) && ((keysym != XBMCK_CAPSLOCK) || !pressed)) + { + m_leds |= LIBINPUT_LED_CAPS_LOCK; + mod |= XBMCKMOD_CAPS; + } + if (xkb_state_led_index_is_active(m_state, m_ledindex[2]) && ((keysym != XBMCK_SCROLLOCK) || !pressed)) + { + m_leds |= LIBINPUT_LED_SCROLL_LOCK; + //mod |= XBMCK_SCROLLOCK; + } + + uint32_t unicode = xkb_state_key_get_utf32(m_state, xkbkey); + if (unicode > std::numeric_limits<std::uint16_t>::max()) + { + // Kodi event system only supports UTF16, so ignore the codepoint if it does not fit + unicode = 0; + } + + uint32_t scancode = libinput_event_keyboard_get_key(e); + if (scancode > std::numeric_limits<unsigned char>::max()) + { + // Kodi scancodes are limited to unsigned char, pretend the scancode is unknown on overflow + scancode = 0; + } + + event.key.keysym.mod = XBMCMod(mod); + event.key.keysym.sym = XBMCKeyForKeysym(keysym, scancode); + event.key.keysym.scancode = scancode; + event.key.keysym.unicode = unicode; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(event); + + if (pressed && xkb_keymap_key_repeats(m_keymap, xkbkey)) + { + libinput_event *ev = libinput_event_keyboard_get_base_event(e); + libinput_device *dev = libinput_event_get_device(ev); + auto data = m_repeatData.find(dev); + if (data != m_repeatData.end()) + { + CLog::Log(LOGDEBUG, "CLibInputKeyboard::{} - using delay: {}ms repeat: {}ms", __FUNCTION__, + data->second.at(0), data->second.at(1)); + + m_repeatRate = data->second.at(1); + m_repeatTimer.Stop(true); + m_repeatEvent = event; + m_repeatTimer.Start(std::chrono::milliseconds(data->second.at(0)), false); + } + } + else + { + m_repeatTimer.Stop(); + } +} + +XBMCKey CLibInputKeyboard::XBMCKeyForKeysym(xkb_keysym_t sym, uint32_t scancode) +{ + if (sym >= 'A' && sym <= 'Z') + { + return static_cast<XBMCKey> (sym + 'a' - 'A'); + } + else if (sym >= XKB_KEY_space && sym <= XKB_KEY_asciitilde) + { + return static_cast<XBMCKey> (sym); + } + else if (sym >= XKB_KEY_F1 && sym <= XKB_KEY_F15) + { + return static_cast<XBMCKey> (XBMCK_F1 + ((int)sym - XKB_KEY_F1)); + } + + auto xkbmapping = xkbMap.find(sym); + if (xkbmapping != xkbMap.end()) + return xkbmapping->second; + + return XBMCK_UNKNOWN; +} + +void CLibInputKeyboard::KeyRepeatTimeout() +{ + m_repeatTimer.RestartAsync(std::chrono::milliseconds(m_repeatRate)); + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(m_repeatEvent); +} + +void CLibInputKeyboard::UpdateLeds(libinput_device *dev) +{ + libinput_device_led_update(dev, static_cast<libinput_led>(m_leds)); +} + +void CLibInputKeyboard::GetRepeat(libinput_device *dev) +{ + int kbdrep[2] = {REPEAT_DELAY, REPEAT_RATE}; + const char *name = libinput_device_get_name(dev); + const char *sysname = libinput_device_get_sysname(dev); + std::string path("/dev/input/"); + path.append(sysname); + auto fd = open(path.c_str(), O_RDONLY); + + if (fd < 0) + { + CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to open {} ({})", __FUNCTION__, sysname, + strerror(errno)); + } + else + { + auto ret = ioctl(fd, EVIOCGREP, &kbdrep); + if (ret < 0) + CLog::Log(LOGDEBUG, "CLibInputKeyboard::{} - could not get key repeat for {} ({})", + __FUNCTION__, sysname, strerror(errno)); + + CLog::Log(LOGDEBUG, "CLibInputKeyboard::{} - delay: {}ms repeat: {}ms for {} ({})", + __FUNCTION__, kbdrep[0], kbdrep[1], name, sysname); + close(fd); + } + + std::vector<int> kbdrepvec(std::begin(kbdrep), std::end(kbdrep)); + + auto data = m_repeatData.find(dev); + if (data == m_repeatData.end()) + { + m_repeatData.insert(std::make_pair(dev, kbdrepvec)); + } +} diff --git a/xbmc/platform/linux/input/LibInputKeyboard.h b/xbmc/platform/linux/input/LibInputKeyboard.h new file mode 100644 index 0000000..76aaeff --- /dev/null +++ b/xbmc/platform/linux/input/LibInputKeyboard.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Timer.h" +#include "windowing/XBMC_events.h" + +#include <map> +#include <vector> + +#include <libinput.h> +#include <xkbcommon/xkbcommon.h> + +class CLibInputKeyboard +{ +public: + CLibInputKeyboard(); + ~CLibInputKeyboard(); + + void ProcessKey(libinput_event_keyboard *e); + void UpdateLeds(libinput_device *dev); + void GetRepeat(libinput_device *dev); + + bool SetKeymap(const std::string& layout); + +private: + XBMCKey XBMCKeyForKeysym(xkb_keysym_t sym, uint32_t scancode); + void KeyRepeatTimeout(); + + xkb_context *m_ctx = nullptr; + xkb_keymap *m_keymap = nullptr; + xkb_state *m_state = nullptr; + xkb_mod_index_t m_modindex[4]; + xkb_led_index_t m_ledindex[3]; + + int m_leds; + + XBMC_Event m_repeatEvent; + std::map<libinput_device*, std::vector<int>> m_repeatData; + CTimer m_repeatTimer; + int m_repeatRate; +}; diff --git a/xbmc/platform/linux/input/LibInputPointer.cpp b/xbmc/platform/linux/input/LibInputPointer.cpp new file mode 100644 index 0000000..42d2bbe --- /dev/null +++ b/xbmc/platform/linux/input/LibInputPointer.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LibInputPointer.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "input/mouse/MouseStat.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include <algorithm> +#include <string.h> + +#include <linux/input.h> + +void CLibInputPointer::ProcessButton(libinput_event_pointer *e) +{ + const uint32_t b = libinput_event_pointer_get_button(e); + const bool pressed = libinput_event_pointer_get_button_state(e) == LIBINPUT_BUTTON_STATE_PRESSED; + unsigned char xbmc_button = 0; + + switch (b) + { + case BTN_LEFT: + xbmc_button = XBMC_BUTTON_LEFT; + break; + case BTN_RIGHT: + xbmc_button = XBMC_BUTTON_RIGHT; + break; + case BTN_MIDDLE: + xbmc_button = XBMC_BUTTON_MIDDLE; + break; + case BTN_SIDE: + xbmc_button = XBMC_BUTTON_X1; + break; + case BTN_EXTRA: + xbmc_button = XBMC_BUTTON_X2; + break; + case BTN_FORWARD: + xbmc_button = XBMC_BUTTON_X3; + break; + case BTN_BACK: + xbmc_button = XBMC_BUTTON_X4; + break; + default: + break; + } + + XBMC_Event event = {}; + + event.type = pressed ? XBMC_MOUSEBUTTONDOWN : XBMC_MOUSEBUTTONUP; + event.button.button = xbmc_button; + event.button.x = static_cast<uint16_t>(m_pos.X); + event.button.y = static_cast<uint16_t>(m_pos.Y); + + CLog::Log(LOGDEBUG, + "CLibInputPointer::{} - event.type: {}, event.button.button: {}, event.button.x: {}, " + "event.button.y: {}", + __FUNCTION__, event.type, event.button.button, event.button.x, event.button.y); + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(event); +} + +void CLibInputPointer::ProcessMotion(libinput_event_pointer *e) +{ + const double dx = libinput_event_pointer_get_dx(e); + const double dy = libinput_event_pointer_get_dy(e); + + m_pos.X += static_cast<int>(dx); + m_pos.Y += static_cast<int>(dy); + + // limit the mouse to the screen width + m_pos.X = std::min(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), m_pos.X); + m_pos.X = std::max(0, m_pos.X); + + // limit the mouse to the screen height + m_pos.Y = std::min(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight(), m_pos.Y); + m_pos.Y = std::max(0, m_pos.Y); + + XBMC_Event event = {}; + + event.type = XBMC_MOUSEMOTION; + event.motion.x = static_cast<uint16_t>(m_pos.X); + event.motion.y = static_cast<uint16_t>(m_pos.Y); + + CLog::Log(LOGDEBUG, + "CLibInputPointer::{} - event.type: {}, event.motion.x: {}, event.motion.y: {}", + __FUNCTION__, event.type, event.motion.x, event.motion.y); + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(event); +} + +void CLibInputPointer::ProcessMotionAbsolute(libinput_event_pointer *e) +{ + m_pos.X = static_cast<int>(libinput_event_pointer_get_absolute_x_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth())); + m_pos.Y = static_cast<int>(libinput_event_pointer_get_absolute_y_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight())); + + XBMC_Event event = {}; + event.type = XBMC_MOUSEMOTION; + event.motion.x = static_cast<uint16_t>(m_pos.X); + event.motion.y = static_cast<uint16_t>(m_pos.Y); + + CLog::Log(LOGDEBUG, + "CLibInputPointer::{} - event.type: {}, event.motion.x: {}, event.motion.y: {}", + __FUNCTION__, event.type, event.motion.x, event.motion.y); + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(event); +} + +void CLibInputPointer::ProcessAxis(libinput_event_pointer *e) +{ + unsigned char scroll = 0; + if (!libinput_event_pointer_has_axis(e, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) + return; + + const double v = libinput_event_pointer_get_axis_value(e, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL); + if (v < 0) + scroll = XBMC_BUTTON_WHEELUP; + else + scroll = XBMC_BUTTON_WHEELDOWN; + + XBMC_Event event = {}; + + event.type = XBMC_MOUSEBUTTONDOWN; + event.button.button = scroll; + event.button.x = static_cast<uint16_t>(m_pos.X); + event.button.y = static_cast<uint16_t>(m_pos.Y); + + CLog::Log(LOGDEBUG, "CLibInputPointer::{} - scroll: {}, event.button.x: {}, event.button.y: {}", + __FUNCTION__, scroll == XBMC_BUTTON_WHEELUP ? "up" : "down", event.button.x, + event.button.y); + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(event); + + event.type = XBMC_MOUSEBUTTONUP; + + if (appPort) + appPort->OnEvent(event); +} diff --git a/xbmc/platform/linux/input/LibInputPointer.h b/xbmc/platform/linux/input/LibInputPointer.h new file mode 100644 index 0000000..608b438 --- /dev/null +++ b/xbmc/platform/linux/input/LibInputPointer.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <libinput.h> + +struct pos +{ + int X; + int Y; +}; + +class CLibInputPointer +{ +public: + CLibInputPointer() = default; + ~CLibInputPointer() = default; + + void ProcessButton(libinput_event_pointer *e); + void ProcessMotion(libinput_event_pointer *e); + void ProcessMotionAbsolute(libinput_event_pointer *e); + void ProcessAxis(libinput_event_pointer *e); + +private: + struct pos m_pos = { 0, 0 }; +}; diff --git a/xbmc/platform/linux/input/LibInputSettings.cpp b/xbmc/platform/linux/input/LibInputSettings.cpp new file mode 100644 index 0000000..25a916e --- /dev/null +++ b/xbmc/platform/linux/input/LibInputSettings.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LibInputSettings.h" + +#include "LibInputHandler.h" +#include "ServiceBroker.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "settings/lib/SettingDefinitions.h" +#include "settings/lib/SettingsManager.h" +#include "utils/XBMCTinyXML.h" +#include "utils/log.h" + +#include <algorithm> + +const std::string CLibInputSettings::SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT = "input.libinputkeyboardlayout"; +static std::vector<StringSettingOption> layouts; + +namespace +{ + inline bool LayoutSort(const StringSettingOption& i, const StringSettingOption& j) + { + return (i.value < j.value); + } +} // unnamed namespace + +CLibInputSettings::CLibInputSettings(CLibInputHandler *handler) : + m_libInputHandler(handler) +{ + const auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return; + + auto settings = settingsComponent->GetSettings(); + if (!settings) + return; + + auto settingsManager = settings->GetSettingsManager(); + if (!settingsManager) + return; + + auto setting = settings->GetSetting(SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT); + if (!setting) + { + CLog::Log(LOGERROR, "Failed to load setting for: {}", SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT); + return; + } + + setting->SetVisible(true); + + std::set<std::string> settingSet; + settingSet.insert(SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT); + settingsManager->RegisterCallback(this, settingSet); + settingsManager->RegisterSettingOptionsFiller("libinputkeyboardlayout", + SettingOptionsKeyboardLayoutsFiller); + + /* load the keyboard layouts from xkeyboard-config */ + std::string xkbFile("/usr/share/X11/xkb/rules/base.xml"); + + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(xkbFile)) + { + CLog::Log(LOGWARNING, "CLibInputSettings: unable to open: {}", xkbFile); + return; + } + + const TiXmlElement* rootElement = xmlDoc.RootElement(); + if (!rootElement) + { + CLog::Log(LOGWARNING, "CLibInputSettings: missing or invalid XML root element in: {}", xkbFile); + return; + } + + if (rootElement->ValueStr() != "xkbConfigRegistry") + { + CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML root element {} in: {}", + rootElement->Value(), xkbFile); + return; + } + + const TiXmlElement* layoutListElement = rootElement->FirstChildElement("layoutList"); + if (!layoutListElement) + { + CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}", + layoutListElement->Value(), xkbFile); + return; + } + + const TiXmlElement* layoutElement = layoutListElement->FirstChildElement("layout"); + while (layoutElement) + { + const TiXmlElement* configElement = layoutElement->FirstChildElement("configItem"); + if (!configElement) + { + CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}", + layoutListElement->Value(), xkbFile); + return; + } + + const TiXmlElement* nameElement = configElement->FirstChildElement("name"); + if (!nameElement) + { + CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}", + configElement->Value(), xkbFile); + return; + } + + const TiXmlElement* descriptionElement = configElement->FirstChildElement("description"); + if (!descriptionElement) + { + CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}", + configElement->Value(), xkbFile); + return; + } + + std::string layout = nameElement->GetText(); + std::string layoutDescription = descriptionElement->GetText(); + + if (!layout.empty() && !layoutDescription.empty()) + layouts.emplace_back(StringSettingOption(layoutDescription, layout)); + + layoutElement = layoutElement->NextSiblingElement(); + } + + std::sort(layouts.begin(), layouts.end(), LayoutSort); +} + +CLibInputSettings::~CLibInputSettings() +{ + const auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return; + + const std::shared_ptr<CSettings> settings = settingsComponent->GetSettings(); + if (!settings) + return; + + settings->GetSettingsManager()->UnregisterSettingOptionsFiller("libinputkeyboardlayout"); + settings->GetSettingsManager()->UnregisterCallback(this); +} + +void CLibInputSettings::SettingOptionsKeyboardLayoutsFiller( + const std::shared_ptr<const CSetting>& setting, + std::vector<StringSettingOption>& list, + std::string& current, + void* data) +{ + list = layouts; +} + +void CLibInputSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT) + { + std::string layout = std::dynamic_pointer_cast<const CSettingString>(setting)->GetValue(); + m_libInputHandler->SetKeymap(layout); + } +} diff --git a/xbmc/platform/linux/input/LibInputSettings.h b/xbmc/platform/linux/input/LibInputSettings.h new file mode 100644 index 0000000..83d5edf --- /dev/null +++ b/xbmc/platform/linux/input/LibInputSettings.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "settings/Settings.h" +#include "settings/lib/ISettingCallback.h" +#include "settings/lib/ISettingsHandler.h" + +#include <memory> +#include <vector> + +class CLibInputHandler; +struct StringSettingOption; + +class CLibInputSettings : public ISettingCallback, public ISettingsHandler +{ +public: + static const std::string SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT; + + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + static void SettingOptionsKeyboardLayoutsFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<StringSettingOption>& list, + std::string& current, + void* data); + + CLibInputSettings(CLibInputHandler *handler); + ~CLibInputSettings() override; + +private: + CLibInputHandler *m_libInputHandler{nullptr}; +}; diff --git a/xbmc/platform/linux/input/LibInputTouch.cpp b/xbmc/platform/linux/input/LibInputTouch.cpp new file mode 100644 index 0000000..545d99a --- /dev/null +++ b/xbmc/platform/linux/input/LibInputTouch.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LibInputTouch.h" + +#include "ServiceBroker.h" +#include "input/touch/generic/GenericTouchActionHandler.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +static inline CPoint GetPos(libinput_event_touch *e) +{ + const double x = libinput_event_touch_get_x_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()); + const double y = libinput_event_touch_get_y_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()); + + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - x: {:f} y: {:f}", __FUNCTION__, x, y); + + return CPoint(x, y); +} + +CLibInputTouch::CLibInputTouch() +{ + m_points.reserve(2); + CGenericTouchInputHandler::GetInstance().RegisterHandler(&CGenericTouchActionHandler::GetInstance()); +} + +void CLibInputTouch::CheckSlot(int slot) +{ + if (slot + 1 > static_cast<int>(m_points.size())) + m_points.resize(slot + 1, std::make_pair(TouchInputUnchanged, CPoint(0, 0))); +} + +TouchInput CLibInputTouch::GetEvent(int slot) +{ + CheckSlot(slot); + return m_points.at(slot).first; +} + +void CLibInputTouch::SetEvent(int slot, TouchInput event) +{ + CheckSlot(slot); + m_points.at(slot).first = event; +} + +void CLibInputTouch::SetPosition(int slot, CPoint point) +{ + CheckSlot(slot); + m_points.at(slot).second = point; +} + +void CLibInputTouch::ProcessTouchDown(libinput_event_touch *e) +{ + int slot = libinput_event_touch_get_seat_slot(e); + + SetPosition(slot, GetPos(e)); + SetEvent(slot, TouchInputDown); + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input down", __FUNCTION__); +} + +void CLibInputTouch::ProcessTouchMotion(libinput_event_touch *e) +{ + int slot = libinput_event_touch_get_seat_slot(e); + uint64_t nanotime = libinput_event_touch_get_time_usec(e) * 1000LL; + + SetPosition(slot, GetPos(e)); + + if (GetEvent(slot) != TouchInputDown) + SetEvent(slot, TouchInputMove); + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input move", __FUNCTION__); + + CGenericTouchInputHandler::GetInstance().UpdateTouchPointer(slot, GetX(slot), GetY(slot), nanotime); +} + +void CLibInputTouch::ProcessTouchUp(libinput_event_touch *e) +{ + int slot = libinput_event_touch_get_seat_slot(e); + + SetEvent(slot, TouchInputUp); + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input up", __FUNCTION__); +} + +void CLibInputTouch::ProcessTouchCancel(libinput_event_touch *e) +{ + int slot = libinput_event_touch_get_seat_slot(e); + uint64_t nanotime = libinput_event_touch_get_time_usec(e) * 1000LL; + + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input cancel", __FUNCTION__); + CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputAbort, GetX(slot), GetY(slot), nanotime, slot); +} + +void CLibInputTouch::ProcessTouchFrame(libinput_event_touch *e) +{ + uint64_t nanotime = libinput_event_touch_get_time_usec(e) * 1000LL; + + for (size_t slot = 0; slot < m_points.size(); ++slot) + { + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input frame: event {}", __FUNCTION__, + GetEvent(slot)); + CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input frame: slot {}", __FUNCTION__, slot); + CGenericTouchInputHandler::GetInstance().HandleTouchInput(GetEvent(slot), GetX(slot), GetY(slot), nanotime, slot); + SetEvent(slot, TouchInputUnchanged); + } +} + diff --git a/xbmc/platform/linux/input/LibInputTouch.h b/xbmc/platform/linux/input/LibInputTouch.h new file mode 100644 index 0000000..9805599 --- /dev/null +++ b/xbmc/platform/linux/input/LibInputTouch.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "input/touch/generic/GenericTouchInputHandler.h" +#include "utils/Geometry.h" + +#include <vector> + +#include <libinput.h> + +struct libinput_event_touch; +struct libinput_device; + +class CLibInputTouch +{ +public: + CLibInputTouch(); + void ProcessTouchDown(libinput_event_touch *e); + void ProcessTouchMotion(libinput_event_touch *e); + void ProcessTouchUp(libinput_event_touch *e); + void ProcessTouchCancel(libinput_event_touch *e); + void ProcessTouchFrame(libinput_event_touch *e); + +private: + void CheckSlot(int slot); + TouchInput GetEvent(int slot); + void SetEvent(int slot, TouchInput event); + void SetPosition(int slot, CPoint point); + int GetX(int slot) { return m_points.at(slot).second.x; } + int GetY(int slot) { return m_points.at(slot).second.y; } + + std::vector<std::pair<TouchInput, CPoint>> m_points{std::make_pair(TouchInputUnchanged, CPoint(0, 0))}; +}; diff --git a/xbmc/platform/linux/network/CMakeLists.txt b/xbmc/platform/linux/network/CMakeLists.txt new file mode 100644 index 0000000..44aa2e7 --- /dev/null +++ b/xbmc/platform/linux/network/CMakeLists.txt @@ -0,0 +1,4 @@ +set(SOURCES NetworkLinux.cpp) +set(HEADERS NetworkLinux.h) + +core_add_library(platform_linux_network) diff --git a/xbmc/platform/linux/network/NetworkLinux.cpp b/xbmc/platform/linux/network/NetworkLinux.cpp new file mode 100644 index 0000000..e9a936a --- /dev/null +++ b/xbmc/platform/linux/network/NetworkLinux.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "NetworkLinux.h" + +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <errno.h> +#include <utility> + +#include <arpa/inet.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <resolv.h> +#include <sys/ioctl.h> + +CNetworkInterfaceLinux::CNetworkInterfaceLinux(CNetworkPosix* network, + std::string interfaceName, + char interfaceMacAddrRaw[6]) + : CNetworkInterfacePosix(network, std::move(interfaceName), interfaceMacAddrRaw) +{ +} + +std::string CNetworkInterfaceLinux::GetCurrentDefaultGateway() const +{ + std::string result; + + FILE* fp = fopen("/proc/net/route", "r"); + if (!fp) + { + // TBD: Error + return result; + } + + char* line = NULL; + char iface[16]; + char dst[128]; + char gateway[128]; + size_t linel = 0; + int n; + int linenum = 0; + while (getdelim(&line, &linel, '\n', fp) > 0) + { + // skip first two lines + if (linenum++ < 1) + continue; + + // search where the word begins + n = sscanf(line, "%15s %127s %127s", iface, dst, gateway); + + if (n < 3) + continue; + + if (strcmp(iface, m_interfaceName.c_str()) == 0 && strcmp(dst, "00000000") == 0 && + strcmp(gateway, "00000000") != 0) + { + unsigned char gatewayAddr[4]; + int len = CNetworkBase::ParseHex(gateway, gatewayAddr); + if (len == 4) + { + struct in_addr in; + in.s_addr = (gatewayAddr[0] << 24) | (gatewayAddr[1] << 16) | (gatewayAddr[2] << 8) | + (gatewayAddr[3]); + result = inet_ntoa(in); + break; + } + } + } + free(line); + fclose(fp); + + return result; +} + +bool CNetworkInterfaceLinux::GetHostMacAddress(unsigned long host_ip, std::string& mac) const +{ + struct arpreq areq; + struct sockaddr_in* sin; + + memset(&areq, 0x0, sizeof(areq)); + + sin = (struct sockaddr_in*)&areq.arp_pa; + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = host_ip; + + sin = (struct sockaddr_in*)&areq.arp_ha; + sin->sin_family = ARPHRD_ETHER; + + strncpy(areq.arp_dev, m_interfaceName.c_str(), sizeof(areq.arp_dev)); + areq.arp_dev[sizeof(areq.arp_dev) - 1] = '\0'; + + int result = ioctl(m_network->GetSocket(), SIOCGARP, (caddr_t)&areq); + + if (result != 0) + { + // CLog::Log(LOGERROR, "{} - GetHostMacAddress/ioctl failed with errno ({})", __FUNCTION__, errno); + return false; + } + + struct sockaddr* res = &areq.arp_ha; + mac = StringUtils::Format("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", (uint8_t)res->sa_data[0], + (uint8_t)res->sa_data[1], (uint8_t)res->sa_data[2], + (uint8_t)res->sa_data[3], (uint8_t)res->sa_data[4], + (uint8_t)res->sa_data[5]); + + for (int i = 0; i < 6; ++i) + if (res->sa_data[i]) + return true; + + return false; +} + +std::unique_ptr<CNetworkBase> CNetworkBase::GetNetwork() +{ + return std::make_unique<CNetworkLinux>(); +} + +CNetworkLinux::CNetworkLinux() : CNetworkPosix() +{ + queryInterfaceList(); +} + +void CNetworkLinux::GetMacAddress(const std::string& interfaceName, char rawMac[6]) +{ + memset(rawMac, 0, 6); + + struct ifreq ifr; + strcpy(ifr.ifr_name, interfaceName.c_str()); + if (ioctl(GetSocket(), SIOCGIFHWADDR, &ifr) >= 0) + { + memcpy(rawMac, ifr.ifr_hwaddr.sa_data, 6); + } +} + +void CNetworkLinux::queryInterfaceList() +{ + char macAddrRaw[6]; + m_interfaces.clear(); + + FILE* fp = fopen("/proc/net/dev", "r"); + if (!fp) + { + // TBD: Error + return; + } + + char* line = NULL; + size_t linel = 0; + int n; + char* p; + int linenum = 0; + while (getdelim(&line, &linel, '\n', fp) > 0) + { + // skip first two lines + if (linenum++ < 2) + continue; + + // search where the word begins + p = line; + while (isspace(*p)) + ++p; + + // read word until : + n = strcspn(p, ": \t"); + p[n] = 0; + + // save the result + std::string interfaceName = p; + GetMacAddress(interfaceName, macAddrRaw); + + // only add interfaces with non-zero mac addresses + if (macAddrRaw[0] || macAddrRaw[1] || macAddrRaw[2] || macAddrRaw[3] || macAddrRaw[4] || + macAddrRaw[5]) + m_interfaces.push_back(new CNetworkInterfaceLinux(this, interfaceName, macAddrRaw)); + } + free(line); + fclose(fp); +} + +std::vector<std::string> CNetworkLinux::GetNameServers() +{ + std::vector<std::string> result; + + res_init(); + + for (int i = 0; i < _res.nscount; i++) + { + std::string ns = inet_ntoa(_res.nsaddr_list[i].sin_addr); + result.push_back(ns); + } + return result; +} + +bool CNetworkLinux::PingHost(unsigned long remote_ip, unsigned int timeout_ms) +{ + char cmd_line[64]; + + struct in_addr host_ip; + host_ip.s_addr = remote_ip; + + sprintf(cmd_line, "ping -c 1 -w %d %s", timeout_ms / 1000 + (timeout_ms % 1000) != 0, + inet_ntoa(host_ip)); + + int status = -1; + status = system(cmd_line); + int result = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + + // http://linux.about.com/od/commands/l/blcmdl8_ping.htm ; + // 0 reply + // 1 no reply + // else some error + + if (result < 0 || result > 1) + CLog::Log(LOGERROR, "Ping fail : status = {}, errno = {} : '{}'", status, errno, cmd_line); + + return result == 0; +} diff --git a/xbmc/platform/linux/network/NetworkLinux.h b/xbmc/platform/linux/network/NetworkLinux.h new file mode 100644 index 0000000..dc7b4c6 --- /dev/null +++ b/xbmc/platform/linux/network/NetworkLinux.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "platform/posix/network/NetworkPosix.h" + +#include <string> +#include <vector> + +class CNetworkInterfaceLinux : public CNetworkInterfacePosix +{ +public: + CNetworkInterfaceLinux(CNetworkPosix* network, + std::string interfaceName, + char interfaceMacAddrRaw[6]); + ~CNetworkInterfaceLinux() override = default; + + std::string GetCurrentDefaultGateway() const override; + bool GetHostMacAddress(unsigned long host, std::string& mac) const override; +}; + +class CNetworkLinux : public CNetworkPosix +{ +public: + CNetworkLinux(); + ~CNetworkLinux() override = default; + + bool PingHost(unsigned long host, unsigned int timeout_ms = 2000) override; + std::vector<std::string> GetNameServers(void) override; + +private: + void GetMacAddress(const std::string& interfaceName, char macAddrRaw[6]) override; + void queryInterfaceList() override; +}; diff --git a/xbmc/platform/linux/network/zeroconf/CMakeLists.txt b/xbmc/platform/linux/network/zeroconf/CMakeLists.txt new file mode 100644 index 0000000..b31ab73 --- /dev/null +++ b/xbmc/platform/linux/network/zeroconf/CMakeLists.txt @@ -0,0 +1,8 @@ +if(AVAHI_FOUND) + set(SOURCES ZeroconfAvahi.cpp + ZeroconfBrowserAvahi.cpp) + set(HEADERS ZeroconfAvahi.h + ZeroconfBrowserAvahi.h) + + core_add_library(platform_linux_network_zeroconf) +endif() diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp new file mode 100644 index 0000000..dea038a --- /dev/null +++ b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ZeroconfAvahi.h" + +#include "utils/log.h" + +#include <cassert> +#include <iostream> +#include <sstream> +#include <string> + +#include <avahi-client/client.h> +#include <avahi-common/alternative.h> +#include <avahi-common/error.h> +#include <avahi-common/malloc.h> +#include <avahi-common/thread-watch.h> +#include <unistd.h> //gethostname + +#include "PlatformDefs.h" + +///helper RAII-struct to block event loop for modifications +struct ScopedEventLoopBlock +{ + explicit ScopedEventLoopBlock(AvahiThreadedPoll* fp_poll):mp_poll(fp_poll) + { + avahi_threaded_poll_lock(mp_poll); + } + + ~ScopedEventLoopBlock() + { + avahi_threaded_poll_unlock(mp_poll); + } +private: + AvahiThreadedPoll* mp_poll; +}; + +///helper to store information on howto create an avahi-group to publish +struct CZeroconfAvahi::ServiceInfo +{ + ServiceInfo(const std::string& fcr_type, const std::string& fcr_name, + unsigned int f_port, AvahiStringList *txt, AvahiEntryGroup* fp_group = 0): + m_type(fcr_type), m_name(fcr_name), m_port(f_port), mp_txt(txt), mp_group(fp_group) + { + } + + std::string m_type; + std::string m_name; + unsigned int m_port; + AvahiStringList* mp_txt; + AvahiEntryGroup* mp_group; +}; + +CZeroconfAvahi::CZeroconfAvahi() +{ + if (! (mp_poll = avahi_threaded_poll_new())) + { + CLog::Log(LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create threaded poll object"); + //! @todo throw exception? + return; + } + + if (!createClient()) + { + CLog::Log(LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create client"); + //yeah, what if not? but should always succeed + } + + //start event loop thread + if (avahi_threaded_poll_start(mp_poll) < 0) + { + CLog::Log(LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Failed to start avahi client thread"); + } +} + +CZeroconfAvahi::~CZeroconfAvahi() +{ + CLog::Log(LOGDEBUG, "CZeroconfAvahi::~CZeroconfAvahi() Going down! cleaning up..."); + + if (mp_poll) + { + //normally we would stop the avahi thread here and do our work, but + //it looks like this does not work -> www.avahi.org/ticket/251 + //so instead of calling + //avahi_threaded_poll_stop(mp_poll); + //we set m_shutdown=true, post an event and wait for it to stop itself + struct timeval tv = { 0, 0 }; //! @todo does tv survive the thread? + AvahiTimeout* lp_timeout; + { + ScopedEventLoopBlock l_block(mp_poll); + const AvahiPoll* cp_apoll = avahi_threaded_poll_get(mp_poll); + m_shutdown = true; + lp_timeout = cp_apoll->timeout_new(cp_apoll, + &tv, + shutdownCallback, + this); + } + + //now wait for the thread to stop + assert(m_thread_id); + pthread_join(m_thread_id, NULL); + avahi_threaded_poll_get(mp_poll)->timeout_free(lp_timeout); + } + + //free the client (frees all browsers, groups, ...) + if (mp_client) + avahi_client_free(mp_client); + if (mp_poll) + avahi_threaded_poll_free(mp_poll); +} + +bool CZeroconfAvahi::doPublishService(const std::string& fcr_identifier, + const std::string& fcr_type, + const std::string& fcr_name, + unsigned int f_port, + const std::vector<std::pair<std::string, std::string> >& txt) +{ + CLog::Log(LOGDEBUG, "CZeroconfAvahi::doPublishService identifier: {} type: {} name:{} port:{}", + fcr_identifier, fcr_type, fcr_name, f_port); + + ScopedEventLoopBlock l_block(mp_poll); + tServiceMap::iterator it = m_services.find(fcr_identifier); + if (it != m_services.end()) + { + //fcr_identifier exists, no update functionality yet, so exit + return false; + } + + //txt records to AvahiStringList + AvahiStringList *txtList = NULL; + for (const auto& it : txt) + { + txtList = avahi_string_list_add_pair(txtList, it.first.c_str(), it.second.c_str()); + } + + //create service info and add it to service map + tServiceMap::mapped_type p_service_info(new CZeroconfAvahi::ServiceInfo(fcr_type, fcr_name, f_port, txtList)); + it = m_services.insert(it, std::make_pair(fcr_identifier, p_service_info)); + + //if client is already running, directly try to add the new service + if ( mp_client && avahi_client_get_state(mp_client) == AVAHI_CLIENT_S_RUNNING ) + { + //client's already running, add this new service + addService(p_service_info, mp_client); + } + else + { + CLog::Log(LOGDEBUG, "CZeroconfAvahi::doPublishService: client not running, queued for publishing"); + } + return true; +} + +bool CZeroconfAvahi::doForceReAnnounceService(const std::string& fcr_identifier) +{ + bool ret = false; + ScopedEventLoopBlock l_block(mp_poll); + tServiceMap::iterator it = m_services.find(fcr_identifier); + if (it != m_services.end() && it->second->mp_group) + { + // to force a reannounce on avahi its enough to reverse the txtrecord list + it->second->mp_txt = avahi_string_list_reverse(it->second->mp_txt); + + // this will trigger the reannouncement + if ((avahi_entry_group_update_service_txt_strlst(it->second->mp_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0), + it->second->m_name.c_str(), + it->second->m_type.c_str(), NULL, it->second->mp_txt)) >= 0) + ret = true; + } + + return ret; +} + +bool CZeroconfAvahi::doRemoveService(const std::string& fcr_ident) +{ + CLog::Log(LOGDEBUG, "CZeroconfAvahi::doRemoveService named: {}", fcr_ident); + ScopedEventLoopBlock l_block(mp_poll); + tServiceMap::iterator it = m_services.find(fcr_ident); + if (it == m_services.end()) + { + return false; + } + + if (it->second->mp_group) + { + avahi_entry_group_free(it->second->mp_group); + it->second->mp_group = 0; + } + + if(it->second->mp_txt) + { + avahi_string_list_free(it->second->mp_txt); + it->second->mp_txt = NULL; + } + + m_services.erase(it); + return true; +} + +void CZeroconfAvahi::doStop() +{ + ScopedEventLoopBlock l_block(mp_poll); + for (auto& it : m_services) + { + if (it.second->mp_group) + { + avahi_entry_group_free(it.second->mp_group); + it.second->mp_group = 0; + } + + if (it.second->mp_txt) + { + avahi_string_list_free(it.second->mp_txt); + it.second->mp_txt = nullptr; + } + } + m_services.clear(); +} + +void CZeroconfAvahi::clientCallback(AvahiClient* fp_client, AvahiClientState f_state, void* fp_data) +{ + CZeroconfAvahi* p_instance = static_cast<CZeroconfAvahi*>(fp_data); + + //store our thread ID and check for shutdown -> check details in destructor + p_instance->m_thread_id = pthread_self(); + + if (p_instance->m_shutdown) + { + avahi_threaded_poll_quit(p_instance->mp_poll); + return; + } + switch(f_state) + { + case AVAHI_CLIENT_S_RUNNING: + CLog::Log(LOGDEBUG, "CZeroconfAvahi::clientCallback: client is up and running"); + p_instance->updateServices(fp_client); + break; + + case AVAHI_CLIENT_FAILURE: + CLog::Log(LOGINFO, "CZeroconfAvahi::clientCallback: client failure. avahi-daemon stopped? Recreating client..."); + //We were forced to disconnect from server. now free and recreate the client object + avahi_client_free(fp_client); + p_instance->mp_client = 0; + //freeing the client also frees all groups and browsers, pointers are undefined afterwards, so fix that now + for (auto& it : p_instance->m_services) + { + it.second->mp_group = 0; + } + p_instance->createClient(); + break; + + case AVAHI_CLIENT_S_COLLISION: + case AVAHI_CLIENT_S_REGISTERING: + //HERE WE SHOULD REMOVE ALL OF OUR SERVICES AND "RESCHEDULE" them for later addition + CLog::Log(LOGDEBUG, "CZeroconfAvahi::clientCallback: uiuui; coll or reg, anyways, resetting groups"); + for (const auto& it : p_instance->m_services) + { + if (it.second->mp_group) + avahi_entry_group_reset(it.second->mp_group); + } + break; + + case AVAHI_CLIENT_CONNECTING: + CLog::Log(LOGINFO, "CZeroconfAvahi::clientCallback: avahi server not available. But may become later..."); + break; + } +} + +void CZeroconfAvahi::groupCallback(AvahiEntryGroup *fp_group, AvahiEntryGroupState f_state, void * fp_data) +{ + CZeroconfAvahi* p_instance = static_cast<CZeroconfAvahi*>(fp_data); + //store our thread ID and check for shutdown -> check details in destructor + p_instance->m_thread_id = pthread_self(); + if (p_instance->m_shutdown) + { + avahi_threaded_poll_quit(p_instance->mp_poll); + return; + } + + switch (f_state) + { + case AVAHI_ENTRY_GROUP_ESTABLISHED : + // The entry group has been established successfully + CLog::Log(LOGDEBUG, "CZeroconfAvahi::groupCallback: Service successfully established"); + break; + + case AVAHI_ENTRY_GROUP_COLLISION : + { + //need to find the ServiceInfo struct for this group + tServiceMap::iterator it = p_instance->m_services.begin(); + for(; it != p_instance->m_services.end(); ++it) + { + if (it->second->mp_group == fp_group) + break; + } + if( it != p_instance->m_services.end() ) { + char* alt_name = avahi_alternative_service_name( it->second->m_name.c_str() ); + it->second->m_name = alt_name; + avahi_free(alt_name); + CLog::Log(LOGINFO, "CZeroconfAvahi::groupCallback: Service name collision. Renamed to: {}", + it->second->m_name); + p_instance->addService(it->second, p_instance->mp_client); + } + break; + } + + case AVAHI_ENTRY_GROUP_FAILURE: + CLog::Log(LOGERROR, "CZeroconfAvahi::groupCallback: Entry group failure: {} ", + (fp_group) + ? avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(fp_group))) + : "Unknown"); + //free the group and set to 0 so it may be added later + if (fp_group) + { + //need to find the ServiceInfo struct for this group + for (auto& it : p_instance->m_services) + { + if (it.second->mp_group == fp_group) + { + avahi_entry_group_free(it.second->mp_group); + it.second->mp_group = 0; + if (it.second->mp_txt) + { + avahi_string_list_free(it.second->mp_txt); + it.second->mp_txt = nullptr; + } + break; + } + } + } + break; + + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + default: + break; + } +} + +void CZeroconfAvahi::shutdownCallback(AvahiTimeout *fp_e, void *fp_data) +{ + CZeroconfAvahi* p_instance = static_cast<CZeroconfAvahi*>(fp_data); + //should only be called on shutdown + if (p_instance->m_shutdown) + { + avahi_threaded_poll_quit(p_instance->mp_poll); + } +} + +bool CZeroconfAvahi::createClient() +{ + if (mp_client) + { + avahi_client_free(mp_client); + } + + int error = 0; + mp_client = avahi_client_new(avahi_threaded_poll_get(mp_poll), + AVAHI_CLIENT_NO_FAIL, &clientCallback,this,&error); + if (!mp_client) + { + CLog::Log(LOGERROR, "CZeroconfAvahi::createClient() failed with {}", error); + return false; + } + return true; +} + +void CZeroconfAvahi::updateServices(AvahiClient* fp_client) +{ + for (const auto& it : m_services) + { + if (!it.second->mp_group) + addService(it.second, fp_client); + } +} + +void CZeroconfAvahi::addService(const tServiceMap::mapped_type& fp_service_info, + AvahiClient* fp_client) +{ + assert(fp_client); + CLog::Log(LOGDEBUG, "CZeroconfAvahi::addService() named: {} type: {} port:{}", + fp_service_info->m_name, fp_service_info->m_type, fp_service_info->m_port); + //create the group if it doesn't exist + if (!fp_service_info->mp_group) + { + if (!(fp_service_info->mp_group = avahi_entry_group_new(fp_client, &CZeroconfAvahi::groupCallback, this))) + { + CLog::Log(LOGDEBUG, "CZeroconfAvahi::addService() avahi_entry_group_new() failed: {}", + avahi_strerror(avahi_client_errno(fp_client))); + fp_service_info->mp_group = 0; + return; + } + } + + + // add entries to the group if it's empty + int ret; + if (avahi_entry_group_is_empty(fp_service_info->mp_group)) + { + if ((ret = avahi_entry_group_add_service_strlst(fp_service_info->mp_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0), + fp_service_info->m_name.c_str(), + fp_service_info->m_type.c_str(), NULL, NULL, fp_service_info->m_port, fp_service_info->mp_txt)) < 0) + { + if (ret == AVAHI_ERR_COLLISION) + { + char* alt_name = avahi_alternative_service_name(fp_service_info->m_name.c_str()); + fp_service_info->m_name = alt_name; + avahi_free(alt_name); + CLog::Log(LOGINFO, "CZeroconfAvahi::addService: Service name collision. Renamed to: {}", + fp_service_info->m_name); + addService(fp_service_info, fp_client); + return; + } + CLog::Log(LOGERROR, + "CZeroconfAvahi::addService(): failed to add service named:{}@$(HOSTNAME) type:{} " + "port:{}. Error:{} :/ FIXME!", + fp_service_info->m_name, fp_service_info->m_type, fp_service_info->m_port, + avahi_strerror(ret)); + return; + } + } + + // Tell the server to register the service + if ((ret = avahi_entry_group_commit(fp_service_info->mp_group)) < 0) + { + CLog::Log(LOGERROR, "CZeroconfAvahi::addService(): Failed to commit entry group! Error:{}", + avahi_strerror(ret)); + //! @todo what now? reset the group? free it? + } +} diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h new file mode 100644 index 0000000..102df9b --- /dev/null +++ b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "network/Zeroconf.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include <avahi-client/client.h> +#include <avahi-client/publish.h> +#include <avahi-common/defs.h> + +struct AvahiThreadedPoll; + +class CZeroconfAvahi : public CZeroconf +{ +public: + CZeroconfAvahi(); + ~CZeroconfAvahi() override; + +protected: + //implement base CZeroConf interface + bool doPublishService(const std::string& fcr_identifier, + const std::string& fcr_type, + const std::string& fcr_name, + unsigned int f_port, + const std::vector<std::pair<std::string, std::string> >& txt) override; + + bool doForceReAnnounceService(const std::string& fcr_identifier) override; + bool doRemoveService(const std::string& fcr_ident) override; + + void doStop() override; + +private: + ///this is where the client calls us if state changes + static void clientCallback(AvahiClient* fp_client, AvahiClientState f_state, void*); + ///here we get notified of group changes + static void groupCallback(AvahiEntryGroup *fp_group, AvahiEntryGroupState f_state, void *); + //shutdown callback; works around a problem in avahi < 0.6.24 see destructor for details + static void shutdownCallback(AvahiTimeout *fp_e, void *); + + ///creates the avahi client; + ///@return true on success + bool createClient(); + + //don't access stuff below without stopping the client thread + //see http://avahi.org/wiki/RunningAvahiClientAsThread + //and use struct ScopedEventLoopBlock + + //helper struct for holding information about creating a service / AvahiEntryGroup + //we have to hold that as it's needed to recreate the service + struct ServiceInfo; + typedef std::map<std::string, std::shared_ptr<ServiceInfo> > tServiceMap; + + //goes through a list of todos and publishs them (takes the client a param, as it might be called from + // from the callbacks) + void updateServices(AvahiClient* fp_client); + //helper that actually does the work of publishing + void addService(const tServiceMap::mapped_type& fp_service_info, AvahiClient* fp_client); + + AvahiClient* mp_client = 0; + AvahiThreadedPoll* mp_poll = 0; + + //this holds all published and unpublished services including info on howto create them + tServiceMap m_services; + + //2 variables below are needed for workaround of avahi bug (see destructor for details) + bool m_shutdown = false; + pthread_t m_thread_id = 0; +}; diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp new file mode 100644 index 0000000..491b98b --- /dev/null +++ b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ZeroconfBrowserAvahi.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUIWindowManager.h" +#include "utils/log.h" + +#include <avahi-common/error.h> +#include <avahi-common/malloc.h> +namespace +{ +///helper RAII-struct to block event loop for modifications +struct ScopedEventLoopBlock +{ + explicit ScopedEventLoopBlock ( AvahiThreadedPoll* fp_poll ) : mp_poll ( fp_poll ) + { + avahi_threaded_poll_lock ( mp_poll ); + } + + ~ScopedEventLoopBlock() + { + avahi_threaded_poll_unlock ( mp_poll ); + } +private: + AvahiThreadedPoll* mp_poll; +}; +} + +CZeroconfBrowserAvahi::CZeroconfBrowserAvahi() +{ + if ( ! ( mp_poll = avahi_threaded_poll_new() ) ) + { + CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create threaded poll object" ); + //! @todo throw exception? can this even happen? + return; + } + + if ( !createClient() ) + { + CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create client" ); + //yeah, what if not? but should always succeed (as client_no_fail or something is passed) + } + + //start event loop thread + if ( avahi_threaded_poll_start ( mp_poll ) < 0 ) + { + CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Failed to start avahi client thread" ); + } +} + +CZeroconfBrowserAvahi::~CZeroconfBrowserAvahi() +{ + CLog::Log ( LOGDEBUG, "CZeroconfAvahi::~CZeroconfAvahi() Going down! cleaning up..." ); + if ( mp_poll ) + { + //stop avahi polling thread + avahi_threaded_poll_stop(mp_poll); + } + //free the client (frees all browsers, groups, ...) + if ( mp_client ) + avahi_client_free ( mp_client ); + if ( mp_poll ) + avahi_threaded_poll_free ( mp_poll ); +} + +bool CZeroconfBrowserAvahi::doAddServiceType ( const std::string& fcr_service_type ) +{ + ScopedEventLoopBlock lock ( mp_poll ); + tBrowserMap::iterator it = m_browsers.find ( fcr_service_type ); + if ( it != m_browsers.end() ) + return false; + else + it = m_browsers.insert ( std::make_pair ( fcr_service_type, ( AvahiServiceBrowser* ) 0 ) ).first; + + //if the client is running, we directly create a browser for the service here + if ( mp_client && avahi_client_get_state ( mp_client ) == AVAHI_CLIENT_S_RUNNING ) + { + AvahiServiceBrowser* browser = createServiceBrowser ( fcr_service_type, mp_client, this); + if ( !browser ) + { + m_browsers.erase ( it ); + return false; + } + else + { + it->second = browser; + return true; + } + } + else + { + CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::doAddServiceType client not available. service browsing queued" ); + return true; + } +} + +bool CZeroconfBrowserAvahi::doRemoveServiceType ( const std::string& fcr_service_type ) +{ + ScopedEventLoopBlock lock ( mp_poll ); + tBrowserMap::iterator it = m_browsers.find ( fcr_service_type ); + if ( it == m_browsers.end() ) + return false; + else + { + if ( it->second ) + { + avahi_service_browser_free ( it->second ); + m_all_for_now_browsers.erase ( it->second ); + } + m_browsers.erase ( it ); + //remove this serviceType from the list of discovered services + for (auto itr = m_discovered_services.begin(); itr != m_discovered_services.end();) + { + if (itr->first.GetType() == fcr_service_type) + itr = m_discovered_services.erase(itr); + else + ++itr; + } + } + return true; +} + +std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowserAvahi::doGetFoundServices() +{ + std::vector<CZeroconfBrowser::ZeroconfService> ret; + ScopedEventLoopBlock lock ( mp_poll ); + ret.reserve ( m_discovered_services.size() ); + for (const auto& it : m_discovered_services) + ret.push_back(it.first); + return ret; +} + +bool CZeroconfBrowserAvahi::doResolveService ( CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout ) +{ + { + //wait for lock on event-loop to schedule resolving + ScopedEventLoopBlock lock ( mp_poll ); + //avahi can only resolve already discovered services, as it needs info from there + tDiscoveredServices::const_iterator it = m_discovered_services.find( fr_service ); + if ( it == m_discovered_services.end() ) + { + CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::doResolveService called with undiscovered service, resolving is NOT possible" ); + return false; + } + //start resolving + m_resolving_service = fr_service; + m_resolved_event.Reset(); + if ( !avahi_service_resolver_new ( mp_client, it->second.interface, it->second.protocol, + it->first.GetName().c_str(), it->first.GetType().c_str(), it->first.GetDomain().c_str(), + AVAHI_PROTO_UNSPEC, AvahiLookupFlags ( 0 ), resolveCallback, this ) ) + { + CLog::Log(LOGERROR, + "CZeroconfBrowserAvahi::doResolveService Failed to resolve service '{}': {}\n", + it->first.GetName(), avahi_strerror(avahi_client_errno(mp_client))); + return false; + } + } // end of this block releases lock of eventloop + + //wait for resolve to return or timeout + m_resolved_event.Wait(std::chrono::duration<double, std::milli>(f_timeout * 1000)); + { + ScopedEventLoopBlock lock ( mp_poll ); + fr_service = m_resolving_service; + return (!fr_service.GetIP().empty()); + } +} + +void CZeroconfBrowserAvahi::clientCallback ( AvahiClient* fp_client, AvahiClientState f_state, void* fp_data ) +{ + CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( fp_data ); + switch ( f_state ) + { + case AVAHI_CLIENT_S_RUNNING: + { + CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::clientCallback: client is up and running" ); + for (auto& it : p_instance->m_browsers) + { + assert(!it.second); + it.second = createServiceBrowser(it.first, fp_client, fp_data); + } + break; + } + case AVAHI_CLIENT_FAILURE: + { + CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::clientCallback: client failure. avahi-daemon stopped? Recreating client..." ); + //We were forced to disconnect from server. now free and recreate the client object + avahi_client_free ( fp_client ); + p_instance->mp_client = 0; + //freeing the client also frees all groups and browsers, pointers are undefined afterwards, so fix that now + for (auto& it : p_instance->m_browsers) + it.second = (AvahiServiceBrowser*)0; + //clean the list of discovered services and update gui (if someone is interested) + p_instance->m_discovered_services.clear(); + CGUIMessage message ( GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH ); + message.SetStringParam ( "zeroconf://" ); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage ( message ); + p_instance->createClient(); + break; + } + case AVAHI_CLIENT_S_COLLISION: + case AVAHI_CLIENT_S_REGISTERING: + //HERE WE SHOULD REMOVE ALL OF OUR SERVICES AND "RESCHEDULE" them for later addition + CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::clientCallback: This should not happen" ); + break; + + case AVAHI_CLIENT_CONNECTING: + CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::clientCallback: avahi server not available. But may become later..." ); + break; + } +} +void CZeroconfBrowserAvahi::browseCallback ( + AvahiServiceBrowser *browser, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, + const char *name, const char *type, const char *domain, + AvahiLookupResultFlags flags, void* fp_data ) +{ + CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( fp_data ); + assert ( browser ); + bool update_gui = false; + /* Called whenever a new services becomes available on the LAN or is removed from the LAN */ + switch ( event ) + { + case AVAHI_BROWSER_FAILURE: + CLog::Log(LOGERROR, "CZeroconfBrowserAvahi::browseCallback error: {}\n", + avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(browser)))); + //! @todo implement + return; + case AVAHI_BROWSER_NEW: + { + CLog::Log( + LOGDEBUG, + "CZeroconfBrowserAvahi::browseCallback NEW: service '{}' of type '{}' in domain '{}'\n", + name, type, domain); + //store the service + ZeroconfService service(name, type, domain); + AvahiSpecificInfos info; + info.interface = interface; + info.protocol = protocol; + p_instance->m_discovered_services.insert ( std::make_pair ( service, info ) ); + //if this browser already sent the all for now message, we need to update the gui now + if( p_instance->m_all_for_now_browsers.find(browser) != p_instance->m_all_for_now_browsers.end() ) + update_gui = true; + break; + } + case AVAHI_BROWSER_REMOVE: + { + //remove the service + ZeroconfService service(name, type, domain); + p_instance->m_discovered_services.erase ( service ); + CLog::Log(LOGDEBUG, + "CZeroconfBrowserAvahi::browseCallback REMOVE: service '{}' of type '{}' in " + "domain '{}'\n", + name, type, domain); + //if this browser already sent the all for now message, we need to update the gui now + if( p_instance->m_all_for_now_browsers.find(browser) != p_instance->m_all_for_now_browsers.end() ) + update_gui = true; + break; + } + case AVAHI_BROWSER_CACHE_EXHAUSTED: + //do we need that? + break; + case AVAHI_BROWSER_ALL_FOR_NOW: + CLog::Log(LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback all for now (service = {})", type); + //if this browser already sent the all for now message, we need to update the gui now + bool success = p_instance->m_all_for_now_browsers.insert(browser).second; + if(!success) + CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback AVAHI_BROWSER_ALL_FOR_NOW sent twice?!"); + update_gui = true; + break; + } + if ( update_gui ) + { + CGUIMessage message ( GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH ); + message.SetStringParam ( "zeroconf://" ); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage ( message ); + CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback sent gui update for path zeroconf://" ); + } +} + +CZeroconfBrowser::ZeroconfService::tTxtRecordMap GetTxtRecords(AvahiStringList *txt) +{ + AvahiStringList *i = NULL; + CZeroconfBrowser::ZeroconfService::tTxtRecordMap recordMap; + + for( i = txt; i; i = i->next ) + { + char *key, *value; + + if( avahi_string_list_get_pair( i, &key, &value, NULL ) < 0 ) + continue; + + recordMap.insert( + std::make_pair( + std::string(key), + std::string(value) + ) + ); + + if( key ) + avahi_free(key); + if( value ) + avahi_free(value); + } + return recordMap; +} + +void CZeroconfBrowserAvahi::resolveCallback( + AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, + const char *name, const char *type, const char *domain, const char *host_name, + const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void* userdata ) +{ + assert ( r ); + assert ( userdata ); + CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( userdata ); + switch ( event ) + { + case AVAHI_RESOLVER_FAILURE: + CLog::Log(LOGERROR, + "CZeroconfBrowserAvahi::resolveCallback Failed to resolve service '{}' of type " + "'{}' in domain '{}': {}\n", + name, type, domain, + avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r)))); + break; + case AVAHI_RESOLVER_FOUND: + { + char a[AVAHI_ADDRESS_STR_MAX]; + CLog::Log(LOGDEBUG, + "CZeroconfBrowserAvahi::resolveCallback resolved service '{}' of type '{}' in " + "domain '{}':\n", + name, type, domain); + + avahi_address_snprint ( a, sizeof ( a ), address ); + p_instance->m_resolving_service.SetIP(a); + p_instance->m_resolving_service.SetPort(port); + //get txt-record list + p_instance->m_resolving_service.SetTxtRecords(GetTxtRecords(txt)); + break; + } + } + avahi_service_resolver_free ( r ); + p_instance->m_resolved_event.Set(); +} + +bool CZeroconfBrowserAvahi::createClient() +{ + assert ( mp_poll ); + if ( mp_client ) + { + avahi_client_free ( mp_client ); + } + mp_client = avahi_client_new ( avahi_threaded_poll_get ( mp_poll ), + AVAHI_CLIENT_NO_FAIL, &clientCallback, this, 0 ); + if ( !mp_client ) + { + mp_client = 0; + return false; + } + return true; +} + +AvahiServiceBrowser* CZeroconfBrowserAvahi::createServiceBrowser ( const std::string& fcr_service_type, AvahiClient* fp_client, void* fp_userdata) +{ + assert(fp_client); + AvahiServiceBrowser* ret = avahi_service_browser_new ( fp_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, fcr_service_type.c_str(), + NULL, ( AvahiLookupFlags ) 0, browseCallback, fp_userdata ); + if ( !ret ) + { + CLog::Log( + LOGERROR, + "CZeroconfBrowserAvahi::createServiceBrowser Failed to create service ({}) browser: {}", + avahi_strerror(avahi_client_errno(fp_client)), fcr_service_type); + } + return ret; +} diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h new file mode 100644 index 0000000..cc8c6c4 --- /dev/null +++ b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "network/ZeroconfBrowser.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <map> +#include <memory> +#include <vector> + +#include <avahi-client/client.h> +#include <avahi-client/lookup.h> +#include <avahi-common/defs.h> +#include <avahi-common/thread-watch.h> + +//platform specific implementation of zeroconfbrowser interface using avahi +class CZeroconfBrowserAvahi : public CZeroconfBrowser +{ + public: + CZeroconfBrowserAvahi(); + ~CZeroconfBrowserAvahi() override; + + private: + ///implementation if CZeroconfBrowser interface + ///@{ + bool doAddServiceType(const std::string& fcr_service_type) override; + bool doRemoveServiceType(const std::string& fcr_service_type) override; + + std::vector<CZeroconfBrowser::ZeroconfService> doGetFoundServices() override; + bool doResolveService(CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout) override; + ///@} + + /// avahi callbacks + ///@{ + static void clientCallback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata); + static void browseCallback( + AvahiServiceBrowser *b, + AvahiIfIndex interface, + AvahiProtocol protocol, + AvahiBrowserEvent event, + const char *name, + const char *type, + const char *domain, + AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, + void* userdata); + static void resolveCallback( + AvahiServiceResolver *r, + AvahiIfIndex interface, + AvahiProtocol protocol, + AvahiResolverEvent event, + const char *name, + const char *type, + const char *domain, + const char *host_name, + const AvahiAddress *address, + uint16_t port, + AvahiStringList *txt, + AvahiLookupResultFlags flags, + AVAHI_GCC_UNUSED void* userdata); + //helpers + bool createClient(); + static AvahiServiceBrowser* createServiceBrowser(const std::string& fcr_service_type, AvahiClient* fp_client, void* fp_userdata); + + //shared variables between avahi thread and interface + AvahiClient* mp_client = 0 ; + AvahiThreadedPoll* mp_poll = 0 ; + // tBrowserMap maps service types the corresponding browser + typedef std::map<std::string, AvahiServiceBrowser*> tBrowserMap; + tBrowserMap m_browsers; + + // if a browser is in this set, it already sent an ALL_FOR_NOW message + // (needed to bundle GUI_MSG_UPDATE_PATH messages + std::set<AvahiServiceBrowser*> m_all_for_now_browsers; + + //this information is needed for avahi to resolve a service, + //so unfortunately we'll only be able to resolve services already discovered + struct AvahiSpecificInfos + { + AvahiIfIndex interface; + AvahiProtocol protocol; + }; + typedef std::map<CZeroconfBrowser::ZeroconfService, AvahiSpecificInfos> tDiscoveredServices; + tDiscoveredServices m_discovered_services; + CZeroconfBrowser::ZeroconfService m_resolving_service; + CEvent m_resolved_event; +}; diff --git a/xbmc/platform/linux/peripherals/CMakeLists.txt b/xbmc/platform/linux/peripherals/CMakeLists.txt new file mode 100644 index 0000000..626aa40 --- /dev/null +++ b/xbmc/platform/linux/peripherals/CMakeLists.txt @@ -0,0 +1,11 @@ +if(UDEV_FOUND) + list(APPEND SOURCES PeripheralBusUSBLibUdev.cpp) + list(APPEND HEADERS PeripheralBusUSBLibUdev.h) +elseif(LIBUSB_FOUND) + list(APPEND SOURCES PeripheralBusUSBLibUSB.cpp) + list(APPEND HEADERS PeripheralBusUSBLibUSB.h) +endif() + +if(SOURCES) + core_add_library(platform_linux_peripherals) +endif() diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp new file mode 100644 index 0000000..5c788bb --- /dev/null +++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralBusUSBLibUSB.h" + +#include "peripherals/Peripherals.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <usb.h> + +using namespace PERIPHERALS; + +CPeripheralBusUSB::CPeripheralBusUSB(CPeripherals& manager) : + CPeripheralBus("PeripBusUSB", manager, PERIPHERAL_BUS_USB) +{ + usb_init(); + usb_find_busses(); + m_busses = usb_get_busses(); + CLog::Log(LOGDEBUG, "{} - using libusb peripheral scanning", __FUNCTION__); +} + +bool CPeripheralBusUSB::PerformDeviceScan(PeripheralScanResults &results) +{ + struct usb_bus *bus; + usb_find_devices(); + for (bus = m_busses; bus; bus = bus->next) + { + struct usb_device *dev; + for (dev = bus->devices; dev; dev = dev->next) + { + PeripheralScanResult result(m_type); + result.m_iVendorId = dev->descriptor.idVendor; + result.m_iProductId = dev->descriptor.idProduct; + result.m_type = (dev->descriptor.bDeviceClass == USB_CLASS_PER_INTERFACE && dev->descriptor.bNumConfigurations > 0 && + dev->config[0].bNumInterfaces > 0 && dev->config[0].interface[0].num_altsetting > 0) ? + GetType(dev->config[0].interface[0].altsetting[0].bInterfaceClass) : + GetType(dev->descriptor.bDeviceClass); +#ifdef TARGET_FREEBSD + result.m_strLocation = std::to_string(dev->filename); +#else + result.m_strLocation = StringUtils::Format("/bus{}/dev{}", bus->dirname, dev->filename); +#endif + result.m_iSequence = GetNumberOfPeripheralsWithId(result.m_iVendorId, result.m_iProductId); + if (!results.ContainsResult(result)) + results.m_results.push_back(result); + } + } + + return true; +} + +const PeripheralType CPeripheralBusUSB::GetType(int iDeviceClass) +{ + switch (iDeviceClass) + { + case USB_CLASS_HID: + return PERIPHERAL_HID; + case USB_CLASS_COMM: + return PERIPHERAL_NIC; + case USB_CLASS_MASS_STORAGE: + return PERIPHERAL_DISK; + case USB_CLASS_PER_INTERFACE: + case USB_CLASS_AUDIO: + case USB_CLASS_PRINTER: + case USB_CLASS_PTP: + case USB_CLASS_HUB: + case USB_CLASS_DATA: + case USB_CLASS_VENDOR_SPEC: + default: + return PERIPHERAL_UNKNOWN; + } +} diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h new file mode 100644 index 0000000..d2e7e48 --- /dev/null +++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "peripherals/bus/PeripheralBus.h" +#include "peripherals/devices/Peripheral.h" + +struct usb_bus; + +namespace PERIPHERALS +{ + class CPeripherals; + + class CPeripheralBusUSB : public CPeripheralBus + { + public: + explicit CPeripheralBusUSB(CPeripherals& manager); + + /*! + * @see PeripheralBus::PerformDeviceScan() + */ + bool PerformDeviceScan(PeripheralScanResults &results); + + protected: + static const PeripheralType GetType(int iDeviceClass); + struct usb_bus *m_busses; + }; +} diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp new file mode 100644 index 0000000..aeb5e71 --- /dev/null +++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralBusUSBLibUdev.h" +#include "peripherals/Peripherals.h" +extern "C" { +#include <libudev.h> +} +#include <cassert> +#include <poll.h> +#include "utils/log.h" + +#ifndef USB_CLASS_PER_INTERFACE +#define USB_CLASS_PER_INTERFACE 0 +#endif + +#ifndef USB_CLASS_AUDIO +#define USB_CLASS_AUDIO 1 +#endif + +#ifndef USB_CLASS_COMM +#define USB_CLASS_COMM 2 +#endif + +#ifndef USB_CLASS_HID +#define USB_CLASS_HID 3 +#endif + +#ifndef USB_CLASS_PHYSICAL +#define USB_CLASS_PHYSICAL 5 +#endif + +#ifndef USB_CLASS_PTP +#define USB_CLASS_PTP 6 +#endif + +#ifndef USB_CLASS_PRINTER +#define USB_CLASS_PRINTER 7 +#endif + +#ifndef USB_CLASS_MASS_STORAGE +#define USB_CLASS_MASS_STORAGE 8 +#endif + +#ifndef USB_CLASS_HUB +#define USB_CLASS_HUB 9 +#endif + +#ifndef USB_CLASS_DATA +#define USB_CLASS_DATA 10 +#endif + +#ifndef USB_CLASS_APP_SPEC +#define USB_CLASS_APP_SPEC 0xfe +#endif + +#ifndef USB_CLASS_VENDOR_SPEC +#define USB_CLASS_VENDOR_SPEC 0xff +#endif + +using namespace PERIPHERALS; + +CPeripheralBusUSB::CPeripheralBusUSB(CPeripherals& manager) : + CPeripheralBus("PeripBusUSBUdev", manager, PERIPHERAL_BUS_USB) +{ + /* the Process() method in this class overrides the one in CPeripheralBus, so leave this set to true */ + m_bNeedsPolling = true; + + m_udev = NULL; + m_udevMon = NULL; +} + +CPeripheralBusUSB::~CPeripheralBusUSB(void) +{ + StopThread(true); +} + +bool CPeripheralBusUSB::PerformDeviceScan(PeripheralScanResults &results) +{ + // We don't want this one to be called from outside world + assert(IsCurrentThread()); + + struct udev_enumerate *enumerate; + struct udev_list_entry *devices, *dev_list_entry; + struct udev_device *dev(NULL), *parent(NULL); + enumerate = udev_enumerate_new(m_udev); + udev_enumerate_scan_devices(enumerate); + devices = udev_enumerate_get_list_entry(enumerate); + + bool bContinue(true); + std::string strPath, strClass; + udev_list_entry_foreach(dev_list_entry, devices) + { + strPath = udev_list_entry_get_name(dev_list_entry); + if (strPath.empty()) + bContinue = false; + + if (bContinue) + { + if (!(parent = udev_device_new_from_syspath(m_udev, strPath.c_str()))) + bContinue = false; + } + + if (bContinue) + { + dev = udev_device_get_parent(udev_device_get_parent(parent)); + if (!dev || !udev_device_get_sysattr_value(dev,"idVendor") || !udev_device_get_sysattr_value(dev, "idProduct")) + bContinue = false; + } + + if (bContinue) + { + strClass = udev_device_get_sysattr_value(dev, "bDeviceClass"); + if (strClass.empty()) + bContinue = false; + } + + if (bContinue) + { + int iClass = PeripheralTypeTranslator::HexStringToInt(strClass.c_str()); + if (iClass == USB_CLASS_PER_INTERFACE) + { + //! @todo just assume this is a HID device for now, since the only devices that we're currently + //! interested in are HID devices + iClass = USB_CLASS_HID; + } + + PeripheralScanResult result(m_type); + result.m_iVendorId = PeripheralTypeTranslator::HexStringToInt(udev_device_get_sysattr_value(dev, "idVendor")); + result.m_iProductId = PeripheralTypeTranslator::HexStringToInt(udev_device_get_sysattr_value(dev, "idProduct")); + result.m_type = GetType(iClass); + result.m_strLocation = udev_device_get_syspath(dev); + result.m_iSequence = GetNumberOfPeripheralsWithId(result.m_iVendorId, result.m_iProductId); + if (!results.ContainsResult(result)) + results.m_results.push_back(result); + } + + bContinue = true; + if (parent) + { + /* unref the _parent_ device */ + udev_device_unref(parent); + parent = NULL; + } + } + /* Free the enumerator object */ + udev_enumerate_unref(enumerate); + + return true; +} + +PeripheralType CPeripheralBusUSB::GetType(int iDeviceClass) +{ + switch (iDeviceClass) + { + case USB_CLASS_HID: + return PERIPHERAL_HID; + case USB_CLASS_COMM: + return PERIPHERAL_NIC; + case USB_CLASS_MASS_STORAGE: + return PERIPHERAL_DISK; + case USB_CLASS_PER_INTERFACE: + case USB_CLASS_AUDIO: + case USB_CLASS_PRINTER: + case USB_CLASS_PTP: + case USB_CLASS_HUB: + case USB_CLASS_DATA: + case USB_CLASS_VENDOR_SPEC: + default: + return PERIPHERAL_UNKNOWN; + } +} + +void CPeripheralBusUSB::Process(void) +{ + if (!(m_udev = udev_new())) + { + CLog::Log(LOGERROR, "{} - failed to allocate udev context", __FUNCTION__); + return; + } + + /* set up a devices monitor that listen for any device change */ + m_udevMon = udev_monitor_new_from_netlink(m_udev, "udev"); + + /* filter to only receive usb events */ + if (udev_monitor_filter_add_match_subsystem_devtype(m_udevMon, "usb", nullptr) < 0) + { + CLog::Log(LOGERROR, "Could not limit filter on USB only"); + } + + CLog::Log(LOGDEBUG, "{} - initialised udev monitor", __FUNCTION__); + + udev_monitor_enable_receiving(m_udevMon); + bool bUpdated(false); + ScanForDevices(); + while (!m_bStop) + { + bUpdated = WaitForUpdate(); + if (bUpdated && !m_bStop) + ScanForDevices(); + } + udev_monitor_unref(m_udevMon); + udev_unref(m_udev); +} + +void CPeripheralBusUSB::Clear(void) +{ + StopThread(false); + + CPeripheralBus::Clear(); +} + +bool CPeripheralBusUSB::WaitForUpdate() +{ + int udevFd = udev_monitor_get_fd(m_udevMon); + + if (udevFd < 0) + { + CLog::Log(LOGERROR, "{} - get udev monitor", __FUNCTION__); + return false; + } + + /* poll for udev changes */ + struct pollfd pollFd; + pollFd.fd = udevFd; + pollFd.events = POLLIN; + int iPollResult; + while (!m_bStop && ((iPollResult = poll(&pollFd, 1, 100)) <= 0)) + if (errno != EINTR && iPollResult != 0) + break; + + /* the thread is being stopped, so just return false */ + if (m_bStop) + return false; + + /* we have to read the message from the queue, even though we're not actually using it */ + struct udev_device *dev = udev_monitor_receive_device(m_udevMon); + if (dev) + udev_device_unref(dev); + else + { + CLog::Log(LOGERROR, "{} - failed to get device from udev_monitor_receive_device()", + __FUNCTION__); + Clear(); + return false; + } + + return true; +} diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h new file mode 100644 index 0000000..72cb437 --- /dev/null +++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "peripherals/bus/PeripheralBus.h" +#include "peripherals/devices/Peripheral.h" + +struct udev; +struct udev_monitor; + +namespace PERIPHERALS +{ + class CPeripherals; + + class CPeripheralBusUSB : public CPeripheralBus + { + public: + explicit CPeripheralBusUSB(CPeripherals& manager); + ~CPeripheralBusUSB(void) override; + + void Clear(void) override; + + /*! + * @see PeripheralBus::PerformDeviceScan() + */ + bool PerformDeviceScan(PeripheralScanResults &results) override; + + protected: + static PeripheralType GetType(int iDeviceClass); + + void Process(void) override; + bool WaitForUpdate(void); + + struct udev * m_udev; + struct udev_monitor *m_udevMon; + }; +} diff --git a/xbmc/platform/linux/powermanagement/CMakeLists.txt b/xbmc/platform/linux/powermanagement/CMakeLists.txt new file mode 100644 index 0000000..3694a79 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES LinuxPowerSyscall.cpp) + +set(HEADERS FallbackPowerSyscall.h + LinuxPowerSyscall.h) + +if(DBUS_FOUND) + list(APPEND SOURCES ConsoleUPowerSyscall.cpp + LogindUPowerSyscall.cpp + UPowerSyscall.cpp) + list(APPEND HEADERS ConsoleUPowerSyscall.h + LogindUPowerSyscall.h + UPowerSyscall.h) +endif() + +if(SOURCES) + core_add_library(platform_linux_powermanagement) +endif() diff --git a/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp new file mode 100644 index 0000000..6f04dd7 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ConsoleUPowerSyscall.h" + +#include "utils/log.h" + +CConsoleUPowerSyscall::CConsoleUPowerSyscall() +{ + m_CanPowerdown = ConsoleKitMethodCall("CanStop"); + m_CanReboot = ConsoleKitMethodCall("CanRestart"); +} + +bool CConsoleUPowerSyscall::Powerdown() +{ + CDBusMessage message("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "Stop"); + return message.SendSystem() != NULL; +} + +bool CConsoleUPowerSyscall::Reboot() +{ + CDBusMessage message("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "Restart"); + return message.SendSystem() != NULL; +} + +bool CConsoleUPowerSyscall::HasConsoleKitAndUPower() +{ + return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, "org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "CanStop") + && HasUPower(); +} + +bool CConsoleUPowerSyscall::ConsoleKitMethodCall(const char *method) +{ + CDBusMessage message("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", method); + DBusMessage *reply = message.SendSystem(); + if (reply) + { + dbus_bool_t boolean = FALSE; + + if (dbus_message_get_args (reply, NULL, DBUS_TYPE_BOOLEAN, &boolean, DBUS_TYPE_INVALID)) + return boolean; + } + + return false; +} diff --git a/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h new file mode 100644 index 0000000..9fdd157 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DBusUtil.h" +#include "UPowerSyscall.h" + +class CConsoleUPowerSyscall : public CUPowerSyscall +{ +public: + CConsoleUPowerSyscall(); + bool Powerdown() override; + bool Reboot() override; + static bool HasConsoleKitAndUPower(); +private: + static bool ConsoleKitMethodCall(const char *method); +}; diff --git a/xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h b/xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h new file mode 100644 index 0000000..6988543 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "powermanagement/IPowerSyscall.h" + +class CFallbackPowerSyscall : public CPowerSyscallWithoutEvents +{ +public: + bool Powerdown() override {return true; } + bool Suspend() override {return false; } + bool Hibernate() override {return false; } + bool Reboot() override {return true; } + + bool CanPowerdown() override {return true; } + bool CanSuspend() override {return false; } + bool CanHibernate() override {return false; } + bool CanReboot() override {return true; } + int BatteryLevel() override {return 0; } +}; diff --git a/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp new file mode 100644 index 0000000..f3dbcdb --- /dev/null +++ b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LinuxPowerSyscall.h" +#include "FallbackPowerSyscall.h" +#if defined(HAS_DBUS) +#include "ConsoleUPowerSyscall.h" +#include "LogindUPowerSyscall.h" +#include "UPowerSyscall.h" +#endif // HAS_DBUS + +#include <functional> +#include <list> +#include <memory> +#include <utility> + +IPowerSyscall* CLinuxPowerSyscall::CreateInstance() +{ +#if defined(HAS_DBUS) + std::unique_ptr<IPowerSyscall> bestPowerManager; + std::unique_ptr<IPowerSyscall> currPowerManager; + int bestCount = -1; + int currCount = -1; + + std::list< std::pair< std::function<bool()>, + std::function<IPowerSyscall*()> > > powerManagers = + { + std::make_pair(CConsoleUPowerSyscall::HasConsoleKitAndUPower, + [] { return new CConsoleUPowerSyscall(); }), + std::make_pair(CLogindUPowerSyscall::HasLogind, + [] { return new CLogindUPowerSyscall(); }), + std::make_pair(CUPowerSyscall::HasUPower, + [] { return new CUPowerSyscall(); }) + }; + for(const auto& powerManager : powerManagers) + { + if (powerManager.first()) + { + currPowerManager.reset(powerManager.second()); + currCount = currPowerManager->CountPowerFeatures(); + if (currCount > bestCount) + { + bestCount = currCount; + bestPowerManager = std::move(currPowerManager); + } + if (bestCount == IPowerSyscall::MAX_COUNT_POWER_FEATURES) + break; + } + } + if (bestPowerManager) + return bestPowerManager.release(); + else +#endif // HAS_DBUS + return new CFallbackPowerSyscall(); +} + +void CLinuxPowerSyscall::Register() +{ + IPowerSyscall::RegisterPowerSyscall(CLinuxPowerSyscall::CreateInstance); +} diff --git a/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h new file mode 100644 index 0000000..b5cc218 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "powermanagement/IPowerSyscall.h" + +class CLinuxPowerSyscall +{ +public: + static IPowerSyscall* CreateInstance(); + static void Register(); +}; diff --git a/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp new file mode 100644 index 0000000..bd04197 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2012 Denis Yantarev + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LogindUPowerSyscall.h" + +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <string.h> + +#include <unistd.h> + +// logind DBus interface specification: +// http://www.freedesktop.org/wiki/Software/Logind/logind +// +// Inhibitor Locks documentation: +// http://www.freedesktop.org/wiki/Software/Logind/inhibit/ + +#define LOGIND_DEST "org.freedesktop.login1" +#define LOGIND_PATH "/org/freedesktop/login1" +#define LOGIND_IFACE "org.freedesktop.login1.Manager" + +CLogindUPowerSyscall::CLogindUPowerSyscall() +{ + m_lowBattery = false; + + CLog::Log(LOGINFO, "Selected Logind/UPower as PowerSyscall"); + + // Check if we have UPower. If not, we avoid any battery related operations. + CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices"); + m_hasUPower = message.SendSystem() != NULL; + + if (!m_hasUPower) + CLog::Log(LOGINFO, "LogindUPowerSyscall - UPower not found, battery information will not be available"); + + m_canPowerdown = LogindCheckCapability("CanPowerOff"); + m_canReboot = LogindCheckCapability("CanReboot"); + m_canHibernate = LogindCheckCapability("CanHibernate"); + m_canSuspend = LogindCheckCapability("CanSuspend"); + + InhibitDelayLockSleep(); + + m_batteryLevel = 0; + if (m_hasUPower) + UpdateBatteryLevel(); + + if (!m_connection.Connect(DBUS_BUS_SYSTEM, true)) + { + return; + } + + CDBusError error; + dbus_connection_set_exit_on_disconnect(m_connection, false); + dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'", error); + + if (!error && m_hasUPower) + dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.UPower',member='DeviceChanged'", error); + + dbus_connection_flush(m_connection); + + if (error) + { + error.Log("UPowerSyscall: Failed to attach to signal"); + m_connection.Destroy(); + } +} + +CLogindUPowerSyscall::~CLogindUPowerSyscall() +{ + ReleaseDelayLockSleep(); + ReleaseDelayLockShutdown(); +} + +bool CLogindUPowerSyscall::Powerdown() +{ + // delay shutdown so that the app can close properly + InhibitDelayLockShutdown(); + return LogindSetPowerState("PowerOff"); +} + +bool CLogindUPowerSyscall::Reboot() +{ + return LogindSetPowerState("Reboot"); +} + +bool CLogindUPowerSyscall::Suspend() +{ + return LogindSetPowerState("Suspend"); +} + +bool CLogindUPowerSyscall::Hibernate() +{ + return LogindSetPowerState("Hibernate"); +} + +bool CLogindUPowerSyscall::CanPowerdown() +{ + return m_canPowerdown; +} + +bool CLogindUPowerSyscall::CanSuspend() +{ + return m_canSuspend; +} + +bool CLogindUPowerSyscall::CanHibernate() +{ + return m_canHibernate; +} + +bool CLogindUPowerSyscall::CanReboot() +{ + return m_canReboot; +} + +bool CLogindUPowerSyscall::HasLogind() +{ + // recommended method by systemd devs. The seats directory + // doesn't exist unless logind created it and therefore is running. + // see also https://mail.gnome.org/archives/desktop-devel-list/2013-March/msg00092.html + if (access("/run/systemd/seats/", F_OK) >= 0) + return true; + + // on some environments "/run/systemd/seats/" doesn't exist, e.g. on flatpak. Try DBUS instead. + CDBusMessage message(LOGIND_DEST, LOGIND_PATH, LOGIND_IFACE, "ListSeats"); + DBusMessage *reply = message.SendSystem(); + if (!reply) + return false; + + DBusMessageIter arrIter; + if (dbus_message_iter_init(reply, &arrIter) && dbus_message_iter_get_arg_type(&arrIter) == DBUS_TYPE_ARRAY) + { + DBusMessageIter structIter; + dbus_message_iter_recurse(&arrIter, &structIter); + if (dbus_message_iter_get_arg_type(&structIter) == DBUS_TYPE_STRUCT) + { + DBusMessageIter strIter; + dbus_message_iter_recurse(&structIter, &strIter); + if (dbus_message_iter_get_arg_type(&strIter) == DBUS_TYPE_STRING) + { + char *seat; + dbus_message_iter_get_basic(&strIter, &seat); + if (StringUtils::StartsWith(seat, "seat")) + { + CLog::Log(LOGDEBUG, "LogindUPowerSyscall::HasLogind - found seat: {}", seat); + return true; + } + } + } + } + return false; +} + +bool CLogindUPowerSyscall::LogindSetPowerState(const char *state) +{ + CDBusMessage message(LOGIND_DEST, LOGIND_PATH, LOGIND_IFACE, state); + // The user_interaction boolean parameters can be used to control + // whether PolicyKit should interactively ask the user for authentication + // credentials if it needs to. + message.AppendArgument(false); + return message.SendSystem() != NULL; +} + +bool CLogindUPowerSyscall::LogindCheckCapability(const char *capability) +{ + char *arg; + CDBusMessage message(LOGIND_DEST, LOGIND_PATH, LOGIND_IFACE, capability); + DBusMessage *reply = message.SendSystem(); + if(reply && dbus_message_get_args(reply, NULL, DBUS_TYPE_STRING, &arg, DBUS_TYPE_INVALID)) + { + // Returns one of "yes", "no" or "challenge". If "challenge" is + // returned the operation is available, but only after authorization. + return (strcmp(arg, "yes") == 0); + } + return false; +} + +int CLogindUPowerSyscall::BatteryLevel() +{ + return m_batteryLevel; +} + +void CLogindUPowerSyscall::UpdateBatteryLevel() +{ + char** source = NULL; + int length = 0; + double batteryLevelSum = 0; + int batteryCount = 0; + + CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices"); + DBusMessage *reply = message.SendSystem(); + + if (!reply) + return; + + if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &source, &length, DBUS_TYPE_INVALID)) + { + CLog::Log(LOGWARNING, "LogindUPowerSyscall: failed to enumerate devices"); + return; + } + + for (int i = 0; i < length; i++) + { + CVariant properties = CDBusUtil::GetAll("org.freedesktop.UPower", source[i], "org.freedesktop.UPower.Device"); + bool isRechargeable = properties["IsRechargeable"].asBoolean(); + + if (isRechargeable) + { + batteryCount++; + batteryLevelSum += properties["Percentage"].asDouble(); + } + } + + dbus_free_string_array(source); + + if (batteryCount > 0) + { + m_batteryLevel = (int)(batteryLevelSum / (double)batteryCount); + m_lowBattery = CDBusUtil::GetVariant("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "OnLowBattery").asBoolean(); + } +} + +bool CLogindUPowerSyscall::PumpPowerEvents(IPowerEventsCallback *callback) +{ + bool result = false; + bool releaseLockSleep = false; + + if (m_connection) + { + dbus_connection_read_write(m_connection, 0); + DBusMessagePtr msg(dbus_connection_pop_message(m_connection)); + + if (msg) + { + if (dbus_message_is_signal(msg.get(), "org.freedesktop.login1.Manager", "PrepareForSleep")) + { + dbus_bool_t arg; + // the boolean argument defines whether we are going to sleep (true) or just woke up (false) + dbus_message_get_args(msg.get(), NULL, DBUS_TYPE_BOOLEAN, &arg, DBUS_TYPE_INVALID); + CLog::Log(LOGDEBUG, "LogindUPowerSyscall: Received PrepareForSleep with arg {}", (int)arg); + if (arg) + { + callback->OnSleep(); + releaseLockSleep = true; + } + else + { + callback->OnWake(); + InhibitDelayLockSleep(); + } + + result = true; + } + else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "DeviceChanged")) + { + bool lowBattery = m_lowBattery; + UpdateBatteryLevel(); + if (m_lowBattery && !lowBattery) + callback->OnLowBattery(); + + result = true; + } + else + CLog::Log(LOGDEBUG, "LogindUPowerSyscall - Received unknown signal {}", + dbus_message_get_member(msg.get())); + } + } + + if (releaseLockSleep) + ReleaseDelayLockSleep(); + + return result; +} + +void CLogindUPowerSyscall::InhibitDelayLockSleep() +{ + m_delayLockSleepFd = InhibitDelayLock("sleep"); +} + +void CLogindUPowerSyscall::InhibitDelayLockShutdown() +{ + m_delayLockShutdownFd = InhibitDelayLock("shutdown"); +} + +int CLogindUPowerSyscall::InhibitDelayLock(const char *what) +{ +#ifdef DBUS_TYPE_UNIX_FD + CDBusMessage message("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "Inhibit"); + message.AppendArgument(what); // what to inhibit + message.AppendArgument("XBMC"); // who + message.AppendArgument(""); // reason + message.AppendArgument("delay"); // mode + + DBusMessage *reply = message.SendSystem(); + + if (!reply) + { + CLog::Log(LOGWARNING, "LogindUPowerSyscall - failed to inhibit {} delay lock", what); + return -1; + } + + int delayLockFd; + if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_UNIX_FD, &delayLockFd, DBUS_TYPE_INVALID)) + { + CLog::Log(LOGWARNING, "LogindUPowerSyscall - failed to get inhibit file descriptor"); + return -1; + } + + CLog::Log(LOGDEBUG, "LogindUPowerSyscall - inhibit lock taken, fd {}", delayLockFd); + return delayLockFd; +#else + CLog::Log(LOGWARNING, "LogindUPowerSyscall - inhibit lock support not compiled in"); + return -1; +#endif +} + +void CLogindUPowerSyscall::ReleaseDelayLockSleep() +{ + ReleaseDelayLock(m_delayLockSleepFd, "sleep"); + m_delayLockSleepFd = -1; +} + +void CLogindUPowerSyscall::ReleaseDelayLockShutdown() +{ + ReleaseDelayLock(m_delayLockShutdownFd, "shutdown"); + m_delayLockShutdownFd = -1; +} + +void CLogindUPowerSyscall::ReleaseDelayLock(int lockFd, const char *what) +{ + if (lockFd != -1) + { + close(lockFd); + CLog::Log(LOGDEBUG, "LogindUPowerSyscall - delay lock {} released", what); + } +} diff --git a/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h new file mode 100644 index 0000000..998e706 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 Denis Yantarev + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DBusUtil.h" +#include "powermanagement/IPowerSyscall.h" + +class CLogindUPowerSyscall : public CAbstractPowerSyscall +{ +public: + CLogindUPowerSyscall(); + ~CLogindUPowerSyscall() override; + bool Powerdown() override; + bool Suspend() override; + bool Hibernate() override; + bool Reboot() override; + bool CanPowerdown() override; + bool CanSuspend() override; + bool CanHibernate() override; + bool CanReboot() override; + int BatteryLevel() override; + bool PumpPowerEvents(IPowerEventsCallback *callback) override; + // we don't require UPower because everything except the battery level works fine without it + static bool HasLogind(); +private: + CDBusConnection m_connection; + bool m_canPowerdown; + bool m_canSuspend; + bool m_canHibernate; + bool m_canReboot; + bool m_hasUPower; + bool m_lowBattery; + int m_batteryLevel; + int m_delayLockSleepFd = -1; // file descriptor for the logind sleep delay lock + int m_delayLockShutdownFd = -1; // file descriptor for the logind powerdown delay lock + void UpdateBatteryLevel(); + void InhibitDelayLockSleep(); + void InhibitDelayLockShutdown(); + int InhibitDelayLock(const char *what); + void ReleaseDelayLockSleep(); + void ReleaseDelayLockShutdown(); + void ReleaseDelayLock(int lockFd, const char *what); + static bool LogindSetPowerState(const char *state); + static bool LogindCheckCapability(const char *capability); +}; diff --git a/xbmc/platform/linux/powermanagement/UPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/UPowerSyscall.cpp new file mode 100644 index 0000000..27ed2f4 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/UPowerSyscall.cpp @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "UPowerSyscall.h" + +#include "utils/log.h" + +CUPowerSource::CUPowerSource(const char *powerSource) +{ + if(powerSource == NULL) + m_powerSource = ""; + else + m_powerSource = powerSource; + + CVariant properties = CDBusUtil::GetAll("org.freedesktop.UPower", m_powerSource.c_str(), "org.freedesktop.UPower.Device"); + m_isRechargeable = properties["IsRechargeable"].asBoolean(); + Update(); +} + +CUPowerSource::~CUPowerSource() = default; + +void CUPowerSource::Update() +{ + CVariant properties = CDBusUtil::GetAll("org.freedesktop.UPower", m_powerSource.c_str(), "org.freedesktop.UPower.Device"); + m_batteryLevel = properties["Percentage"].asDouble(); +} + +bool CUPowerSource::IsRechargeable() +{ + return m_isRechargeable; +} + +double CUPowerSource::BatteryLevel() +{ + return m_batteryLevel; +} + +CUPowerSyscall::CUPowerSyscall() +{ + CLog::Log(LOGINFO, "Selected UPower as PowerSyscall"); + + m_lowBattery = false; + + //! @todo do not use dbus_connection_pop_message() that requires the use of a + //! private connection + if (m_connection.Connect(DBUS_BUS_SYSTEM, true)) + { + dbus_connection_set_exit_on_disconnect(m_connection, false); + + CDBusError error; + dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.UPower'", error); + dbus_connection_flush(m_connection); + + if (error) + { + error.Log("UPower: Failed to attach to signal"); + m_connection.Destroy(); + } + } + + m_CanPowerdown = false; + m_CanReboot = false; + + UpdateCapabilities(); + + EnumeratePowerSources(); +} + +bool CUPowerSyscall::Powerdown() +{ + return false; +} + +bool CUPowerSyscall::Suspend() +{ + // UPower 0.9.1 does not signal sleeping unless you tell that its about to sleep... + CDBusMessage aboutToSleepMessage("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "AboutToSleep"); + aboutToSleepMessage.SendAsyncSystem(); + + CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "Suspend"); + return message.SendAsyncSystem(); +} + +bool CUPowerSyscall::Hibernate() +{ + // UPower 0.9.1 does not signal sleeping unless you tell that its about to sleep... + CDBusMessage aboutToSleepMessage("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "AboutToSleep"); + aboutToSleepMessage.SendAsyncSystem(); + + CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "Hibernate"); + return message.SendAsyncSystem(); +} + +bool CUPowerSyscall::Reboot() +{ + return false; +} + +bool CUPowerSyscall::CanPowerdown() +{ + return m_CanPowerdown; +} + +bool CUPowerSyscall::CanSuspend() +{ + return m_CanSuspend; +} + +bool CUPowerSyscall::CanHibernate() +{ + return m_CanHibernate; +} + +bool CUPowerSyscall::CanReboot() +{ + return m_CanReboot; +} + +int CUPowerSyscall::BatteryLevel() +{ + unsigned int nBatteryCount = 0; + double subCapacity = 0; + double batteryLevel = 0; + + for (auto& itr : m_powerSources) + { + itr.Update(); + if (itr.IsRechargeable()) + { + nBatteryCount++; + subCapacity += itr.BatteryLevel(); + } + } + + if(nBatteryCount) + batteryLevel = subCapacity / (double)nBatteryCount; + + return (int) batteryLevel; +} + +void CUPowerSyscall::EnumeratePowerSources() +{ + CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices"); + DBusMessage *reply = message.SendSystem(); + if (reply) + { + char** source = NULL; + int length = 0; + + if (dbus_message_get_args (reply, NULL, DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &source, &length, DBUS_TYPE_INVALID)) + { + for (int i = 0; i < length; i++) + { + m_powerSources.emplace_back(source[i]); + } + + dbus_free_string_array(source); + } + } +} + +bool CUPowerSyscall::HasUPower() +{ + return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, "org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices"); +} + +bool CUPowerSyscall::PumpPowerEvents(IPowerEventsCallback *callback) +{ + bool result = false; + + if (m_connection) + { + dbus_connection_read_write(m_connection, 0); + DBusMessagePtr msg(dbus_connection_pop_message(m_connection)); + + if (msg) + { + result = true; + if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "Sleeping")) + callback->OnSleep(); + else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "Resuming")) + callback->OnWake(); + else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "Changed")) + { + bool lowBattery = m_lowBattery; + UpdateCapabilities(); + if (m_lowBattery && !lowBattery) + callback->OnLowBattery(); + } + else + CLog::Log(LOGDEBUG, "UPower: Received an unknown signal {}", + dbus_message_get_member(msg.get())); + } + } + return result; +} + +void CUPowerSyscall::UpdateCapabilities() +{ + m_CanSuspend = CDBusUtil::GetVariant("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "CanSuspend").asBoolean(false); + m_CanHibernate = CDBusUtil::GetVariant("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "CanHibernate").asBoolean(false); +} diff --git a/xbmc/platform/linux/powermanagement/UPowerSyscall.h b/xbmc/platform/linux/powermanagement/UPowerSyscall.h new file mode 100644 index 0000000..aed08d8 --- /dev/null +++ b/xbmc/platform/linux/powermanagement/UPowerSyscall.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DBusUtil.h" +#include "powermanagement/IPowerSyscall.h" + +#include <list> +#include <string> + +class CUPowerSource +{ +public: + CUPowerSource(const char *powerSource); + ~CUPowerSource(); + + void Update(); + bool IsRechargeable(); + double BatteryLevel(); + +private: + std::string m_powerSource; + bool m_isRechargeable; + double m_batteryLevel; +}; + +class CUPowerSyscall : public CAbstractPowerSyscall +{ +public: + CUPowerSyscall(); + bool Powerdown() override; + bool Suspend() override; + bool Hibernate() override; + bool Reboot() override; + bool CanPowerdown() override; + bool CanSuspend() override; + bool CanHibernate() override; + bool CanReboot() override; + int BatteryLevel() override; + bool PumpPowerEvents(IPowerEventsCallback *callback) override; + static bool HasUPower(); +protected: + bool m_CanPowerdown; + bool m_CanSuspend; + bool m_CanHibernate; + bool m_CanReboot; + + void UpdateCapabilities(); +private: + std::list<CUPowerSource> m_powerSources; + CDBusConnection m_connection; + + bool m_lowBattery; + void EnumeratePowerSources(); +}; diff --git a/xbmc/platform/linux/sse4/CMakeLists.txt b/xbmc/platform/linux/sse4/CMakeLists.txt new file mode 100644 index 0000000..6ec73f1 --- /dev/null +++ b/xbmc/platform/linux/sse4/CMakeLists.txt @@ -0,0 +1,8 @@ +if(HAVE_SSE4_1) + set(SOURCES CopyFrame.cpp) + set(HEADERS DllLibSSE4.h) + + core_add_shared_library(sse4) + set_target_properties(sse4 PROPERTIES FOLDER lib) + target_compile_options(sse4 PRIVATE -msse4.1) +endif() diff --git a/xbmc/platform/linux/sse4/CopyFrame.cpp b/xbmc/platform/linux/sse4/CopyFrame.cpp new file mode 100644 index 0000000..2147c3c --- /dev/null +++ b/xbmc/platform/linux/sse4/CopyFrame.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * See LICENSES/README.md for more information. + */ + +#include <smmintrin.h> + +#define CACHED_BUFFER_SIZE 4096 + +extern "C" +{ + +/* + * http://software.intel.com/en-us/articles/copying-accelerated-video-decode-frame-buffers + * COPIES VIDEO FRAMES FROM USWC MEMORY TO WB SYSTEM MEMORY VIA CACHED BUFFER + * ASSUMES PITCH IS A MULTIPLE OF 64B CACHE LINE SIZE, WIDTH MAY NOT BE + */ +void copy_frame( void * pSrc, void * pDest, void * pCacheBlock, + unsigned int width, unsigned int height, unsigned int pitch ) +{ + __m128i x0, x1, x2, x3; + __m128i *pLoad; + __m128i *pStore; + __m128i *pCache; + unsigned int x, y, yLoad, yStore; + unsigned int rowsPerBlock; + unsigned int width64; + unsigned int extraPitch; + + + rowsPerBlock = CACHED_BUFFER_SIZE / pitch; + width64 = (width + 63) & ~0x03f; + extraPitch = (pitch - width64) / 16; + + pLoad = (__m128i *)pSrc; + pStore = (__m128i *)pDest; + + // COPY THROUGH 4KB CACHED BUFFER + for( y = 0; y < height; y += rowsPerBlock ) + { + // ROWS LEFT TO COPY AT END + if( y + rowsPerBlock > height ) + rowsPerBlock = height - y; + + pCache = (__m128i *)pCacheBlock; + + _mm_mfence(); + + // LOAD ROWS OF PITCH WIDTH INTO CACHED BLOCK + for( yLoad = 0; yLoad < rowsPerBlock; yLoad++ ) + { + // COPY A ROW, CACHE LINE AT A TIME + for( x = 0; x < pitch; x +=64 ) + { + x0 = _mm_stream_load_si128( pLoad +0 ); + x1 = _mm_stream_load_si128( pLoad +1 ); + x2 = _mm_stream_load_si128( pLoad +2 ); + x3 = _mm_stream_load_si128( pLoad +3 ); + + _mm_store_si128( pCache +0, x0 ); + _mm_store_si128( pCache +1, x1 ); + _mm_store_si128( pCache +2, x2 ); + _mm_store_si128( pCache +3, x3 ); + + pCache += 4; + pLoad += 4; + } + } + + _mm_mfence(); + + pCache = (__m128i *)pCacheBlock; + + // STORE ROWS OF FRAME WIDTH FROM CACHED BLOCK + for( yStore = 0; yStore < rowsPerBlock; yStore++ ) + { + // copy a row, cache line at a time + for( x = 0; x < width64; x +=64 ) + { + x0 = _mm_load_si128( pCache ); + x1 = _mm_load_si128( pCache +1 ); + x2 = _mm_load_si128( pCache +2 ); + x3 = _mm_load_si128( pCache +3 ); + + _mm_stream_si128( pStore, x0 ); + _mm_stream_si128( pStore +1, x1 ); + _mm_stream_si128( pStore +2, x2 ); + _mm_stream_si128( pStore +3, x3 ); + + pCache += 4; + pStore += 4; + } + + pCache += extraPitch; + pStore += extraPitch; + } + } +} +} diff --git a/xbmc/platform/linux/sse4/DllLibSSE4.h b/xbmc/platform/linux/sse4/DllLibSSE4.h new file mode 100644 index 0000000..a89f713 --- /dev/null +++ b/xbmc/platform/linux/sse4/DllLibSSE4.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DynamicDll.h" + +extern "C" { + +} + +class DllLibSSE4Interface +{ +public: + virtual ~DllLibSSE4Interface() = default; + virtual void copy_frame(void * pSrc, void * pDest, void * pCacheBlock, unsigned int width, unsigned int height, unsigned int pitch) = 0; +}; + +class DllLibSSE4 : public DllDynamic, DllLibSSE4Interface +{ + DECLARE_DLL_WRAPPER(DllLibSSE4, DLL_PATH_LIBSSE4) + DEFINE_METHOD6(void, copy_frame, (void *p1, void *p2, void *p3, unsigned int p4, unsigned int p5, unsigned int p6)) + + BEGIN_METHOD_RESOLVE() + RESOLVE_METHOD(copy_frame) + END_METHOD_RESOLVE() +}; diff --git a/xbmc/platform/linux/storage/CMakeLists.txt b/xbmc/platform/linux/storage/CMakeLists.txt new file mode 100644 index 0000000..223655a --- /dev/null +++ b/xbmc/platform/linux/storage/CMakeLists.txt @@ -0,0 +1,19 @@ +set(SOURCES LinuxStorageProvider.cpp) + +set(HEADERS LinuxStorageProvider.h) + +if(DBUS_FOUND) + list(APPEND SOURCES UDisksProvider.cpp + UDisks2Provider.cpp) + list(APPEND HEADERS UDisksProvider.h + UDisks2Provider.h) +endif() + +if(UDEV_FOUND) + list(APPEND SOURCES UDevProvider.cpp) + list(APPEND HEADERS UDevProvider.h) +endif() + +if(SOURCES) + core_add_library(platform_linux_storage) +endif() diff --git a/xbmc/platform/linux/storage/LinuxStorageProvider.cpp b/xbmc/platform/linux/storage/LinuxStorageProvider.cpp new file mode 100644 index 0000000..6206466 --- /dev/null +++ b/xbmc/platform/linux/storage/LinuxStorageProvider.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LinuxStorageProvider.h" +#include "guilib/LocalizeStrings.h" +#include "UDevProvider.h" +#ifdef HAS_DBUS +#include "UDisksProvider.h" +#include "UDisks2Provider.h" +#endif +#include "platform/posix/PosixMountProvider.h" + +std::unique_ptr<IStorageProvider> IStorageProvider::CreateInstance() +{ + return std::make_unique<CLinuxStorageProvider>(); +} + +CLinuxStorageProvider::CLinuxStorageProvider() +{ + m_instance = NULL; + +#ifdef HAS_DBUS + if (CUDisks2Provider::HasUDisks2()) + m_instance = new CUDisks2Provider(); + else if (CUDisksProvider::HasUDisks()) + m_instance = new CUDisksProvider(); +#endif +#ifdef HAVE_LIBUDEV + if (m_instance == NULL) + m_instance = new CUDevProvider(); +#endif + + if (m_instance == NULL) + m_instance = new CPosixMountProvider(); +} + +CLinuxStorageProvider::~CLinuxStorageProvider() +{ + delete m_instance; +} + +void CLinuxStorageProvider::Initialize() +{ + m_instance->Initialize(); +} + +void CLinuxStorageProvider::Stop() +{ + m_instance->Stop(); +} + +void CLinuxStorageProvider::GetLocalDrives(VECSOURCES &localDrives) +{ + // Home directory + CMediaSource share; + share.strPath = getenv("HOME"); + share.strName = g_localizeStrings.Get(21440); + share.m_ignore = true; + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + localDrives.push_back(share); + share.strPath = "/"; + share.strName = g_localizeStrings.Get(21453); + localDrives.push_back(share); + + m_instance->GetLocalDrives(localDrives); +} + +void CLinuxStorageProvider::GetRemovableDrives(VECSOURCES &removableDrives) +{ + m_instance->GetRemovableDrives(removableDrives); +} + +bool CLinuxStorageProvider::Eject(const std::string& mountpath) +{ + return m_instance->Eject(mountpath); +} + +std::vector<std::string> CLinuxStorageProvider::GetDiskUsage() +{ + return m_instance->GetDiskUsage(); +} + +bool CLinuxStorageProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback) +{ + return m_instance->PumpDriveChangeEvents(callback); +} diff --git a/xbmc/platform/linux/storage/LinuxStorageProvider.h b/xbmc/platform/linux/storage/LinuxStorageProvider.h new file mode 100644 index 0000000..e1c575f --- /dev/null +++ b/xbmc/platform/linux/storage/LinuxStorageProvider.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "storage/IStorageProvider.h" + +#include <vector> + +class CLinuxStorageProvider : public IStorageProvider +{ +public: + CLinuxStorageProvider(); + ~CLinuxStorageProvider() override; + + void Initialize() override; + void Stop() override; + void GetLocalDrives(VECSOURCES &localDrives) override; + void GetRemovableDrives(VECSOURCES &removableDrives) override; + bool Eject(const std::string& mountpath) override; + std::vector<std::string> GetDiskUsage() override; + bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override; + +private: + IStorageProvider *m_instance; +}; diff --git a/xbmc/platform/linux/storage/UDevProvider.cpp b/xbmc/platform/linux/storage/UDevProvider.cpp new file mode 100644 index 0000000..7123e87 --- /dev/null +++ b/xbmc/platform/linux/storage/UDevProvider.cpp @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "UDevProvider.h" + +#include "platform/posix/PosixMountProvider.h" +#include "utils/log.h" +#include "utils/URIUtils.h" + +extern "C" { +#include <libudev.h> +#include <poll.h> +} + +static const char *get_mountpoint(const char *devnode) +{ + static char buf[4096]; + const char *delim = " "; + const char *mountpoint = nullptr; + FILE *fp = fopen("/proc/mounts", "r"); + if (!fp) + return nullptr; + + while (fgets(buf, sizeof (buf), fp)) + { + const char *node = strtok(buf, delim); + if (strcmp(node, devnode) == 0) + { + mountpoint = strtok(nullptr, delim); + break; + } + } + + if (mountpoint) + { + // If mount point contain characters like space, it is converted to + // "\040". This situation should be handled. + char *c1, *c2; + for (c1 = c2 = const_cast<char*>(mountpoint); *c2; ++c1) + { + if (*c2 == '\\') + { + *c1 = (((c2[1] - '0') << 6) | ((c2[2] - '0') << 3) | (c2[3] - '0')); + c2 += 4; + continue; + } + if (c1 != c2) + *c1 = *c2; + ++c2; + } + *c1 = *c2; + } + + fclose(fp); + return mountpoint; +} + +CUDevProvider::CUDevProvider() +{ + m_udev = nullptr; + m_udevMon = nullptr; +} + +void CUDevProvider::Initialize() +{ + CLog::Log(LOGDEBUG, "Selected UDev as storage provider"); + + m_udev = udev_new(); + if (!m_udev) + { + CLog::Log(LOGERROR, "{} - failed to allocate udev context", __FUNCTION__); + return; + } + /* set up a devices monitor that listen for any device change */ + m_udevMon = udev_monitor_new_from_netlink(m_udev, "udev"); + udev_monitor_filter_add_match_subsystem_devtype(m_udevMon, "block", "disk"); + udev_monitor_filter_add_match_subsystem_devtype(m_udevMon, "block", "partition"); + udev_monitor_enable_receiving(m_udevMon); + + PumpDriveChangeEvents(nullptr); +} + +void CUDevProvider::Stop() +{ + udev_monitor_unref(m_udevMon); + udev_unref(m_udev); +} + +void CUDevProvider::GetDisks(VECSOURCES& disks, bool removable) +{ + // enumerate existing block devices + struct udev_enumerate *u_enum = udev_enumerate_new(m_udev); + if (!u_enum) + { + fprintf(stderr, "Error: udev_enumerate_new(udev)\n"); + return; + } + + udev_enumerate_add_match_subsystem(u_enum, "block"); + udev_enumerate_add_match_property(u_enum, "DEVTYPE", "disk"); + udev_enumerate_add_match_property(u_enum, "DEVTYPE", "partition"); + udev_enumerate_scan_devices(u_enum); + + struct udev_list_entry *u_list_ent; + struct udev_list_entry *u_first_list_ent; + u_first_list_ent = udev_enumerate_get_list_entry(u_enum); + udev_list_entry_foreach(u_list_ent, u_first_list_ent) + { + const char *name = udev_list_entry_get_name(u_list_ent); + struct udev *context = udev_enumerate_get_udev(u_enum); + struct udev_device *device = udev_device_new_from_syspath(context, name); + if (!device) + continue; + + // filter out devices without devnode + const char* devnode = udev_device_get_devnode(device); + if (!devnode) + { + udev_device_unref(device); + continue; + } + + // filter out devices that are not mounted + const char* mountpoint = get_mountpoint(devnode); + if (!mountpoint) + { + udev_device_unref(device); + continue; + } + + // filter out root partition + if (strcmp(mountpoint, "/") == 0) + { + udev_device_unref(device); + continue; + } + + // filter out things mounted on /tmp + if (strstr(mountpoint, "/tmp")) + { + udev_device_unref(device); + continue; + } + + // look for devices on the usb bus, or mounted on */media/ (sdcards), or optical devices + const char *bus = udev_device_get_property_value(device, "ID_BUS"); + const char *optical = udev_device_get_property_value(device, "ID_CDROM"); // matches also DVD, Blu-ray + bool isRemovable = ((bus && strstr(bus, "usb")) || + (optical && strstr(optical,"1")) || + (mountpoint && strstr(mountpoint, "/media/"))); + + // filter according to requested device type + if (removable != isRemovable) + { + udev_device_unref(device); + continue; + } + + const char *udev_label = udev_device_get_property_value(device, "ID_FS_LABEL"); + std::string label; + if (udev_label) + label = udev_label; + else + label = URIUtils::GetFileName(mountpoint); + + CMediaSource share; + share.strName = label; + share.strPath = mountpoint; + share.m_ignore = true; + if (isRemovable) + { + if (optical) + share.m_iDriveType = CMediaSource::SOURCE_TYPE_DVD; + else + share.m_iDriveType = CMediaSource::SOURCE_TYPE_REMOVABLE; + } + else + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + + disks.push_back(share); + udev_device_unref(device); + } + udev_enumerate_unref(u_enum); +} + +void CUDevProvider::GetLocalDrives(VECSOURCES &localDrives) +{ + GetDisks(localDrives, false); +} + +void CUDevProvider::GetRemovableDrives(VECSOURCES &removableDrives) +{ + GetDisks(removableDrives, true); +} + +bool CUDevProvider::Eject(const std::string& mountpath) +{ + // just go ahead and try to umount the disk + // if it does umount, life is good, if not, no loss. + std::string cmd = "umount \"" + mountpath + "\""; + int status = system(cmd.c_str()); + + if (status == 0) + return true; + + return false; +} + +std::vector<std::string> CUDevProvider::GetDiskUsage() +{ + CPosixMountProvider legacy; + return legacy.GetDiskUsage(); +} + +bool CUDevProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback) +{ + bool changed = false; + + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(udev_monitor_get_fd(m_udevMon), &readfds); + + // non-blocking, check the file descriptor for received data + struct timeval tv = {}; + int count = select(udev_monitor_get_fd(m_udevMon) + 1, &readfds, nullptr, nullptr, &tv); + if (count < 0) + return false; + + if (FD_ISSET(udev_monitor_get_fd(m_udevMon), &readfds)) + { + struct udev_device* dev = udev_monitor_receive_device(m_udevMon); + if (!dev) + return false; + + const char* action = udev_device_get_action(dev); + const char* devnode = udev_device_get_devnode(dev); + if (action && devnode) + { + MEDIA_DETECT::STORAGE::StorageDevice storageDevice; + const char *udev_label = udev_device_get_property_value(dev, "ID_FS_LABEL"); + const char* mountpoint = get_mountpoint(devnode); + if (udev_label) + storageDevice.label = udev_label; + else if (mountpoint) + { + storageDevice.label = URIUtils::GetFileName(mountpoint); + storageDevice.path = mountpoint; + } + + const char *fs_usage = udev_device_get_property_value(dev, "ID_FS_USAGE"); + if (mountpoint && strcmp(action, "add") == 0 && (fs_usage && strcmp(fs_usage, "filesystem") == 0)) + { + CLog::Log(LOGINFO, "UDev: Added {}", mountpoint); + if (callback) + callback->OnStorageAdded(storageDevice); + changed = true; + } + if (strcmp(action, "remove") == 0 && (fs_usage && strcmp(fs_usage, "filesystem") == 0)) + { + if (callback) + callback->OnStorageSafelyRemoved(storageDevice); + changed = true; + } + // browse disk dialog is not wanted for blu-rays + const char *bd = udev_device_get_property_value(dev, "ID_CDROM_MEDIA_BD"); + if (strcmp(action, "change") == 0 && !(bd && strcmp(bd, "1") == 0)) + { + const char *optical = udev_device_get_property_value(dev, "ID_CDROM"); + bool isOptical = optical && (strcmp(optical, "1") != 0); + storageDevice.type = + isOptical ? MEDIA_DETECT::STORAGE::Type::OPTICAL : MEDIA_DETECT::STORAGE::Type::UNKNOWN; + + if (mountpoint && !isOptical) + { + CLog::Log(LOGINFO, "UDev: Changed / Added {}", mountpoint); + if (callback) + callback->OnStorageAdded(storageDevice); + changed = true; + } + const char *eject_request = udev_device_get_property_value(dev, "DISK_EJECT_REQUEST"); + if (eject_request && strcmp(eject_request, "1") == 0) + { + if (callback) + callback->OnStorageSafelyRemoved(storageDevice); + changed = true; + } + } + } + udev_device_unref(dev); + } + + return changed; +} diff --git a/xbmc/platform/linux/storage/UDevProvider.h b/xbmc/platform/linux/storage/UDevProvider.h new file mode 100644 index 0000000..a4c83b3 --- /dev/null +++ b/xbmc/platform/linux/storage/UDevProvider.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "storage/IStorageProvider.h" + +#include <string> +#include <vector> + +struct udev; +struct udev_monitor; + +class CUDevProvider : public IStorageProvider +{ +public: + CUDevProvider(); + ~CUDevProvider() override = default; + + void Initialize() override; + void Stop() override; + + void GetLocalDrives(VECSOURCES &localDrives) override; + void GetRemovableDrives(VECSOURCES &removableDrives) override; + + bool Eject(const std::string& mountpath) override; + + std::vector<std::string> GetDiskUsage() override; + + bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override; + +private: + void GetDisks(VECSOURCES& devices, bool removable); + + struct udev *m_udev; + struct udev_monitor *m_udevMon; +}; diff --git a/xbmc/platform/linux/storage/UDisks2Provider.cpp b/xbmc/platform/linux/storage/UDisks2Provider.cpp new file mode 100644 index 0000000..b3431c7 --- /dev/null +++ b/xbmc/platform/linux/storage/UDisks2Provider.cpp @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "UDisks2Provider.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include "platform/posix/PosixMountProvider.h" + +#include <algorithm> +#include <functional> + +#define BOOL2SZ(b) ((b) ? "true" : "false") + +#define DBUS_INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager" + +#define UDISKS2_SERVICE_UDISKS2 "org.freedesktop.UDisks2" + +#define UDISKS2_PATH_MANAGER "/org/freedesktop/UDisks2/Manager" +#define UDISKS2_PATH_UDISKS2 "/org/freedesktop/UDisks2" + +#define UDISKS2_INTERFACE_BLOCK "org.freedesktop.UDisks2.Block" +#define UDISKS2_INTERFACE_DRIVE "org.freedesktop.UDisks2.Drive" +#define UDISKS2_INTERFACE_FILESYSTEM "org.freedesktop.UDisks2.Filesystem" +#define UDISKS2_INTERFACE_MANAGER "org.freedesktop.UDisks2.Manager" + +#define UDISKS2_MATCH_RULE "type='signal',sender='" UDISKS2_SERVICE_UDISKS2 "',path_namespace='" UDISKS2_PATH_UDISKS2 "'" + +CUDisks2Provider::Drive::Drive(const char *object) : m_object(object) +{ +} + +bool CUDisks2Provider::Drive::IsOptical() const +{ + return std::any_of(m_mediaCompatibility.begin(), m_mediaCompatibility.end(), + [](const std::string& kind) { return kind.compare(0, 7, "optical") == 0; }); +} + +std::string CUDisks2Provider::Drive::ToString() const +{ + return StringUtils::Format("Drive {}: IsRemovable {} IsOptical {}", m_object, + BOOL2SZ(m_isRemovable), BOOL2SZ(IsOptical())); +} + +CUDisks2Provider::Block::Block(const char *object) : m_object(object) +{ +} + +bool CUDisks2Provider::Block::IsReady() +{ + return m_drive != nullptr; +} + +std::string CUDisks2Provider::Block::ToString() const +{ + return StringUtils::Format("Block device {}: Device {} Label {} IsSystem: {} Drive {}", m_object, + m_device, m_label.empty() ? "none" : m_label, BOOL2SZ(m_isSystem), + m_driveobject.empty() ? "none" : m_driveobject); +} + +CUDisks2Provider::Filesystem::Filesystem(const char *object) : m_object(object) +{ +} + +std::string CUDisks2Provider::Filesystem::ToString() const +{ + return StringUtils::Format("Filesystem {}: IsMounted {} MountPoint {}", m_object, + BOOL2SZ(m_isMounted), m_mountPoint.empty() ? "none" : m_mountPoint); +} + +MEDIA_DETECT::STORAGE::StorageDevice CUDisks2Provider::Filesystem::ToStorageDevice() const +{ + MEDIA_DETECT::STORAGE::StorageDevice device; + device.label = GetDisplayName(); + device.path = GetMountPoint(); + device.type = GetStorageType(); + return device; +} + +bool CUDisks2Provider::Filesystem::IsReady() const +{ + return m_block != nullptr && m_block->IsReady(); +} + +bool CUDisks2Provider::Filesystem::IsOptical() const +{ + return m_block->m_drive->IsOptical(); +} + +MEDIA_DETECT::STORAGE::Type CUDisks2Provider::Filesystem::GetStorageType() const +{ + if (m_block == nullptr || !m_block->IsReady()) + return MEDIA_DETECT::STORAGE::Type::UNKNOWN; + + if (IsOptical()) + return MEDIA_DETECT::STORAGE::Type::OPTICAL; + + return MEDIA_DETECT::STORAGE::Type::UNKNOWN; +} + +std::string CUDisks2Provider::Filesystem::GetMountPoint() const +{ + return m_mountPoint; +} + +std::string CUDisks2Provider::Filesystem::GetObject() const +{ + return m_object; +} + +void CUDisks2Provider::Filesystem::ResetMountPoint() +{ + m_mountPoint.clear(); + m_isMounted = false; +} + +void CUDisks2Provider::Filesystem::SetMountPoint(const std::string& mountPoint) +{ + m_mountPoint = mountPoint; + m_isMounted = true; +} + +bool CUDisks2Provider::Filesystem::IsMounted() const +{ + return m_isMounted; +} + +bool CUDisks2Provider::Filesystem::Mount() +{ + if (m_block->m_isSystem) { + CLog::Log(LOGDEBUG, "UDisks2: Skip mount of system device {}", ToString()); + return false; + } + else if (m_isMounted) + { + CLog::Log(LOGDEBUG, "UDisks2: Skip mount of already mounted device {}", ToString()); + return false; + } + else + { + CLog::Log(LOGDEBUG, "UDisks2: Mounting {}", m_block->m_device); + CDBusMessage message(UDISKS2_SERVICE_UDISKS2, m_object, UDISKS2_INTERFACE_FILESYSTEM, "Mount"); + AppendEmptyOptions(message.GetArgumentIter()); + DBusMessage *reply = message.SendSystem(); + return (reply && dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR); + } +} + +bool CUDisks2Provider::Filesystem::Unmount() +{ + if (m_block->m_isSystem) { + CLog::Log(LOGDEBUG, "UDisks2: Skip unmount of system device {}", ToString()); + return false; + } + else if (!m_isMounted) + { + CLog::Log(LOGDEBUG, "UDisks2: Skip unmount of not mounted device {}", ToString()); + return false; + } + else + { + CLog::Log(LOGDEBUG, "UDisks2: Unmounting {}", m_block->m_device); + CDBusMessage message(UDISKS2_SERVICE_UDISKS2, m_object, UDISKS2_INTERFACE_FILESYSTEM, "Unmount"); + AppendEmptyOptions(message.GetArgumentIter()); + DBusMessage *reply = message.SendSystem(); + return (reply && dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR); + } +} + +std::string CUDisks2Provider::Filesystem::GetDisplayName() const +{ + if (m_block->m_label.empty()) + { + std::string strSize = StringUtils::SizeToString(m_block->m_size); + return StringUtils::Format("{} {}", strSize, g_localizeStrings.Get(155)); + } + else + return m_block->m_label; +} + +CMediaSource CUDisks2Provider::Filesystem::ToMediaShare() const +{ + CMediaSource source; + source.strPath = m_mountPoint; + source.strName = GetDisplayName(); + if (IsOptical()) + source.m_iDriveType = CMediaSource::SOURCE_TYPE_DVD; + else if (m_block->m_isSystem) + source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + else + source.m_iDriveType = CMediaSource::SOURCE_TYPE_REMOVABLE; + source.m_ignore = true; + return source; +} + +bool CUDisks2Provider::Filesystem::IsApproved() const +{ + return IsReady() && + (m_isMounted && m_mountPoint != "/" && m_mountPoint != "/boot" && m_mountPoint.compare(0, 6, "/boot/") != 0) /*|| + m_block->m_drive->m_isOptical*/; +} + +CUDisks2Provider::CUDisks2Provider() +{ + if (!m_connection.Connect(DBUS_BUS_SYSTEM, true)) + { + return; + } + + dbus_connection_set_exit_on_disconnect(m_connection, static_cast<dbus_bool_t>(false)); + + CDBusError error; + dbus_bus_add_match(m_connection, UDISKS2_MATCH_RULE, error); + dbus_connection_flush(m_connection); + + if (error) + { + error.Log("UDisks2: Failed to attach to signal"); + m_connection.Destroy(); + } +} + +CUDisks2Provider::~CUDisks2Provider() +{ + for (auto &elt : m_filesystems) + { + delete elt.second; + } + m_filesystems.clear(); + + for (auto &elt : m_blocks) + { + delete elt.second; + } + m_blocks.clear(); + + for (auto &elt : m_drives) + { + delete elt.second; + } + m_drives.clear(); +} + +void CUDisks2Provider::Initialize() +{ + CLog::Log(LOGDEBUG, "Selected UDisks2 as storage provider"); + m_daemonVersion = CDBusUtil::GetVariant(UDISKS2_SERVICE_UDISKS2, UDISKS2_PATH_MANAGER, UDISKS2_INTERFACE_MANAGER, + "Version").asString(); + CLog::Log(LOGDEBUG, "UDisks2: Daemon version {}", m_daemonVersion); + + CLog::Log(LOGDEBUG, "UDisks2: Querying available devices"); + CDBusMessage message(UDISKS2_SERVICE_UDISKS2, UDISKS2_PATH_UDISKS2, DBUS_INTERFACE_OBJECT_MANAGER, + "GetManagedObjects"); + DBusMessage *reply = message.SendSystem(); + + if (reply && dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR) + { + HandleManagedObjects(reply); + } +} + +bool CUDisks2Provider::PumpDriveChangeEvents(IStorageEventsCallback *callback) +{ + if (m_connection) + { + dbus_connection_read_write(m_connection, 0); + DBusMessagePtr msg(dbus_connection_pop_message(m_connection)); + + if (msg) + { + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Message received (interface: {}, member: {})", + dbus_message_get_interface(msg.get()), dbus_message_get_member(msg.get())); + + if (dbus_message_is_signal(msg.get(), DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesAdded")) + { + HandleInterfacesAdded(msg.get()); + return false; + } + else if (dbus_message_is_signal(msg.get(), DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesRemoved")) + { + return HandleInterfacesRemoved(msg.get(), callback); + } + else if (dbus_message_is_signal(msg.get(), DBUS_INTERFACE_PROPERTIES, "PropertiesChanged")) + { + return HandlePropertiesChanged(msg.get(), callback); + } + } + } + return false; +} + +bool CUDisks2Provider::HasUDisks2() +{ + return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, UDISKS2_SERVICE_UDISKS2, UDISKS2_PATH_UDISKS2, DBUS_INTERFACE_PEER, + "Ping"); +} + +bool CUDisks2Provider::Eject(const std::string &mountpath) +{ + std::string path(mountpath); + URIUtils::RemoveSlashAtEnd(path); + + for (const auto &elt: m_filesystems) + { + Filesystem *fs = elt.second; + if (fs->GetMountPoint() == path) + { + return fs->Unmount(); + } + } + + return false; +} + +std::vector<std::string> CUDisks2Provider::GetDiskUsage() +{ + CPosixMountProvider legacy; + return legacy.GetDiskUsage(); +} + +void CUDisks2Provider::GetDisks(VECSOURCES &devices, bool enumerateRemovable) +{ + for (const auto &elt: m_filesystems) + { + Filesystem *fs = elt.second; + if (fs->IsApproved() && fs->m_block->m_isSystem != enumerateRemovable) + devices.push_back(fs->ToMediaShare()); + } +} + +void CUDisks2Provider::DriveAdded(Drive *drive) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Drive added - {}", drive->ToString()); + + if (m_drives[drive->m_object]) + { + CLog::Log(LOGWARNING, "UDisks2: Inconsistency found! DriveAdded on an indexed drive"); + delete m_drives[drive->m_object]; + } + + m_drives[drive->m_object] = drive; + + for (auto &elt: m_blocks) + { + auto block = elt.second; + if (block->m_driveobject == drive->m_object) + { + block->m_drive = drive; + BlockAdded(block, false); + } + } +} + +bool CUDisks2Provider::DriveRemoved(const std::string& object) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Drive removed ({})", object); + + if (m_drives.count(object) > 0) + { + delete m_drives[object]; + m_drives.erase(object); + } + + for (auto &elt: m_blocks) + { + auto block = elt.second; + if (block->m_driveobject == object) + { + block->m_drive = nullptr; + } + } + + return false; +} + +void CUDisks2Provider::BlockAdded(Block *block, bool isNew) +{ + if (isNew) + { + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Block added - {}", block->ToString()); + + if (m_drives.count(block->m_driveobject) > 0) + block->m_drive = m_drives[block->m_driveobject]; + + if (m_blocks[block->m_object]) + { + CLog::Log(LOGWARNING, "UDisks2: Inconsistency found! BlockAdded on an indexed block device"); + delete m_blocks[block->m_object]; + } + + m_blocks[block->m_object] = block; + } + + + if (m_filesystems.count(block->m_object) > 0) + { + auto fs = m_filesystems[block->m_object]; + fs->m_block = block; + FilesystemAdded(fs, false); + } +} + +bool CUDisks2Provider::BlockRemoved(const std::string& object) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Block removed ({})", object); + + if (m_blocks.count(object) > 0) + { + delete m_blocks[object]; + m_blocks.erase(object); + } + + if (m_filesystems.count(object) > 0) + { + m_filesystems[object]->m_block = nullptr; + } + + return false; +} + +void CUDisks2Provider::FilesystemAdded(Filesystem *fs, bool isNew) +{ + if (isNew) + { + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Filesystem added - {}", fs->ToString()); + + if (m_blocks.count(fs->GetObject()) > 0) + fs->m_block = m_blocks[fs->GetObject()]; + + if (m_filesystems[fs->GetObject()]) + { + CLog::Log(LOGWARNING, "UDisks2: Inconsistency found! FilesystemAdded on an indexed filesystem"); + delete m_filesystems[fs->GetObject()]; + } + + m_filesystems[fs->GetObject()] = fs; + } + + if (fs->IsReady() && !fs->IsMounted()) + { + // optical drives should be always mounted by default unless explicitly disabled by the user + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_handleMounting || + (fs->IsOptical() && + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_autoMountOpticalMedia)) + { + fs->Mount(); + } + } +} + +bool CUDisks2Provider::FilesystemRemoved(const char *object, IStorageEventsCallback *callback) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Filesystem removed ({})", object); + bool result = false; + if (m_filesystems.count(object) > 0) + { + auto fs = m_filesystems[object]; + if (fs->IsMounted()) + { + callback->OnStorageUnsafelyRemoved(fs->ToStorageDevice()); + result = true; + } + delete m_filesystems[object]; + m_filesystems.erase(object); + } + return result; +} + +void CUDisks2Provider::HandleManagedObjects(DBusMessage *msg) +{ + DBusMessageIter msgIter, dictIter; + dbus_message_iter_init(msg, &msgIter); + dbus_message_iter_recurse(&msgIter, &dictIter); + while (dbus_message_iter_get_arg_type(&dictIter) == DBUS_TYPE_DICT_ENTRY) + { + DBusMessageIter objIter; + dbus_message_iter_recurse(&dictIter, &objIter); + ParseInterfaces(&objIter); + dbus_message_iter_next(&dictIter); + } +} + +void CUDisks2Provider::HandleInterfacesAdded(DBusMessage *msg) +{ + DBusMessageIter msgIter; + dbus_message_iter_init(msg, &msgIter); + ParseInterfaces(&msgIter); +} + +bool CUDisks2Provider::HandleInterfacesRemoved(DBusMessage *msg, IStorageEventsCallback *callback) +{ + DBusMessageIter msgIter, ifaceIter; + const char *path, *iface; + bool result = false; + dbus_message_iter_init(msg, &msgIter); + dbus_message_iter_get_basic(&msgIter, &path); + dbus_message_iter_next(&msgIter); + dbus_message_iter_recurse(&msgIter, &ifaceIter); + while (dbus_message_iter_get_arg_type(&ifaceIter) == DBUS_TYPE_STRING) + { + dbus_message_iter_get_basic(&ifaceIter, &iface); + result |= RemoveInterface(path, iface, callback); + dbus_message_iter_next(&ifaceIter); + } + return result; +} + +bool CUDisks2Provider::HandlePropertiesChanged(DBusMessage *msg, IStorageEventsCallback *callback) +{ + DBusMessageIter msgIter, propsIter; + const char *object = dbus_message_get_path(msg); + const char *iface; + + dbus_message_iter_init(msg, &msgIter); + dbus_message_iter_get_basic(&msgIter, &iface); + dbus_message_iter_next(&msgIter); + dbus_message_iter_recurse(&msgIter, &propsIter); + + if (strcmp(iface, UDISKS2_INTERFACE_DRIVE) == 0) + { + return DrivePropertiesChanged(object, &propsIter); + } + else if (strcmp(iface, UDISKS2_INTERFACE_BLOCK) == 0) + { + return BlockPropertiesChanged(object, &propsIter); + } + else if (strcmp(iface, UDISKS2_INTERFACE_FILESYSTEM) == 0) + { + return FilesystemPropertiesChanged(object, &propsIter, callback); + } + + return false; +} + +bool CUDisks2Provider::DrivePropertiesChanged(const char *object, DBusMessageIter *propsIter) +{ + if (m_drives.count(object) > 0) + { + auto drive = m_drives[object]; + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Before update: {}", drive->ToString()); + auto ParseDriveProperty = std::bind(&CUDisks2Provider::ParseDriveProperty, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + ParseProperties(drive, propsIter, ParseDriveProperty); + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: After update: {}", drive->ToString()); + } + return false; +} + +bool CUDisks2Provider::BlockPropertiesChanged(const char *object, DBusMessageIter *propsIter) +{ + if (m_blocks.count(object) > 0) + { + auto block = m_blocks[object]; + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Before update: {}", block->ToString()); + auto ParseBlockProperty = std::bind(&CUDisks2Provider::ParseBlockProperty, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + ParseProperties(block, propsIter, ParseBlockProperty); + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: After update: {}", block->ToString()); + } + return false; +} + +bool CUDisks2Provider::FilesystemPropertiesChanged(const char *object, DBusMessageIter *propsIter, IStorageEventsCallback *callback) +{ + if (m_filesystems.count(object) > 0) + { + auto fs = m_filesystems[object]; + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Before update: {}", fs->ToString()); + bool wasMounted = fs->IsMounted(); + auto ParseFilesystemProperty = std::bind(&CUDisks2Provider::ParseFilesystemProperty, this, + std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + ParseProperties(fs, propsIter, ParseFilesystemProperty); + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: After update: {}", fs->ToString()); + + if (!wasMounted && fs->IsMounted() && fs->IsApproved()) + { + CLog::Log(LOGINFO, "UDisks2: Added {}", fs->GetMountPoint()); + if (callback) + callback->OnStorageAdded(fs->ToStorageDevice()); + return true; + } + else if (wasMounted && !fs->IsMounted()) + { + CLog::Log(LOGINFO, "UDisks2: Removed {}", fs->m_block->m_device); + if (callback) + callback->OnStorageSafelyRemoved(fs->ToStorageDevice()); + return true; + } + } + return false; +} + +bool CUDisks2Provider::RemoveInterface(const char *path, const char *iface, IStorageEventsCallback *callback) +{ + if (strcmp(iface, UDISKS2_INTERFACE_DRIVE) == 0) + { + return DriveRemoved(path); + } + else if (strcmp(iface, UDISKS2_INTERFACE_BLOCK) == 0) + { + return BlockRemoved(path); + } + else if (strcmp(iface, UDISKS2_INTERFACE_FILESYSTEM) == 0) + { + return FilesystemRemoved(path, callback); + } + else + { + return false; + } +} + + +void CUDisks2Provider::ParseInterfaces(DBusMessageIter *objIter) +{ + DBusMessageIter dictIter; + const char *object; + dbus_message_iter_get_basic(objIter, &object); + dbus_message_iter_next(objIter); + dbus_message_iter_recurse(objIter, &dictIter); + while (dbus_message_iter_get_arg_type(&dictIter) == DBUS_TYPE_DICT_ENTRY) + { + DBusMessageIter ifaceIter, propsIter; + const char *iface; + dbus_message_iter_recurse(&dictIter, &ifaceIter); + dbus_message_iter_get_basic(&ifaceIter, &iface); + dbus_message_iter_next(&ifaceIter); + dbus_message_iter_recurse(&ifaceIter, &propsIter); + ParseInterface(object, iface, &propsIter/*, &discovery*/); + dbus_message_iter_next(&dictIter); + } +} + +void CUDisks2Provider::ParseInterface(const char *object, const char *iface, DBusMessageIter *propsIter) +{ + if (strcmp(iface, UDISKS2_INTERFACE_DRIVE) == 0) + { + auto *drive = new Drive(object); + auto f = std::bind(&CUDisks2Provider::ParseDriveProperty, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + ParseProperties(drive, propsIter, f); + DriveAdded(drive); + } + else if (strcmp(iface, UDISKS2_INTERFACE_BLOCK) == 0) + { + auto *block = new Block(object); + auto f = std::bind(&CUDisks2Provider::ParseBlockProperty, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + ParseProperties(block, propsIter, f); + BlockAdded(block); + } + else if (strcmp(iface, UDISKS2_INTERFACE_FILESYSTEM) == 0) + { + auto *fs = new Filesystem(object); + auto f = std::bind(&CUDisks2Provider::ParseFilesystemProperty, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3); + ParseProperties(fs, propsIter, f); + FilesystemAdded(fs); + } +} + + +template<class Object, class Function> +void CUDisks2Provider::ParseProperties(Object *ref, DBusMessageIter *propsIter, Function f) +{ + while (dbus_message_iter_get_arg_type(propsIter) == DBUS_TYPE_DICT_ENTRY) + { + DBusMessageIter propIter, varIter; + const char *key; + + dbus_message_iter_recurse(propsIter, &propIter); + + dbus_message_iter_get_basic(&propIter, &key); + dbus_message_iter_next(&propIter); + + dbus_message_iter_recurse(&propIter, &varIter); + + f(ref, key, &varIter); + + dbus_message_iter_next(propsIter); + } + +} + +void CUDisks2Provider::ParseDriveProperty(Drive *drive, const char *key, DBusMessageIter *varIter) +{ + switch (dbus_message_iter_get_arg_type(varIter)) + { + case DBUS_TYPE_BOOLEAN: + { + dbus_bool_t value; + + if (strcmp(key, "Removable") == 0) + { + dbus_message_iter_get_basic(varIter, &value); + drive->m_isRemovable = static_cast<bool>(value); + } + + break; + } + case DBUS_TYPE_ARRAY: + { + DBusMessageIter arrIter; + + if (strcmp(key, "MediaCompatibility") == 0) + { + dbus_message_iter_recurse(varIter, &arrIter); + while (dbus_message_iter_get_arg_type(&arrIter) == DBUS_TYPE_STRING) + { + const char *compatibility; + dbus_message_iter_get_basic(&arrIter, &compatibility); + drive->m_mediaCompatibility.emplace_back(compatibility); + dbus_message_iter_next(&arrIter); + } + } + + break; + } + default: + break; + } +} + + +void CUDisks2Provider::ParseBlockProperty(Block *block, const char *key, DBusMessageIter *varIter) +{ + switch (dbus_message_iter_get_arg_type(varIter)) + { + case DBUS_TYPE_OBJECT_PATH: + { + const char *value; + + if (strcmp(key, "Drive") == 0) + { + dbus_message_iter_get_basic(varIter, &value); + block->m_driveobject.assign(value); + } + + break; + } + case DBUS_TYPE_STRING: + { + const char *value; + + if (strcmp(key, "IdLabel") == 0) + { + dbus_message_iter_get_basic(varIter, &value); + block->m_label.assign(value); + } + + break; + } + case DBUS_TYPE_BOOLEAN: + { + dbus_bool_t value; + + if (strcmp(key, "HintSystem") == 0) + { + dbus_message_iter_get_basic(varIter, &value); + block->m_isSystem = static_cast<bool>(value); + } + + break; + } + case DBUS_TYPE_UINT64: + { + dbus_uint64_t value; + + if (strcmp(key, "Size") == 0) + { + dbus_message_iter_get_basic(varIter, &value); + block->m_size = value; + } + + break; + } + case DBUS_TYPE_ARRAY: + { + DBusMessageIter arrIter; + + if (strcmp(key, "PreferredDevice") == 0) + { + dbus_message_iter_recurse(varIter, &arrIter); + block->m_device.assign(ParseByteArray(&arrIter)); + } + + break; + } + default: + break; + } +} + +void CUDisks2Provider::ParseFilesystemProperty(Filesystem *fs, const char *key, DBusMessageIter *varIter) +{ + switch (dbus_message_iter_get_arg_type(varIter)) + { + case DBUS_TYPE_ARRAY: + { + DBusMessageIter arrIter; + + if (strcmp(key, "MountPoints") == 0) + { + dbus_message_iter_recurse(varIter, &arrIter); + + if (dbus_message_iter_get_arg_type(&arrIter) == DBUS_TYPE_ARRAY) + { + DBusMessageIter valIter; + + dbus_message_iter_recurse(&arrIter, &valIter); + fs->SetMountPoint(ParseByteArray(&valIter)); + + dbus_message_iter_next(&arrIter); + } + else + { + fs->ResetMountPoint(); + } + } + + break; + } + default: + break; + } +} + +std::string CUDisks2Provider::ParseByteArray(DBusMessageIter *arrIter) +{ + std::ostringstream strStream; + + while (dbus_message_iter_get_arg_type(arrIter) == DBUS_TYPE_BYTE) + { + dbus_int16_t value = 0; + dbus_message_iter_get_basic(arrIter, &value); + if (value == 0) + break; + strStream << static_cast<char>(value); + dbus_message_iter_next(arrIter); + } + + return strStream.str(); +} + +void CUDisks2Provider::AppendEmptyOptions(DBusMessageIter *argsIter) +{ + DBusMessageIter dictIter; + dbus_message_iter_open_container(argsIter, DBUS_TYPE_ARRAY, "{sv}", &dictIter); + dbus_message_iter_close_container(argsIter, &dictIter); +} diff --git a/xbmc/platform/linux/storage/UDisks2Provider.h b/xbmc/platform/linux/storage/UDisks2Provider.h new file mode 100644 index 0000000..b4c85ee --- /dev/null +++ b/xbmc/platform/linux/storage/UDisks2Provider.h @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DBusUtil.h" +#include "storage/IStorageProvider.h" + +#include <string> +#include <vector> + +class CUDisks2Provider : public IStorageProvider +{ + class Drive + { + public: + std::string m_object; + bool m_isRemovable = false; + std::vector<std::string> m_mediaCompatibility; + + explicit Drive(const char *object); + ~Drive() = default; + + /*! \brief Check if the drive is optical + * @return true if the drive is optical, false otherwise + */ + bool IsOptical() const; + + /*! \brief Get a representation of the drive as a readable string + * @return drive as a string + */ + std::string ToString() const; + }; + + class Block + { + public: + Drive *m_drive = nullptr; + std::string m_object; + std::string m_driveobject; + std::string m_label; + std::string m_device; + bool m_isSystem = false; + u_int64_t m_size = 0; + + explicit Block(const char *object); + ~Block() = default; + + bool IsReady(); + + /*! \brief Get a representation of the block as a readable string + * @return block as a string + */ + std::string ToString() const; + }; + + class Filesystem + { + public: + Block *m_block = nullptr; + + explicit Filesystem(const char *object); + ~Filesystem() = default; + + bool Mount(); + bool Unmount(); + + /*! \brief Get the device display name/label + * @return the device display name/label + */ + std::string GetDisplayName() const; + + /*! \brief Check if the device is approved/whitelisted + * @return true if the device is approved/whitelisted, false otherwise + */ + bool IsApproved() const; + + /*! \brief Check if the device is mounted + * @return true if the device is mounted, false otherwise + */ + bool IsMounted() const; + + /*! \brief Check if the device is ready + * @return true if the device is ready, false otherwise + */ + bool IsReady() const; + + /*! \brief Check if the device is optical + * @return true if the device is optical, false otherwise + */ + bool IsOptical() const; + + /*! \brief Get the storage type of this device + * @return the storage type (e.g. OPTICAL) or UNKNOWN if + * the type couldn't be detected + */ + MEDIA_DETECT::STORAGE::Type GetStorageType() const; + + /*! \brief Get the device mount point + * @return the device mount point + */ + std::string GetMountPoint() const; + + /*! \brief Reset the device mount point + */ + void ResetMountPoint(); + + /*! \brief Set the device mount point + * @param mountPoint the device mount point + */ + void SetMountPoint(const std::string& mountPoint); + + /*! \brief Get the device dbus object + * @return the device dbus object + */ + std::string GetObject() const; + + /*! \brief Get a representation of the device as a readable string + * @return device as a string + */ + std::string ToString() const; + + /*! \brief Get a representation of the device as a media share + * @return the media share + */ + CMediaSource ToMediaShare() const; + + /*! \brief Get a representation of the device as a storage device abstraction + * @return the storage device abstraction of the device + */ + MEDIA_DETECT::STORAGE::StorageDevice ToStorageDevice() const; + + private: + bool m_isMounted = false; + std::string m_object; + std::string m_mountPoint; + }; + + typedef std::map<std::string, Drive *> DriveMap; + typedef std::map<std::string, Block *> BlockMap; + typedef std::map<std::string, Filesystem *> FilesystemMap; + +public: + CUDisks2Provider(); + ~CUDisks2Provider() override; + + void Initialize() override; + + bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override; + + static bool HasUDisks2(); + + bool Eject(const std::string &mountpath) override; + + std::vector<std::string> GetDiskUsage() override; + + void GetLocalDrives(VECSOURCES &localDrives) override + { GetDisks(localDrives, false); } + + void GetRemovableDrives(VECSOURCES &removableDrives) override + { GetDisks(removableDrives, true); } + + void Stop() override + {} + +private: + CDBusConnection m_connection; + + DriveMap m_drives; + BlockMap m_blocks; + FilesystemMap m_filesystems; + + std::string m_daemonVersion; + + void GetDisks(VECSOURCES &devices, bool enumerateRemovable); + + void DriveAdded(Drive *drive); + bool DriveRemoved(const std::string& object); + void BlockAdded(Block *block, bool isNew = true); + bool BlockRemoved(const std::string& object); + void FilesystemAdded(Filesystem *fs, bool isNew = true); + bool FilesystemRemoved(const char *object, IStorageEventsCallback *callback); + + bool HandleInterfacesRemoved(DBusMessage *msg, IStorageEventsCallback *callback); + void HandleInterfacesAdded(DBusMessage *msg); + bool HandlePropertiesChanged(DBusMessage *msg, IStorageEventsCallback *callback); + + bool DrivePropertiesChanged(const char *object, DBusMessageIter *propsIter); + bool BlockPropertiesChanged(const char *object, DBusMessageIter *propsIter); + bool FilesystemPropertiesChanged(const char *object, DBusMessageIter *propsIter, IStorageEventsCallback *callback); + + bool RemoveInterface(const char *path, const char *iface, IStorageEventsCallback *callback); + + template<class Object, class Function> + void ParseProperties(Object *ref, DBusMessageIter *dictIter, Function f); + void ParseInterfaces(DBusMessageIter *dictIter); + void ParseDriveProperty(Drive *drive, const char *key, DBusMessageIter *varIter); + void ParseBlockProperty(Block *block, const char *key, DBusMessageIter *varIter); + void ParseFilesystemProperty(Filesystem *fs, const char *key, DBusMessageIter *varIter); + std::string ParseByteArray(DBusMessageIter *arrIter); + void HandleManagedObjects(DBusMessage *msg); + void ParseInterface(const char *object, const char *iface, DBusMessageIter *propsIter); + + static void AppendEmptyOptions(DBusMessageIter *argsIter); +}; diff --git a/xbmc/platform/linux/storage/UDisksProvider.cpp b/xbmc/platform/linux/storage/UDisksProvider.cpp new file mode 100644 index 0000000..7a2a034 --- /dev/null +++ b/xbmc/platform/linux/storage/UDisksProvider.cpp @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "UDisksProvider.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include "platform/posix/PosixMountProvider.h" + +CUDiskDevice::CUDiskDevice(const char *DeviceKitUDI): + m_DeviceKitUDI(DeviceKitUDI) +{ + m_isMounted = false; + m_isMountedByUs = false; + m_isRemovable = false; + m_isPartition = false; + m_isFileSystem = false; + m_isSystemInternal = false; + m_isOptical = false; + m_PartitionSize = 0; + Update(); +} + +void CUDiskDevice::Update() +{ + CVariant properties = CDBusUtil::GetAll("org.freedesktop.UDisks", m_DeviceKitUDI.c_str(), "org.freedesktop.UDisks.Device"); + + m_isFileSystem = properties["IdUsage"].asString() == "filesystem"; + if (m_isFileSystem) + { + m_UDI = properties["IdUuid"].asString(); + m_Label = properties["IdLabel"].asString(); + m_FileSystem = properties["IdType"].asString(); + } + else + { + m_UDI.clear(); + m_Label.clear(); + m_FileSystem.clear(); + } + + m_isMounted = properties["DeviceIsMounted"].asBoolean(); + if (m_isMounted && !properties["DeviceMountPaths"].empty()) + m_MountPath = properties["DeviceMountPaths"][0].asString(); + else + m_MountPath.clear(); + + m_PartitionSize = properties["PartitionSize"].asUnsignedInteger(); + m_isPartition = properties["DeviceIsPartition"].asBoolean(); + m_isSystemInternal = properties["DeviceIsSystemInternal"].asBoolean(); + m_isOptical = properties["DeviceIsOpticalDisc"].asBoolean(); + if (m_isPartition) + { + CVariant isRemovable = CDBusUtil::GetVariant("org.freedesktop.UDisks", properties["PartitionSlave"].asString().c_str(), "org.freedesktop.UDisks.Device", "DeviceIsRemovable"); + + if ( !isRemovable.isNull() ) + m_isRemovable = isRemovable.asBoolean(); + else + m_isRemovable = false; + } + else + m_isRemovable = properties["DeviceIsRemovable"].asBoolean(); +} + +bool CUDiskDevice::Mount() +{ + if (!m_isMounted && !m_isSystemInternal && m_isFileSystem) + { + CLog::Log(LOGDEBUG, "UDisks: Mounting {}", m_DeviceKitUDI); + CDBusMessage message("org.freedesktop.UDisks", m_DeviceKitUDI.c_str(), "org.freedesktop.UDisks.Device", "FilesystemMount"); + message.AppendArgument(""); + const char *array[] = {}; + message.AppendArgument(array, 0); + + DBusMessage *reply = message.SendSystem(); + if (reply) + { + char *mountPoint; + if (dbus_message_get_args (reply, NULL, DBUS_TYPE_STRING, &mountPoint, DBUS_TYPE_INVALID)) + { + m_MountPath = mountPoint; + CLog::Log(LOGDEBUG, "UDisks: Successfully mounted {} on {}", m_DeviceKitUDI, mountPoint); + m_isMountedByUs = m_isMounted = true; + } + } + + return m_isMounted; + } + else + CLog::Log(LOGDEBUG, "UDisks: Is not able to mount {}", ToString()); + + return false; +} + +bool CUDiskDevice::UnMount() +{ + if (m_isMounted && !m_isSystemInternal && m_isFileSystem) + { + CDBusMessage message("org.freedesktop.UDisks", m_DeviceKitUDI.c_str(), "org.freedesktop.UDisks.Device", "FilesystemUnmount"); + + const char *array[1]; + message.AppendArgument(array, 0); + + DBusMessage *reply = message.SendSystem(); + if (reply) + m_isMountedByUs = m_isMounted = false; + + return !m_isMounted; + } + else + CLog::Log(LOGDEBUG, "UDisks: Is not able to unmount {}", ToString()); + + return false; +} + +CMediaSource CUDiskDevice::ToMediaShare() const +{ + CMediaSource source; + source.strPath = m_MountPath; + if (m_Label.empty()) + { + std::string strSize = StringUtils::SizeToString(m_PartitionSize); + source.strName = StringUtils::Format("{} {}", strSize, g_localizeStrings.Get(155)); + } + else + source.strName = m_Label; + if (m_isOptical) + source.m_iDriveType = CMediaSource::SOURCE_TYPE_DVD; + else if (m_isSystemInternal) + source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + else + source.m_iDriveType = CMediaSource::SOURCE_TYPE_REMOVABLE; + source.m_ignore = true; + return source; +} + +bool CUDiskDevice::IsApproved() const +{ + return (m_isFileSystem && m_isMounted && m_UDI.length() > 0 && (m_FileSystem.length() > 0 && m_FileSystem != "swap") + && m_MountPath != "/" && m_MountPath != "/boot") || m_isOptical; +} + +bool CUDiskDevice::IsOptical() const +{ + return m_isOptical; +} + +MEDIA_DETECT::STORAGE::Type CUDiskDevice::GetStorageType() const +{ + if (IsOptical()) + return MEDIA_DETECT::STORAGE::Type::OPTICAL; + + return MEDIA_DETECT::STORAGE::Type::UNKNOWN; +} + +bool CUDiskDevice::IsMounted() const +{ + return m_isMounted; +} + +std::string CUDiskDevice::GetDisplayName() const +{ + return m_Label; +} + +std::string CUDiskDevice::GetMountPoint() const +{ + return m_MountPath; +} + +bool CUDiskDevice::IsSystemInternal() const +{ + return m_isSystemInternal; +} + +MEDIA_DETECT::STORAGE::StorageDevice CUDiskDevice::ToStorageDevice() const +{ + MEDIA_DETECT::STORAGE::StorageDevice device; + device.label = GetDisplayName(); + device.path = GetMountPoint(); + device.type = GetStorageType(); + return device; +} + +#define BOOL2SZ(b) ((b) ? "true" : "false") + +std::string CUDiskDevice::ToString() const +{ + return StringUtils::Format("DeviceUDI {}: IsFileSystem {} HasFileSystem {} " + "IsSystemInternal {} IsMounted {} IsRemovable {} IsPartition {} " + "IsOptical {}", + m_DeviceKitUDI, BOOL2SZ(m_isFileSystem), m_FileSystem, + BOOL2SZ(m_isSystemInternal), BOOL2SZ(m_isMounted), + BOOL2SZ(m_isRemovable), BOOL2SZ(m_isPartition), BOOL2SZ(m_isOptical)); +} + +CUDisksProvider::CUDisksProvider() +{ + //! @todo do not use dbus_connection_pop_message() that requires the use of a + //! private connection + if (!m_connection.Connect(DBUS_BUS_SYSTEM, true)) + { + return; + } + + dbus_connection_set_exit_on_disconnect(m_connection, false); + + CDBusError error; + dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.UDisks'", error); + dbus_connection_flush(m_connection); + + if (error) + { + error.Log("UDisks: Failed to attach to signal"); + m_connection.Destroy(); + } +} + +CUDisksProvider::~CUDisksProvider() +{ + for (auto& itr : m_AvailableDevices) + delete itr.second; + + m_AvailableDevices.clear(); +} + +void CUDisksProvider::Initialize() +{ + CLog::Log(LOGDEBUG, "Selected UDisks as storage provider"); + m_DaemonVersion = atoi(CDBusUtil::GetVariant("org.freedesktop.UDisks", "/org/freedesktop/UDisks", "org.freedesktop.UDisks", "DaemonVersion").asString().c_str()); + CLog::Log(LOGDEBUG, "UDisks: DaemonVersion {}", m_DaemonVersion); + + CLog::Log(LOGDEBUG, "UDisks: Querying available devices"); + std::vector<std::string> devices = EnumerateDisks(); + for (unsigned int i = 0; i < devices.size(); i++) + DeviceAdded(devices[i].c_str(), NULL); +} + +bool CUDisksProvider::Eject(const std::string& mountpath) +{ + std::string path(mountpath); + URIUtils::RemoveSlashAtEnd(path); + + for (auto& itr : m_AvailableDevices) + { + CUDiskDevice* device = itr.second; + if (device->GetMountPoint() == path) + return device->UnMount(); + } + + return false; +} + +std::vector<std::string> CUDisksProvider::GetDiskUsage() +{ + CPosixMountProvider legacy; + return legacy.GetDiskUsage(); +} + +bool CUDisksProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback) +{ + bool result = false; + if (m_connection) + { + dbus_connection_read_write(m_connection, 0); + DBusMessagePtr msg(dbus_connection_pop_message(m_connection)); + + if (msg) + { + char *object; + if (dbus_message_get_args (msg.get(), NULL, DBUS_TYPE_OBJECT_PATH, &object, DBUS_TYPE_INVALID)) + { + result = true; + if (dbus_message_is_signal(msg.get(), "org.freedesktop.UDisks", "DeviceAdded")) + DeviceAdded(object, callback); + else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UDisks", "DeviceRemoved")) + DeviceRemoved(object, callback); + else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UDisks", "DeviceChanged")) + DeviceChanged(object, callback); + } + } + } + return result; +} + +bool CUDisksProvider::HasUDisks() +{ + return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, "org.freedesktop.UDisks", "/org/freedesktop/UDisks", "org.freedesktop.UDisks", "EnumerateDevices"); +} + +void CUDisksProvider::DeviceAdded(const char *object, IStorageEventsCallback *callback) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceAdded ({})", object); + + if (m_AvailableDevices[object]) + { + CLog::Log(LOGWARNING, "UDisks: Inconsistency found! DeviceAdded on an indexed disk"); + delete m_AvailableDevices[object]; + } + + CUDiskDevice *device = NULL; + device = new CUDiskDevice(object); + m_AvailableDevices[object] = device; + + // optical drives should be always mounted by default unless explicitly disabled by the user + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_handleMounting || + (device->IsOptical() && + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_autoMountOpticalMedia)) + { + device->Mount(); + } + + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceAdded - {}", device->ToString()); + + if (device->IsMounted() && device->IsApproved()) + { + CLog::Log(LOGINFO, "UDisks: Added {}", device->GetMountPoint()); + if (callback) + callback->OnStorageAdded(device->ToStorageDevice()); + } +} + +void CUDisksProvider::DeviceRemoved(const char *object, IStorageEventsCallback *callback) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceRemoved ({})", object); + + CUDiskDevice *device = m_AvailableDevices[object]; + if (device) + { + if (device->IsMounted() && callback) + callback->OnStorageUnsafelyRemoved(device->ToStorageDevice()); + + delete m_AvailableDevices[object]; + m_AvailableDevices.erase(object); + } +} + +void CUDisksProvider::DeviceChanged(const char *object, IStorageEventsCallback *callback) +{ + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceChanged ({})", object); + + CUDiskDevice *device = m_AvailableDevices[object]; + if (device == NULL) + { + CLog::Log(LOGWARNING, "UDisks: Inconsistency found! DeviceChanged on an unindexed disk"); + DeviceAdded(object, callback); + } + else + { + bool mounted = device->IsMounted(); + // make sure to not silently remount ejected usb thumb drives that user wants to eject + // optical drives should be always mounted by default unless explicitly disabled by the user + const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + if (!mounted && device->IsOptical()) + { + if (advancedSettings->m_handleMounting || advancedSettings->m_autoMountOpticalMedia) + { + device->Mount(); + } + } + + device->Update(); + if (!mounted && device->IsMounted() && callback) + callback->OnStorageAdded(device->ToStorageDevice()); + else if (mounted && !device->IsMounted() && callback) + callback->OnStorageSafelyRemoved(device->ToStorageDevice()); + + CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceChanged - {}", device->ToString()); + } +} + +std::vector<std::string> CUDisksProvider::EnumerateDisks() +{ + std::vector<std::string> devices; + CDBusMessage message("org.freedesktop.UDisks", "/org/freedesktop/UDisks", "org.freedesktop.UDisks", "EnumerateDevices"); + DBusMessage *reply = message.SendSystem(); + if (reply) + { + char** disks = NULL; + int length = 0; + + if (dbus_message_get_args (reply, NULL, DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &disks, &length, DBUS_TYPE_INVALID)) + { + for (int i = 0; i < length; i++) + devices.emplace_back(disks[i]); + + dbus_free_string_array(disks); + } + } + + return devices; +} + +void CUDisksProvider::GetDisks(VECSOURCES& devices, bool EnumerateRemovable) +{ + for (auto& itr : m_AvailableDevices) + { + CUDiskDevice* device = itr.second; + if (device && device->IsApproved() && device->IsSystemInternal() != EnumerateRemovable) + devices.push_back(device->ToMediaShare()); + } +} diff --git a/xbmc/platform/linux/storage/UDisksProvider.h b/xbmc/platform/linux/storage/UDisksProvider.h new file mode 100644 index 0000000..c5372f8 --- /dev/null +++ b/xbmc/platform/linux/storage/UDisksProvider.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DBusUtil.h" +#include "storage/IStorageProvider.h" + +#include <string> +#include <vector> + +class CUDiskDevice +{ +public: + CUDiskDevice(const char *DeviceKitUDI); + ~CUDiskDevice() = default; + + void Update(); + + bool Mount(); + bool UnMount(); + + /*! \brief Check if the device is approved/whitelisted + * @return true if the device is approved/whitelisted, false otherwise + */ + bool IsApproved() const; + + /*! \brief Get the storage type of this device + * @return the storage type (e.g. OPTICAL) or UNKNOWN if + * the type couldn't be detected + */ + MEDIA_DETECT::STORAGE::Type GetStorageType() const; + + /*! \brief Check if the device is optical + * @return true if the device is optical, false otherwise + */ + bool IsOptical() const; + + /*! \brief Check if the device is mounted + * @return true if the device is mounted, false otherwise + */ + bool IsMounted() const; + + /*! \brief Check if the device is internal to the system + * @return true if the device is internal to the system, false otherwise + */ + bool IsSystemInternal() const; + + /*! \brief Get the device display name/label + * @return the device display name/label + */ + std::string GetDisplayName() const; + + /*! \brief Get the device mount point + * @return the device mount point + */ + std::string GetMountPoint() const; + + /*! \brief Get a representation of the device as a readable string + * @return device as a string + */ + std::string ToString() const; + + /*! \brief Get a representation of the device as a media share + * @return the media share + */ + CMediaSource ToMediaShare() const; + + /*! \brief Get a representation of the device as a storage device abstraction + * @return the storage device abstraction of the device + */ + MEDIA_DETECT::STORAGE::StorageDevice ToStorageDevice() const; + +private: + std::string m_UDI; + std::string m_DeviceKitUDI; + std::string m_MountPath; + std::string m_FileSystem; + std::string m_Label; + bool m_isMounted; + bool m_isMountedByUs; + bool m_isRemovable; + bool m_isPartition; + bool m_isFileSystem; + bool m_isSystemInternal; + bool m_isOptical; + int64_t m_PartitionSize; +}; + +class CUDisksProvider : public IStorageProvider +{ +public: + CUDisksProvider(); + ~CUDisksProvider() override; + + void Initialize() override; + void Stop() override { } + + void GetLocalDrives(VECSOURCES &localDrives) override { GetDisks(localDrives, false); } + void GetRemovableDrives(VECSOURCES &removableDrives) override { GetDisks(removableDrives, true); } + + bool Eject(const std::string& mountpath) override; + + std::vector<std::string> GetDiskUsage() override; + + bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override; + + static bool HasUDisks(); +private: + typedef std::map<std::string, CUDiskDevice *> DeviceMap; + typedef std::pair<std::string, CUDiskDevice *> DevicePair; + + void DeviceAdded(const char *object, IStorageEventsCallback *callback); + void DeviceRemoved(const char *object, IStorageEventsCallback *callback); + void DeviceChanged(const char *object, IStorageEventsCallback *callback); + + std::vector<std::string> EnumerateDisks(); + + void GetDisks(VECSOURCES& devices, bool EnumerateRemovable); + + int m_DaemonVersion; + + DeviceMap m_AvailableDevices; + + CDBusConnection m_connection; +}; diff --git a/xbmc/platform/linux/test/CMakeLists.txt b/xbmc/platform/linux/test/CMakeLists.txt new file mode 100644 index 0000000..1fb0281 --- /dev/null +++ b/xbmc/platform/linux/test/CMakeLists.txt @@ -0,0 +1,3 @@ +list(APPEND SOURCES TestSysfsPath.cpp) + +core_add_test_library(linux_test) diff --git a/xbmc/platform/linux/test/TestSysfsPath.cpp b/xbmc/platform/linux/test/TestSysfsPath.cpp new file mode 100644 index 0000000..91e7763 --- /dev/null +++ b/xbmc/platform/linux/test/TestSysfsPath.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "platform/linux/SysfsPath.h" +#include "utils/StringUtils.h" + +#include <fstream> +#include <sstream> +#include <string> + +#include <gtest/gtest.h> + +struct TestSysfsPath : public ::testing::Test +{ + std::string GetTestFilePath() + { + std::string tmpdir{"/tmp"}; + const char *test_tmpdir = getenv("TMPDIR"); + + if (test_tmpdir && test_tmpdir[0] != '\0') { + tmpdir.assign(test_tmpdir); + } + + return tmpdir + "/kodi-test-" + StringUtils::CreateUUID(); + } +}; + +TEST_F(TestSysfsPath, SysfsPathTestInt) +{ + std::string filepath = GetTestFilePath(); + std::ofstream m_output(filepath); + + int temp{1234}; + m_output << temp; + m_output.close(); + + CSysfsPath path(filepath); + ASSERT_TRUE(path.Exists()); + EXPECT_EQ(path.Get<int>(), 1234); + EXPECT_EQ(path.Get<float>(), 1234); + EXPECT_EQ(path.Get<double>(), 1234); + EXPECT_EQ(path.Get<uint64_t>(), 1234); + EXPECT_EQ(path.Get<uint16_t>(), 1234); + EXPECT_EQ(path.Get<unsigned int>(), 1234); + EXPECT_EQ(path.Get<unsigned long int>(), 1234); + + std::remove(filepath.c_str()); +} + +TEST_F(TestSysfsPath, SysfsPathTestString) +{ + std::string filepath = GetTestFilePath(); + std::ofstream m_output{filepath}; + + std::string temp{"test"}; + m_output << temp; + m_output.close(); + + CSysfsPath path(filepath); + ASSERT_TRUE(path.Exists()); + EXPECT_EQ(path.Get<std::string>(), "test"); + + std::remove(filepath.c_str()); +} + +TEST_F(TestSysfsPath, SysfsPathTestLongString) +{ + std::string filepath = GetTestFilePath().append("-long"); + std::ofstream m_output{filepath}; + + std::string temp{"test with spaces"}; + m_output << temp; + m_output.close(); + + CSysfsPath path(filepath); + ASSERT_TRUE(path.Exists()); + EXPECT_EQ(path.Get<std::string>(), "test with spaces"); + + std::remove(filepath.c_str()); +} + +TEST_F(TestSysfsPath, SysfsPathTestPathDoesNotExist) +{ + CSysfsPath otherPath{"/thispathdoesnotexist"}; + ASSERT_FALSE(otherPath.Exists()); +} diff --git a/xbmc/platform/linux/threads/CMakeLists.txt b/xbmc/platform/linux/threads/CMakeLists.txt new file mode 100644 index 0000000..b90d55e --- /dev/null +++ b/xbmc/platform/linux/threads/CMakeLists.txt @@ -0,0 +1,4 @@ +set(SOURCES ThreadImplLinux.cpp) +set(HEADERS ThreadImplLinux.h) + +core_add_library(platform_linux_threads) diff --git a/xbmc/platform/linux/threads/ThreadImplLinux.cpp b/xbmc/platform/linux/threads/ThreadImplLinux.cpp new file mode 100644 index 0000000..f4810cf --- /dev/null +++ b/xbmc/platform/linux/threads/ThreadImplLinux.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ThreadImplLinux.h" + +#include "utils/Map.h" +#include "utils/log.h" + +#include <algorithm> +#include <array> +#include <limits.h> + +#include <sys/resource.h> +#include <unistd.h> + +#if !defined(TARGET_ANDROID) && (defined(__GLIBC__) || defined(__UCLIBC__)) +#if defined(__UCLIBC__) || !__GLIBC_PREREQ(2, 30) +#include <sys/syscall.h> +#endif +#endif + +namespace +{ + +constexpr auto nativeThreadPriorityMap = make_map<ThreadPriority, int>({ + {ThreadPriority::LOWEST, -1}, + {ThreadPriority::BELOW_NORMAL, -1}, + {ThreadPriority::NORMAL, 0}, + {ThreadPriority::ABOVE_NORMAL, 1}, + {ThreadPriority::HIGHEST, 1}, +}); + +static_assert(static_cast<size_t>(ThreadPriority::PRIORITY_COUNT) == nativeThreadPriorityMap.size(), + "nativeThreadPriorityMap doesn't match the size of ThreadPriority, did you forget to " + "add/remove a mapping?"); + +constexpr int ThreadPriorityToNativePriority(const ThreadPriority& priority) +{ + const auto it = nativeThreadPriorityMap.find(priority); + if (it != nativeThreadPriorityMap.cend()) + { + return it->second; + } + else + { + throw std::range_error("Priority not found"); + } +} + +#if !defined(TARGET_ANDROID) && (defined(__GLIBC__) || defined(__UCLIBC__)) +#if defined(__UCLIBC__) || !__GLIBC_PREREQ(2, 30) +static pid_t gettid() +{ + return syscall(__NR_gettid); +} +#endif +#endif + +} // namespace + +static int s_maxPriority; +static bool s_maxPriorityIsSet{false}; + +// We need to return what the best number than can be passed +// to SetPriority is. It will basically be relative to the +// the main thread's nice level, inverted (since "higher" priority +// nice levels are actually lower numbers). +static int GetUserMaxPriority(int maxPriority) +{ + if (s_maxPriorityIsSet) + return s_maxPriority; + + // if we're root, then we can do anything. So we'll allow + // max priority. + if (geteuid() == 0) + return maxPriority; + + // get user max prio + struct rlimit limit; + if (getrlimit(RLIMIT_NICE, &limit) != 0) + { + // If we fail getting the limit for nice we just assume we can't raise the priority + return 0; + } + + const int appNice = getpriority(PRIO_PROCESS, getpid()); + const int rlimVal = limit.rlim_cur; + + // according to the docs, limit.rlim_cur shouldn't be zero, yet, here we are. + // if a user has no entry in limits.conf rlim_cur is zero. In this case the best + // nice value we can hope to achieve is '0' as a regular user + const int userBestNiceValue = (rlimVal == 0) ? 0 : (20 - rlimVal); + + // running the app with nice -n 10 -> + // e.g. +10 10 - 0 // default non-root user. + // e.g. +30 10 - -20 // if root with rlimits set. + // running the app default -> + // e.g. 0 0 - 0 // default non-root user. + // e.g. +20 0 - -20 // if root with rlimits set. + const int bestUserSetPriority = appNice - userBestNiceValue; // nice is inverted from prio. + + // static because we only need to check this once. + // we shouldn't expect a user to change RLIMIT_NICE while running + // and it won't work anyway for threads that already set their priority. + s_maxPriority = std::min(maxPriority, bestUserSetPriority); + s_maxPriorityIsSet = true; + + return s_maxPriority; +} + +std::unique_ptr<IThreadImpl> IThreadImpl::CreateThreadImpl(std::thread::native_handle_type handle) +{ + return std::make_unique<CThreadImplLinux>(handle); +} + +CThreadImplLinux::CThreadImplLinux(std::thread::native_handle_type handle) + : IThreadImpl(handle), m_threadID(gettid()) +{ +} + +void CThreadImplLinux::SetThreadInfo(const std::string& name) +{ +#if defined(__GLIBC__) + pthread_setname_np(m_handle, name.c_str()); +#endif + + // get user max prio + const int maxPrio = ThreadPriorityToNativePriority(ThreadPriority::HIGHEST); + const int userMaxPrio = GetUserMaxPriority(maxPrio); + + // if the user does not have an entry in limits.conf the following + // call will fail + if (userMaxPrio > 0) + { + // start thread with nice level of application + const int appNice = getpriority(PRIO_PROCESS, getpid()); + if (setpriority(PRIO_PROCESS, m_threadID, appNice) != 0) + CLog::Log(LOGERROR, "[threads] failed to set priority: {}", strerror(errno)); + } +} + +bool CThreadImplLinux::SetPriority(const ThreadPriority& priority) +{ + // keep priority in bounds + const int prio = ThreadPriorityToNativePriority(priority); + const int maxPrio = ThreadPriorityToNativePriority(ThreadPriority::HIGHEST); + const int minPrio = ThreadPriorityToNativePriority(ThreadPriority::LOWEST); + + // get user max prio given max prio (will take the min) + const int userMaxPrio = GetUserMaxPriority(maxPrio); + + // clamp to min and max priorities + const int adjustedPrio = std::clamp(prio, minPrio, userMaxPrio); + + // nice level of application + const int appNice = getpriority(PRIO_PROCESS, getpid()); + const int newNice = appNice - adjustedPrio; + + if (setpriority(PRIO_PROCESS, m_threadID, newNice) != 0) + { + CLog::Log(LOGERROR, "[threads] failed to set priority: {}", strerror(errno)); + return false; + } + + return true; +} diff --git a/xbmc/platform/linux/threads/ThreadImplLinux.h b/xbmc/platform/linux/threads/ThreadImplLinux.h new file mode 100644 index 0000000..986ffe5 --- /dev/null +++ b/xbmc/platform/linux/threads/ThreadImplLinux.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/IThreadImpl.h" + +class CThreadImplLinux : public IThreadImpl +{ +public: + CThreadImplLinux(std::thread::native_handle_type handle); + + ~CThreadImplLinux() override = default; + + void SetThreadInfo(const std::string& name) override; + + bool SetPriority(const ThreadPriority& priority) override; + +private: + pid_t m_threadID; +}; |