From 82066946e5f72f30f1256a577cd7dfd01f42502c Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 25 Nov 2021 22:08:26 -0800 Subject: [PATCH] [wpiutil] Add mDNS resolver and announcer (#3733) --- CMakeLists.txt | 6 + wpiutil/.styleguide | 1 + wpiutil/CMakeLists.txt | 7 + wpiutil/build.gradle | 20 ++ .../first/util/MulticastServiceAnnouncer.java | 45 +++ .../first/util/MulticastServiceResolver.java | 44 +++ .../java/edu/wpi/first/util/ServiceData.java | 66 +++++ .../java/edu/wpi/first/util/WPIUtilJNI.java | 25 ++ .../native/cpp/MulticastHandleManager.cpp | 12 + .../main/native/cpp/MulticastHandleManager.h | 25 ++ .../native/cpp/MulticastServiceAnnouncer.cpp | 67 +++++ .../native/cpp/MulticastServiceResolver.cpp | 134 +++++++++ .../src/main/native/cpp/jni/WPIUtilJNI.cpp | 246 ++++++++++++++++ .../include/wpi/MulticastServiceAnnouncer.h | 61 ++++ .../include/wpi/MulticastServiceResolver.h | 87 ++++++ .../src/main/native/include/wpi/jni_util.h | 22 ++ wpiutil/src/main/native/linux/AvahiClient.cpp | 115 ++++++++ wpiutil/src/main/native/linux/AvahiClient.h | 249 ++++++++++++++++ .../linux/MulticastServiceAnnouncer.cpp | 154 ++++++++++ .../native/linux/MulticastServiceResolver.cpp | 148 ++++++++++ .../macOS/MulticastServiceAnnouncer.cpp | 86 ++++++ .../native/macOS/MulticastServiceResolver.cpp | 214 ++++++++++++++ .../src/main/native/macOS/ResolverThread.cpp | 109 +++++++ .../src/main/native/macOS/ResolverThread.h | 45 +++ .../src/main/native/windows/DynamicDns.cpp | 41 +++ wpiutil/src/main/native/windows/DynamicDns.h | 59 ++++ .../windows/MulticastServiceAnnouncer.cpp | 267 ++++++++++++++++++ .../windows/MulticastServiceResolver.cpp | 207 ++++++++++++++ 28 files changed, 2562 insertions(+) create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceAnnouncer.java create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceResolver.java create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/ServiceData.java create mode 100644 wpiutil/src/main/native/cpp/MulticastHandleManager.cpp create mode 100644 wpiutil/src/main/native/cpp/MulticastHandleManager.h create mode 100644 wpiutil/src/main/native/cpp/MulticastServiceAnnouncer.cpp create mode 100644 wpiutil/src/main/native/cpp/MulticastServiceResolver.cpp create mode 100644 wpiutil/src/main/native/include/wpi/MulticastServiceAnnouncer.h create mode 100644 wpiutil/src/main/native/include/wpi/MulticastServiceResolver.h create mode 100644 wpiutil/src/main/native/linux/AvahiClient.cpp create mode 100644 wpiutil/src/main/native/linux/AvahiClient.h create mode 100644 wpiutil/src/main/native/linux/MulticastServiceAnnouncer.cpp create mode 100644 wpiutil/src/main/native/linux/MulticastServiceResolver.cpp create mode 100644 wpiutil/src/main/native/macOS/MulticastServiceAnnouncer.cpp create mode 100644 wpiutil/src/main/native/macOS/MulticastServiceResolver.cpp create mode 100644 wpiutil/src/main/native/macOS/ResolverThread.cpp create mode 100644 wpiutil/src/main/native/macOS/ResolverThread.h create mode 100644 wpiutil/src/main/native/windows/DynamicDns.cpp create mode 100644 wpiutil/src/main/native/windows/DynamicDns.h create mode 100644 wpiutil/src/main/native/windows/MulticastServiceAnnouncer.cpp create mode 100644 wpiutil/src/main/native/windows/MulticastServiceResolver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a2b56f4c25..5d79029afc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,11 +6,17 @@ FATAL: In-source builds are not allowed. ") endif() +if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + set(CMAKE_SYSTEM_VERSION 10.0.18362.0 CACHE STRING INTERNAL FORCE) + set(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION 10.0.18362.0 CACHE STRING INTERNAL FORCE) +endif() project(allwpilib) cmake_minimum_required(VERSION 3.3.0) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") +message(STATUS "Platform version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") + set(WPILIB_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}) INCLUDE(CPack) diff --git a/wpiutil/.styleguide b/wpiutil/.styleguide index e8c1f84cef..f0ae0655c3 100644 --- a/wpiutil/.styleguide +++ b/wpiutil/.styleguide @@ -77,6 +77,7 @@ generatedFileExclude { src/main/native/resources/ src/test/native/cpp/llvm/ src/main/native/windows/StackWalker + src/main/native/linux/AvahiClient } licenseUpdateExclude { diff --git a/wpiutil/CMakeLists.txt b/wpiutil/CMakeLists.txt index a152018b3b..a5871bcf50 100644 --- a/wpiutil/CMakeLists.txt +++ b/wpiutil/CMakeLists.txt @@ -88,6 +88,8 @@ GENERATE_RESOURCES(src/main/native/resources generated/main/cpp WPI wpi wpiutil_ file(GLOB_RECURSE wpiutil_native_src src/main/native/cpp/*.cpp) list(REMOVE_ITEM wpiutil_native_src ${wpiutil_jni_src}) file(GLOB_RECURSE wpiutil_unix_src src/main/native/unix/*.cpp) +file(GLOB_RECURSE wpiutil_linux_src src/main/native/linux/*.cpp) +file(GLOB_RECURSE wpiutil_macos_src src/main/native/macOS/*.cpp) file(GLOB_RECURSE wpiutil_windows_src src/main/native/windows/*.cpp) file(GLOB fmtlib_native_src src/main/native/fmtlib/src/*.cpp) @@ -189,6 +191,11 @@ if (MSVC) target_sources(wpiutil PRIVATE ${wpiutil_windows_src}) else () target_sources(wpiutil PRIVATE ${wpiutil_unix_src}) + if (APPLE) + target_sources(wpiutil PRIVATE ${wpiutil_macos_src}) + else() + target_sources(wpiutil PRIVATE ${wpiutil_linux_src}) + endif() endif() target_include_directories(wpiutil PUBLIC diff --git a/wpiutil/build.gradle b/wpiutil/build.gradle index 752e709333..46fe6e6e61 100644 --- a/wpiutil/build.gradle +++ b/wpiutil/build.gradle @@ -129,6 +129,16 @@ ext { srcDirs 'src/main/native/include', 'src/main/native/libuv/include', 'src/main/native/libuv/src' } } + wpiutilmacOSCpp(CppSourceSet) { + source { + srcDirs 'src/main/native/macOS' + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/main/native/include', 'src/main/native/cpp' + include '**/*.h' + } + } } } else { it.sources { @@ -148,6 +158,16 @@ ext { srcDirs 'src/main/native/include', 'src/main/native/libuv/include', 'src/main/native/libuv/src' } } + wpiutilLinuxCpp(CppSourceSet) { + source { + srcDirs 'src/main/native/linux' + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/main/native/include', 'src/main/native/cpp', 'src/main/native/fmtlib/include' + include '**/*.h' + } + } } } } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceAnnouncer.java b/wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceAnnouncer.java new file mode 100644 index 0000000000..1c4b2ac42a --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceAnnouncer.java @@ -0,0 +1,45 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util; + +import java.util.Map; + +/** Class to announce over mDNS that a service is available. */ +public class MulticastServiceAnnouncer implements AutoCloseable { + private final int m_handle; + + /** + * Creates a MulticastServiceAnnouncer. + * + * @param serviceName service name + * @param serviceType service type + * @param port port + * @param txt txt + */ + public MulticastServiceAnnouncer( + String serviceName, String serviceType, int port, Map txt) { + String[] keys = txt.keySet().toArray(String[]::new); + String[] values = txt.values().toArray(String[]::new); + m_handle = + WPIUtilJNI.createMulticastServiceAnnouncer(serviceName, serviceType, port, keys, values); + } + + @Override + public void close() { + WPIUtilJNI.freeMulticastServiceAnnouncer(m_handle); + } + + public void start() { + WPIUtilJNI.startMulticastServiceAnnouncer(m_handle); + } + + public void stop() { + WPIUtilJNI.stopMulticastServiceAnnouncer(m_handle); + } + + public boolean hasImplementation() { + return WPIUtilJNI.getMulticastServiceAnnouncerHasImplementation(m_handle); + } +} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceResolver.java b/wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceResolver.java new file mode 100644 index 0000000000..fda3e52d2f --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/MulticastServiceResolver.java @@ -0,0 +1,44 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util; + +/** Class to resolve a service over mDNS. */ +public class MulticastServiceResolver implements AutoCloseable { + private final int m_handle; + + /** + * Creates a MulticastServiceResolver. + * + * @param serviceType service type to look for + */ + public MulticastServiceResolver(String serviceType) { + m_handle = WPIUtilJNI.createMulticastServiceResolver(serviceType); + } + + @Override + public void close() { + WPIUtilJNI.freeMulticastServiceResolver(m_handle); + } + + public void start() { + WPIUtilJNI.startMulticastServiceResolver(m_handle); + } + + public void stop() { + WPIUtilJNI.stopMulticastServiceResolver(m_handle); + } + + public boolean hasImplementation() { + return WPIUtilJNI.getMulticastServiceResolverHasImplementation(m_handle); + } + + public int getEventHandle() { + return WPIUtilJNI.getMulticastServiceResolverEventHandle(m_handle); + } + + public ServiceData getData() { + return WPIUtilJNI.getMulticastServiceResolverData(m_handle); + } +} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/ServiceData.java b/wpiutil/src/main/java/edu/wpi/first/util/ServiceData.java new file mode 100644 index 0000000000..0e5601010a --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/ServiceData.java @@ -0,0 +1,66 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util; + +import java.util.HashMap; +import java.util.Map; + +/** Service data for MulticastServiceResolver. */ +public class ServiceData { + /** + * Constructs a ServiceData. + * + * @param ipv4Address ipv4Address + * @param port port + * @param serviceName found service name + * @param hostName found host name + * @param keys txt keys + * @param values txt values + */ + public ServiceData( + long ipv4Address, + int port, + String serviceName, + String hostName, + String[] keys, + String[] values) { + this.m_serviceName = serviceName; + this.m_hostName = hostName; + this.m_port = port; + this.m_ipv4Address = ipv4Address; + + m_txt = new HashMap<>(); + + for (int i = 0; i < keys.length; i++) { + m_txt.put(keys[i], values[i]); + } + } + + public Map getTxt() { + return m_txt; + } + + public String getHostName() { + return m_hostName; + } + + public String getServiceName() { + return m_serviceName; + } + + public int getPort() { + return m_port; + } + + public long getIpv4Address() { + return m_ipv4Address; + } + + private final Map m_txt; + private final long m_ipv4Address; + private final int m_port; + private final String m_serviceName; + private final String m_hostName; +} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java b/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java index b7b2f59e14..12318af01a 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java @@ -122,4 +122,29 @@ public final class WPIUtilJNI { */ public static native int[] waitForObjectsTimeout(int[] handles, double timeout) throws InterruptedException; + + public static native int createMulticastServiceAnnouncer( + String serviceName, String serviceType, int port, String[] keys, String[] values); + + public static native void freeMulticastServiceAnnouncer(int handle); + + public static native void startMulticastServiceAnnouncer(int handle); + + public static native void stopMulticastServiceAnnouncer(int handle); + + public static native boolean getMulticastServiceAnnouncerHasImplementation(int handle); + + public static native int createMulticastServiceResolver(String serviceType); + + public static native void freeMulticastServiceResolver(int handle); + + public static native void startMulticastServiceResolver(int handle); + + public static native void stopMulticastServiceResolver(int handle); + + public static native boolean getMulticastServiceResolverHasImplementation(int handle); + + public static native int getMulticastServiceResolverEventHandle(int handle); + + public static native ServiceData getMulticastServiceResolverData(int handle); } diff --git a/wpiutil/src/main/native/cpp/MulticastHandleManager.cpp b/wpiutil/src/main/native/cpp/MulticastHandleManager.cpp new file mode 100644 index 0000000000..d249a1c6a9 --- /dev/null +++ b/wpiutil/src/main/native/cpp/MulticastHandleManager.cpp @@ -0,0 +1,12 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "MulticastHandleManager.h" + +using namespace wpi; + +MulticastHandleManager& wpi::GetMulticastManager() { + static MulticastHandleManager manager; + return manager; +} diff --git a/wpiutil/src/main/native/cpp/MulticastHandleManager.h b/wpiutil/src/main/native/cpp/MulticastHandleManager.h new file mode 100644 index 0000000000..be8d061659 --- /dev/null +++ b/wpiutil/src/main/native/cpp/MulticastHandleManager.h @@ -0,0 +1,25 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include "wpi/DenseMap.h" +#include "wpi/MulticastServiceAnnouncer.h" +#include "wpi/MulticastServiceResolver.h" +#include "wpi/UidVector.h" + +namespace wpi { +struct MulticastHandleManager { + wpi::mutex mutex; + wpi::UidVector handleIds; + wpi::DenseMap> + resolvers; + wpi::DenseMap> + announcers; +}; + +MulticastHandleManager& GetMulticastManager(); +} // namespace wpi diff --git a/wpiutil/src/main/native/cpp/MulticastServiceAnnouncer.cpp b/wpiutil/src/main/native/cpp/MulticastServiceAnnouncer.cpp new file mode 100644 index 0000000000..736a03da0d --- /dev/null +++ b/wpiutil/src/main/native/cpp/MulticastServiceAnnouncer.cpp @@ -0,0 +1,67 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/MulticastServiceAnnouncer.h" + +#include + +#include "MulticastHandleManager.h" + +extern "C" { +WPI_MulticastServiceAnnouncerHandle WPI_CreateMulticastServiceAnnouncer( + const char* serviceName, const char* serviceType, int32_t port, + int32_t txtCount, const char** keys, const char** values) + +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + + wpi::SmallVector, 8> txts; + + for (int32_t i = 0; i < txtCount; i++) { + txts.emplace_back( + std::pair{keys[i], values[i]}); + } + + auto announcer = std::make_unique( + serviceName, serviceType, port, txts); + + size_t index = manager.handleIds.emplace_back(3); + manager.announcers[index] = std::move(announcer); + + return index; +} + +void WPI_FreeMulticastServiceAnnouncer( + WPI_MulticastServiceAnnouncerHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + manager.announcers[handle] = nullptr; + manager.handleIds.erase(handle); +} + +void WPI_StartMulticastServiceAnnouncer( + WPI_MulticastServiceAnnouncerHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& announcer = manager.announcers[handle]; + announcer->Start(); +} + +void WPI_StopMulticastServiceAnnouncer( + WPI_MulticastServiceAnnouncerHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& announcer = manager.announcers[handle]; + announcer->Stop(); +} + +int32_t WPI_GetMulticastServiceAnnouncerHasImplementation( + WPI_MulticastServiceAnnouncerHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& announcer = manager.announcers[handle]; + return announcer->HasImplementation(); +} +} // extern "C" diff --git a/wpiutil/src/main/native/cpp/MulticastServiceResolver.cpp b/wpiutil/src/main/native/cpp/MulticastServiceResolver.cpp new file mode 100644 index 0000000000..00e2513500 --- /dev/null +++ b/wpiutil/src/main/native/cpp/MulticastServiceResolver.cpp @@ -0,0 +1,134 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/MulticastServiceResolver.h" + +#include "MulticastHandleManager.h" +#include "wpi/MemAlloc.h" + +extern "C" { +WPI_MulticastServiceResolverHandle WPI_CreateMulticastServiceResolver( + const char* serviceType) + +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + + auto resolver = std::make_unique(serviceType); + + size_t index = manager.handleIds.emplace_back(2); + manager.resolvers[index] = std::move(resolver); + + return index; +} + +void WPI_FreeMulticastServiceResolver( + WPI_MulticastServiceResolverHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + manager.resolvers[handle] = nullptr; + manager.handleIds.erase(handle); +} + +void WPI_StartMulticastServiceResolver( + WPI_MulticastServiceResolverHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + resolver->Start(); +} + +void WPI_StopMulticastServiceResolver( + WPI_MulticastServiceResolverHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + resolver->Stop(); +} + +int32_t WPI_GetMulticastServiceResolverHasImplementation( + WPI_MulticastServiceResolverHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + return resolver->HasImplementation(); +} + +WPI_EventHandle WPI_GetMulticastServiceResolverEventHandle( + WPI_MulticastServiceResolverHandle handle) { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + return resolver->GetEventHandle(); +} + +WPI_ServiceData* WPI_GetMulticastServiceResolverData( + WPI_MulticastServiceResolverHandle handle) { + wpi::MulticastServiceResolver::ServiceData data; + { + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + data = resolver->GetData(); + } + size_t allocSize = sizeof(WPI_ServiceData); + // Include space for hostName and serviceType (+ terminators) + allocSize += data.hostName.size() + data.serviceName.size() + 2; + + size_t keysTotalLength = 0; + size_t valuesTotalLength = 0; + // Include space for all keys and values, and pointer array + for (auto&& t : data.txt) { + allocSize += sizeof(const char*); + keysTotalLength += (t.first.size() + 1); + allocSize += sizeof(const char*); + valuesTotalLength += (t.second.size() + 1); + } + allocSize += keysTotalLength; + allocSize += valuesTotalLength; + uint8_t* cDataRaw = reinterpret_cast(wpi::safe_malloc(allocSize)); + if (!cDataRaw) { + return nullptr; + } + WPI_ServiceData* cData = reinterpret_cast(cDataRaw); + cData->ipv4Address = data.ipv4Address; + cData->port = data.port; + cData->txtCount = data.txt.size(); + cDataRaw += sizeof(WPI_ServiceData); + + std::memcpy(cDataRaw, data.hostName.c_str(), data.hostName.size() + 1); + cData->hostName = reinterpret_cast(cDataRaw); + cDataRaw += data.hostName.size() + 1; + + std::memcpy(cDataRaw, data.serviceName.c_str(), data.serviceName.size() + 1); + cData->serviceName = reinterpret_cast(cDataRaw); + cDataRaw += data.serviceName.size() + 1; + + char** valuesPtrArr = reinterpret_cast(cDataRaw); + cDataRaw += (sizeof(char**) * data.txt.size()); + char** keysPtrArr = reinterpret_cast(cDataRaw); + cDataRaw += (sizeof(char**) * data.txt.size()); + + cData->txtKeys = const_cast(keysPtrArr); + cData->txtValues = const_cast(valuesPtrArr); + + for (size_t i = 0; i < data.txt.size(); i++) { + keysPtrArr[i] = reinterpret_cast(cDataRaw); + std::memcpy(keysPtrArr[i], data.txt[i].first.c_str(), + data.txt[i].first.size() + 1); + cDataRaw += (data.txt[i].first.size() + 1); + + valuesPtrArr[i] = reinterpret_cast(cDataRaw); + std::memcpy(valuesPtrArr[i], data.txt[i].second.c_str(), + data.txt[i].second.size() + 1); + cDataRaw += (data.txt[i].second.size() + 1); + } + + return cData; +} + +void WPI_FreeServiceData(WPI_ServiceData* serviceData) { + std::free(serviceData); +} +} // extern "C" diff --git a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp index 4fc5f45354..ecd8f06126 100644 --- a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp +++ b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp @@ -4,9 +4,14 @@ #include +#include "../MulticastHandleManager.h" #include "edu_wpi_first_util_WPIUtilJNI.h" +#include "wpi/DenseMap.h" +#include "wpi/MulticastServiceAnnouncer.h" +#include "wpi/MulticastServiceResolver.h" #include "wpi/PortForwarder.h" #include "wpi/Synchronization.h" +#include "wpi/UidVector.h" #include "wpi/jni_util.h" #include "wpi/timestamp.h" @@ -16,6 +21,7 @@ static bool mockTimeEnabled = false; static uint64_t mockNow = 0; static JException interruptedEx; +static JClass serviceDataCls; extern "C" { @@ -30,6 +36,11 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { return JNI_ERR; } + serviceDataCls = JClass{env, "edu/wpi/first/util/ServiceData"}; + if (!serviceDataCls) { + return JNI_ERR; + } + return JNI_VERSION_1_6; } @@ -39,6 +50,7 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) { return; } interruptedEx.free(env); + serviceDataCls.free(env); } /* @@ -273,4 +285,238 @@ Java_edu_wpi_first_util_WPIUtilJNI_waitForObjectsTimeout return MakeJIntArray(env, signaled); } +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: createMulticastServiceAnnouncer + * Signature: (Ljava/lang/String;Ljava/lang/String;I[Ljava/lang/Object;[Ljava/lang/Object;)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_createMulticastServiceAnnouncer + (JNIEnv* env, jclass, jstring serviceName, jstring serviceType, jint port, + jobjectArray keys, jobjectArray values) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + + JStringRef serviceNameRef{env, serviceName}; + JStringRef serviceTypeRef{env, serviceType}; + + size_t keysLen = env->GetArrayLength(keys); + wpi::SmallVector, 8> txtVec; + txtVec.reserve(keysLen); + for (size_t i = 0; i < keysLen; i++) { + JLocal key{ + env, static_cast(env->GetObjectArrayElement(keys, i))}; + JLocal value{ + env, static_cast(env->GetObjectArrayElement(values, i))}; + + txtVec.emplace_back(std::pair{ + JStringRef{env, key}.str(), JStringRef{env, value}.str()}); + } + + auto announcer = std::make_unique( + serviceNameRef.str(), serviceTypeRef.str(), port, txtVec); + + size_t index = manager.handleIds.emplace_back(1); + + manager.announcers[index] = std::move(announcer); + + return static_cast(index); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: freeMulticastServiceAnnouncer + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_freeMulticastServiceAnnouncer + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + manager.announcers[handle] = nullptr; + manager.handleIds.erase(handle); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: startMulticastServiceAnnouncer + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_startMulticastServiceAnnouncer + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& announcer = manager.announcers[handle]; + announcer->Start(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: stopMulticastServiceAnnouncer + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_stopMulticastServiceAnnouncer + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& announcer = manager.announcers[handle]; + announcer->Stop(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: getMulticastServiceAnnouncerHasImplementation + * Signature: (I)Z + */ +JNIEXPORT jboolean JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_getMulticastServiceAnnouncerHasImplementation + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& announcer = manager.announcers[handle]; + return announcer->HasImplementation(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: createMulticastServiceResolver + * Signature: (Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_createMulticastServiceResolver + (JNIEnv* env, jclass, jstring serviceType) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + JStringRef serviceTypeRef{env, serviceType}; + + auto resolver = + std::make_unique(serviceTypeRef.str()); + + size_t index = manager.handleIds.emplace_back(2); + + manager.resolvers[index] = std::move(resolver); + + return static_cast(index); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: freeMulticastServiceResolver + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_freeMulticastServiceResolver + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + manager.resolvers[handle] = nullptr; + manager.handleIds.erase(handle); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: startMulticastServiceResolver + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_startMulticastServiceResolver + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + resolver->Start(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: stopMulticastServiceResolver + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_stopMulticastServiceResolver + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + resolver->Stop(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: getMulticastServiceResolverHasImplementation + * Signature: (I)Z + */ +JNIEXPORT jboolean JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_getMulticastServiceResolverHasImplementation + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + return resolver->HasImplementation(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: getMulticastServiceResolverEventHandle + * Signature: (I)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_getMulticastServiceResolverEventHandle + (JNIEnv* env, jclass, jint handle) +{ + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + return resolver->GetEventHandle(); +} + +/* + * Class: edu_wpi_first_util_WPIUtilJNI + * Method: getMulticastServiceResolverData + * Signature: (I)Ljava/lang/Object; + */ +JNIEXPORT jobject JNICALL +Java_edu_wpi_first_util_WPIUtilJNI_getMulticastServiceResolverData + (JNIEnv* env, jclass, jint handle) +{ + static jmethodID constructor = + env->GetMethodID(serviceDataCls, "", + "(JILjava/lang/String;Ljava/lang/String;[Ljava/lang/" + "String;[Ljava/lang/String;)V"); + auto& manager = wpi::GetMulticastManager(); + std::scoped_lock lock{manager.mutex}; + auto& resolver = manager.resolvers[handle]; + auto data = resolver->GetData(); + JLocal serviceName{env, MakeJString(env, data.serviceName)}; + JLocal hostName{env, MakeJString(env, data.hostName)}; + + wpi::SmallVector keysRef; + wpi::SmallVector valuesRef; + + for (auto&& txt : data.txt) { + keysRef.emplace_back(txt.first); + valuesRef.emplace_back(txt.second); + } + + JLocal keys{env, MakeJStringArray(env, keysRef)}; + JLocal values{env, MakeJStringArray(env, valuesRef)}; + + return env->NewObject(serviceDataCls, constructor, + static_cast(data.ipv4Address), + static_cast(data.port), serviceName.obj(), + hostName.obj(), keys.obj(), values.obj()); +} + } // extern "C" diff --git a/wpiutil/src/main/native/include/wpi/MulticastServiceAnnouncer.h b/wpiutil/src/main/native/include/wpi/MulticastServiceAnnouncer.h new file mode 100644 index 0000000000..5d2048dfe9 --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/MulticastServiceAnnouncer.h @@ -0,0 +1,61 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#ifdef __cplusplus +#include +#include +#include +#include + +#include "wpi/span.h" +namespace wpi { +class MulticastServiceAnnouncer { + public: + MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt); + MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt); + ~MulticastServiceAnnouncer() noexcept; + void Start(); + void Stop(); + bool HasImplementation() const; + struct Impl; + + private: + std::unique_ptr pImpl; +}; +} // namespace wpi +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int WPI_MulticastServiceAnnouncerHandle; // NOLINT + +WPI_MulticastServiceAnnouncerHandle WPI_CreateMulticastServiceAnnouncer( + const char* serviceName, const char* serviceType, int32_t port, + int32_t txtCount, const char** keys, const char** values); + +void WPI_FreeMulticastServiceAnnouncer( + WPI_MulticastServiceAnnouncerHandle handle); + +void WPI_StartMulticastServiceAnnouncer( + WPI_MulticastServiceAnnouncerHandle handle); + +void WPI_StopMulticastServiceAnnouncer( + WPI_MulticastServiceAnnouncerHandle handle); + +int32_t WPI_GetMulticastServiceAnnouncerHasImplementation( + WPI_MulticastServiceAnnouncerHandle handle); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/wpiutil/src/main/native/include/wpi/MulticastServiceResolver.h b/wpiutil/src/main/native/include/wpi/MulticastServiceResolver.h new file mode 100644 index 0000000000..ffd95475d1 --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/MulticastServiceResolver.h @@ -0,0 +1,87 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include "wpi/Synchronization.h" + +#ifdef __cplusplus +#include +#include +#include +#include +#include +#include + +#include "wpi/ConcurrentQueue.h" +#include "wpi/span.h" +namespace wpi { +class MulticastServiceResolver { + public: + explicit MulticastServiceResolver(std::string_view serviceType); + ~MulticastServiceResolver() noexcept; + struct ServiceData { + unsigned int ipv4Address; + int port; + std::string serviceName; + std::string hostName; + std::vector> txt; + }; + void Start(); + void Stop(); + WPI_EventHandle GetEventHandle() { return event.GetHandle(); } + ServiceData GetData() { return eventQueue.pop(); } + bool HasImplementation() const; + struct Impl; + + private: + wpi::Event event; + wpi::ConcurrentQueue eventQueue; + std::unique_ptr pImpl; +}; +} // namespace wpi +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int WPI_MulticastServiceResolverHandle; // NOLINT + +WPI_MulticastServiceResolverHandle WPI_CreateMulticastServiceResolver( + const char* serviceType); + +void WPI_FreeMulticastServiceResolver( + WPI_MulticastServiceResolverHandle handle); + +void WPI_StartMulticastServiceResolver( + WPI_MulticastServiceResolverHandle handle); + +void WPI_StopMulticastServiceResolver( + WPI_MulticastServiceResolverHandle handle); + +int32_t WPI_GetMulticastServiceResolverHasImplementation( + WPI_MulticastServiceResolverHandle handle); + +WPI_EventHandle WPI_GetMulticastServiceResolverEventHandle( + WPI_MulticastServiceResolverHandle handle); + +typedef struct WPI_ServiceData { // NOLINT + uint32_t ipv4Address; + int32_t port; + const char* serviceName; + const char* hostName; + int32_t txtCount; + const char** txtKeys; + const char** txtValues; +} WPI_ServiceData; + +WPI_ServiceData* WPI_GetMulticastServiceResolverData( + WPI_MulticastServiceResolverHandle handle); + +void WPI_FreeServiceData(WPI_ServiceData* serviceData); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/wpiutil/src/main/native/include/wpi/jni_util.h b/wpiutil/src/main/native/include/wpi/jni_util.h index 195e37e30b..585c1c635d 100644 --- a/wpiutil/src/main/native/include/wpi/jni_util.h +++ b/wpiutil/src/main/native/include/wpi/jni_util.h @@ -566,6 +566,28 @@ inline jobjectArray MakeJStringArray(JNIEnv* env, span arr) { return jarr; } +/** + * Convert an array of std::string into a jarray of jstring. + * + * @param env JRE environment. + * @param arr Array to convert. + */ +inline jobjectArray MakeJStringArray(JNIEnv* env, span arr) { + static JClass stringCls{env, "java/lang/String"}; + if (!stringCls) { + return nullptr; + } + jobjectArray jarr = env->NewObjectArray(arr.size(), stringCls, nullptr); + if (!jarr) { + return nullptr; + } + for (size_t i = 0; i < arr.size(); ++i) { + JLocal elem{env, MakeJString(env, arr[i])}; + env->SetObjectArrayElement(jarr, i, elem.obj()); + } + return jarr; +} + /** * Generic callback thread implementation. * diff --git a/wpiutil/src/main/native/linux/AvahiClient.cpp b/wpiutil/src/main/native/linux/AvahiClient.cpp new file mode 100644 index 0000000000..5bb9ca061e --- /dev/null +++ b/wpiutil/src/main/native/linux/AvahiClient.cpp @@ -0,0 +1,115 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "AvahiClient.h" + +#include + +#include + +#include "dlfcn.h" + +using namespace wpi; + +#define AvahiFunctionLoad(snake_name) \ + do { \ + snake_name = \ + reinterpret_cast(dlsym(lib, "avahi_" #snake_name)); \ + if (!snake_name) { \ + return; \ + } \ + } while (false) + +AvahiFunctionTable::AvahiFunctionTable() { + void* lib = dlopen("libavahi-common.so.3", RTLD_LAZY); + + valid = false; + + if (lib == nullptr) { + return; + } + + AvahiFunctionLoad(threaded_poll_new); + AvahiFunctionLoad(threaded_poll_free); + AvahiFunctionLoad(threaded_poll_get); + AvahiFunctionLoad(threaded_poll_start); + AvahiFunctionLoad(threaded_poll_stop); + AvahiFunctionLoad(threaded_poll_lock); + AvahiFunctionLoad(threaded_poll_unlock); + AvahiFunctionLoad(string_list_new_from_array); + AvahiFunctionLoad(string_list_free); + AvahiFunctionLoad(unescape_label); + + lib = dlopen("libavahi-client.so.3", RTLD_LAZY); + + if (lib == nullptr) { + return; + } + + AvahiFunctionLoad(client_new); + AvahiFunctionLoad(client_free); + AvahiFunctionLoad(service_browser_new); + AvahiFunctionLoad(service_browser_get_client); + AvahiFunctionLoad(service_browser_free); + AvahiFunctionLoad(service_resolver_new); + AvahiFunctionLoad(service_resolver_free); + AvahiFunctionLoad(entry_group_new); + AvahiFunctionLoad(entry_group_free); + AvahiFunctionLoad(entry_group_add_service_strlst); + AvahiFunctionLoad(entry_group_reset); + AvahiFunctionLoad(entry_group_is_empty); + AvahiFunctionLoad(entry_group_commit); + + valid = true; +} + +AvahiFunctionTable& AvahiFunctionTable::Get() { + static AvahiFunctionTable table; + return table; +} + +static wpi::mutex ThreadLoopLock; +static std::weak_ptr ThreadLoop; + +std::shared_ptr AvahiThread::Get() { + std::scoped_lock lock{ThreadLoopLock}; + auto locked = ThreadLoop.lock(); + if (!locked) { + locked = std::make_unique(private_init{}); + ThreadLoop = locked; + } + return locked; +} + +AvahiThread::AvahiThread(const private_init&) { + if (!table.IsValid()) { + return; + } + + threadedPoll = table.threaded_poll_new(); + table.threaded_poll_start(threadedPoll); +} + +AvahiThread::~AvahiThread() noexcept { + if (!table.IsValid()) { + return; + } + + if (threadedPoll) { + table.threaded_poll_stop(threadedPoll); + table.threaded_poll_free(threadedPoll); + } +} + +void AvahiThread::lock() { + table.threaded_poll_lock(threadedPoll); +} + +void AvahiThread::unlock() { + table.threaded_poll_unlock(threadedPoll); +} + +const AvahiPoll* AvahiThread::GetPoll() const { + return table.threaded_poll_get(threadedPoll); +} diff --git a/wpiutil/src/main/native/linux/AvahiClient.h b/wpiutil/src/main/native/linux/AvahiClient.h new file mode 100644 index 0000000000..d747162541 --- /dev/null +++ b/wpiutil/src/main/native/linux/AvahiClient.h @@ -0,0 +1,249 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include + +/*** + This file is part of avahi. + avahi is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + avahi is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General + Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with avahi; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + USA. +***/ + +typedef struct AvahiPoll AvahiPoll; + +typedef enum { + AVAHI_SERVER_INVALID, + AVAHI_SERVER_REGISTERING, + AVAHI_SERVER_RUNNING, + AVAHI_SERVER_COLLISION, + AVAHI_SERVER_FAILURE +} AvahiServerState; + +typedef struct AvahiClient AvahiClient; + +typedef enum { + AVAHI_CLIENT_S_REGISTERING = AVAHI_SERVER_REGISTERING, + AVAHI_CLIENT_S_RUNNING = AVAHI_SERVER_RUNNING, + AVAHI_CLIENT_S_COLLISION = AVAHI_SERVER_COLLISION, + AVAHI_CLIENT_FAILURE = 100, + AVAHI_CLIENT_CONNECTING = 101 +} AvahiClientState; + +typedef enum { + AVAHI_CLIENT_IGNORE_USER_CONFIG = 1, + AVAHI_CLIENT_NO_FAIL = 2 +} AvahiClientFlags; + +typedef void (*AvahiClientCallback)(AvahiClient* s, AvahiClientState state, + void* userdata); + +typedef struct AvahiServiceBrowser AvahiServiceBrowser; + +typedef int AvahiProtocol; + +typedef int AvahiIfIndex; + +typedef enum { + AVAHI_BROWSER_NEW, + AVAHI_BROWSER_REMOVE, + AVAHI_BROWSER_CACHE_EXHAUSTED, + AVAHI_BROWSER_ALL_FOR_NOW, + AVAHI_BROWSER_FAILURE +} AvahiBrowserEvent; + +typedef enum { + AVAHI_LOOKUP_RESULT_CACHED = 1, + AVAHI_LOOKUP_RESULT_WIDE_AREA = 2, + AVAHI_LOOKUP_RESULT_MULTICAST = 4, + AVAHI_LOOKUP_RESULT_LOCAL = 8, + AVAHI_LOOKUP_RESULT_OUR_OWN = 16, + AVAHI_LOOKUP_RESULT_STATIC = 32 +} AvahiLookupResultFlags; + +typedef void (*AvahiServiceBrowserCallback)( + AvahiServiceBrowser* b, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiBrowserEvent event, const char* name, const char* type, + const char* domain, AvahiLookupResultFlags flags, void* userdata); + +typedef enum { + AVAHI_LOOKUP_USE_WIDE_AREA = 1, + AVAHI_LOOKUP_USE_MULTICAST = 2, + + AVAHI_LOOKUP_NO_TXT = 4, + AVAHI_LOOKUP_NO_ADDRESS = 8 +} AvahiLookupFlags; + +typedef struct AvahiServiceResolver AvahiServiceResolver; + +typedef enum { + AVAHI_RESOLVER_FOUND, + AVAHI_RESOLVER_FAILURE +} AvahiResolverEvent; + +typedef struct AvahiIPv4Address { + uint32_t address; +} AvahiIPv4Address; + +typedef struct AvahiIPv6Address { + uint8_t address[16]; +} AvahiIPv6Address; + +typedef struct AvahiAddress { + AvahiProtocol proto; + + union { + AvahiIPv6Address ipv6; + AvahiIPv4Address ipv4; + uint8_t data[1]; + } data; +} AvahiAddress; + +typedef struct AvahiStringList { + struct AvahiStringList* next; + size_t size; + uint8_t text[1]; +} AvahiStringList; + +typedef void (*AvahiServiceResolverCallback)( + AvahiServiceResolver* r, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiResolverEvent event, const char* name, const char* type, + const char* domain, const char* host_name, const AvahiAddress* a, + uint16_t port, AvahiStringList* txt, AvahiLookupResultFlags flags, + void* userdata); + +typedef struct AvahiThreadedPoll AvahiThreadedPoll; + +typedef struct AvahiEntryGroup AvahiEntryGroup; + +typedef enum { + AVAHI_ENTRY_GROUP_UNCOMMITED, + AVAHI_ENTRY_GROUP_REGISTERING, + AVAHI_ENTRY_GROUP_ESTABLISHED, + AVAHI_ENTRY_GROUP_COLLISION, + AVAHI_ENTRY_GROUP_FAILURE +} AvahiEntryGroupState; + +typedef void (*AvahiEntryGroupCallback)(AvahiEntryGroup* g, + AvahiEntryGroupState state, + void* userdata); + +typedef enum { + AVAHI_PUBLISH_UNIQUE = 1, + AVAHI_PUBLISH_NO_PROBE = 2, + AVAHI_PUBLISH_NO_ANNOUNCE = 4, + AVAHI_PUBLISH_ALLOW_MULTIPLE = 8, + + AVAHI_PUBLISH_NO_REVERSE = 16, + AVAHI_PUBLISH_NO_COOKIE = 32, + AVAHI_PUBLISH_UPDATE = 64, + AVAHI_PUBLISH_USE_WIDE_AREA = 128, + AVAHI_PUBLISH_USE_MULTICAST = 256 +} AvahiPublishFlags; + +enum { AVAHI_IF_UNSPEC = -1 }; + +enum { AVAHI_PROTO_INET = 0, AVAHI_PROTO_INET6 = 1, AVAHI_PROTO_UNSPEC = -1 }; + +namespace wpi { +class AvahiFunctionTable { + public: +#define AvahiFunction(CapName, RetType, Parameters) \ + using CapName##_func = RetType(*) Parameters; \ + CapName##_func CapName = nullptr + + AvahiFunction(threaded_poll_new, AvahiThreadedPoll*, (void)); + AvahiFunction(threaded_poll_free, void, (AvahiThreadedPoll*)); + AvahiFunction(threaded_poll_get, const AvahiPoll*, (AvahiThreadedPoll*)); + AvahiFunction(threaded_poll_start, int, (AvahiThreadedPoll*)); + AvahiFunction(threaded_poll_stop, int, (AvahiThreadedPoll*)); + AvahiFunction(threaded_poll_lock, int, (AvahiThreadedPoll*)); + AvahiFunction(threaded_poll_unlock, int, (AvahiThreadedPoll*)); + + AvahiFunction(client_new, AvahiClient*, + (const AvahiPoll* poll_api, AvahiClientFlags flags, + AvahiClientCallback callback, void* userdata, int* error)); + AvahiFunction(client_free, void, (AvahiClient*)); + + AvahiFunction(service_browser_new, AvahiServiceBrowser*, + (AvahiClient * client, AvahiIfIndex interface, + AvahiProtocol protocol, const char* type, const char* domain, + AvahiLookupFlags flags, AvahiServiceBrowserCallback callback, + void* userdata)); + + AvahiFunction(service_browser_free, int, (AvahiServiceBrowser*)); + + AvahiFunction(service_resolver_new, AvahiServiceResolver*, + (AvahiClient * client, AvahiIfIndex interface, + AvahiProtocol protocol, const char* name, const char* type, + const char* domain, AvahiProtocol aprotocol, + AvahiLookupFlags flags, AvahiServiceResolverCallback callback, + void* userdata)); + AvahiFunction(service_resolver_free, int, (AvahiServiceResolver*)); + + AvahiFunction(entry_group_new, AvahiEntryGroup*, + (AvahiClient*, AvahiEntryGroupCallback, void*)); + AvahiFunction(entry_group_free, int, (AvahiEntryGroup*)); + + AvahiFunction(entry_group_add_service_strlst, int, + (AvahiEntryGroup * group, AvahiIfIndex interface, + AvahiProtocol protocol, AvahiPublishFlags flags, + const char* name, const char* type, const char* domain, + const char* host, uint16_t port, AvahiStringList*)); + + AvahiFunction(entry_group_reset, int, (AvahiEntryGroup*)); + AvahiFunction(entry_group_is_empty, int, (AvahiEntryGroup*)); + AvahiFunction(entry_group_commit, int, (AvahiEntryGroup*)); + + AvahiFunction(string_list_new_from_array, AvahiStringList*, + (const char** array, int len)); + AvahiFunction(string_list_free, void, (AvahiStringList*)); + + AvahiFunction(service_browser_get_client, AvahiClient*, + (AvahiServiceBrowser*)); + + AvahiFunction(unescape_label, char*, (const char**, char*, size_t)); + + bool IsValid() const { return valid; } + + static AvahiFunctionTable& Get(); + + private: + AvahiFunctionTable(); + bool valid; +}; + +class AvahiThread { + private: + struct private_init {}; + + public: + explicit AvahiThread(const private_init&); + ~AvahiThread() noexcept; + + void lock(); + const AvahiPoll* GetPoll() const; + void unlock(); + + static std::shared_ptr Get(); + + private: + AvahiThreadedPoll* threadedPoll; + AvahiFunctionTable& table = AvahiFunctionTable::Get(); +}; + +} // namespace wpi diff --git a/wpiutil/src/main/native/linux/MulticastServiceAnnouncer.cpp b/wpiutil/src/main/native/linux/MulticastServiceAnnouncer.cpp new file mode 100644 index 0000000000..0df050b171 --- /dev/null +++ b/wpiutil/src/main/native/linux/MulticastServiceAnnouncer.cpp @@ -0,0 +1,154 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/MulticastServiceAnnouncer.h" + +#include + +#include "AvahiClient.h" +#include "fmt/format.h" +#include "wpi/mutex.h" + +using namespace wpi; + +struct MulticastServiceAnnouncer::Impl { + AvahiFunctionTable& table = AvahiFunctionTable::Get(); + std::shared_ptr thread = AvahiThread::Get(); + AvahiClient* client; + AvahiEntryGroup* group = nullptr; + std::string serviceName; + std::string serviceType; + int port; + AvahiStringList* stringList = nullptr; + + ~Impl() noexcept { + if (stringList != nullptr && table.IsValid()) { + table.string_list_free(stringList); + } + } +}; + +static void EntryGroupCallback(AvahiEntryGroup*, AvahiEntryGroupState, void*) {} + +static void RegisterService(AvahiClient* client, + MulticastServiceAnnouncer::Impl* impl) { + if (impl->group == nullptr) { + impl->group = + impl->table.entry_group_new(client, EntryGroupCallback, nullptr); + } + + if (impl->table.entry_group_is_empty(impl->group)) { + impl->table.entry_group_add_service_strlst( + impl->group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, + AVAHI_PUBLISH_USE_MULTICAST, impl->serviceName.c_str(), + impl->serviceType.c_str(), "local", nullptr, impl->port, + impl->stringList); + impl->table.entry_group_commit(impl->group); + } +} + +static void ClientCallback(AvahiClient* client, AvahiClientState state, + void* userdata) { + MulticastServiceAnnouncer::Impl* impl = + reinterpret_cast(userdata); + + if (state == AVAHI_CLIENT_S_RUNNING) { + RegisterService(client, impl); + } else if (state == AVAHI_CLIENT_S_COLLISION || + state == AVAHI_CLIENT_S_REGISTERING) { + if (impl->group) { + impl->table.entry_group_reset(impl->group); + } + } +} + +MulticastServiceAnnouncer::MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt) { + pImpl = std::make_unique(); + + if (!pImpl->table.IsValid()) { + return; + } + + pImpl->serviceName = serviceName; + pImpl->serviceType = serviceType; + pImpl->port = port; + + std::vector txts; + for (auto&& i : txt) { + txts.push_back(fmt::format("{}={}", i.first, i.second)); + } + + std::vector txtArr; + for (auto&& i : txts) { + txtArr.push_back(i.c_str()); + } + + pImpl->stringList = + pImpl->table.string_list_new_from_array(txtArr.data(), txtArr.size()); +} + +MulticastServiceAnnouncer::MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt) { + pImpl = std::make_unique(); + + if (!pImpl->table.IsValid()) { + return; + } + + pImpl->serviceName = serviceName; + pImpl->serviceType = serviceType; + pImpl->port = port; + + std::vector txts; + for (auto&& i : txt) { + txts.push_back(fmt::format("{}={}", i.first, i.second)); + } + + std::vector txtArr; + for (auto&& i : txts) { + txtArr.push_back(i.c_str()); + } + + pImpl->stringList = + pImpl->table.string_list_new_from_array(txtArr.data(), txtArr.size()); +} + +MulticastServiceAnnouncer::~MulticastServiceAnnouncer() noexcept { + Stop(); +} + +bool MulticastServiceAnnouncer::HasImplementation() const { + return pImpl->table.IsValid(); +} + +void MulticastServiceAnnouncer::Start() { + if (!pImpl->table.IsValid()) { + return; + } + std::scoped_lock lock{*pImpl->thread}; + if (pImpl->client) { + return; + } + pImpl->client = + pImpl->table.client_new(pImpl->thread->GetPoll(), AVAHI_CLIENT_NO_FAIL, + ClientCallback, pImpl.get(), nullptr); +} + +void MulticastServiceAnnouncer::Stop() { + if (!pImpl->table.IsValid()) { + return; + } + std::scoped_lock lock{*pImpl->thread}; + if (pImpl->client) { + if (pImpl->group) { + pImpl->table.entry_group_free(pImpl->group); + pImpl->group = nullptr; + } + pImpl->table.client_free(pImpl->client); + pImpl->client = nullptr; + } +} diff --git a/wpiutil/src/main/native/linux/MulticastServiceResolver.cpp b/wpiutil/src/main/native/linux/MulticastServiceResolver.cpp new file mode 100644 index 0000000000..eb05916fee --- /dev/null +++ b/wpiutil/src/main/native/linux/MulticastServiceResolver.cpp @@ -0,0 +1,148 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/MulticastServiceResolver.h" + +#include "AvahiClient.h" +#include "wpi/SmallString.h" +#include "wpi/mutex.h" + +using namespace wpi; + +struct MulticastServiceResolver::Impl { + AvahiFunctionTable& table = AvahiFunctionTable::Get(); + std::shared_ptr thread = AvahiThread::Get(); + AvahiClient* client; + AvahiServiceBrowser* browser; + std::string serviceType; + MulticastServiceResolver* resolver; + + void onFound(ServiceData&& data) { + resolver->eventQueue.push(std::move(data)); + resolver->event.Set(); + } +}; + +MulticastServiceResolver::MulticastServiceResolver( + std::string_view serviceType) { + pImpl = std::make_unique(); + pImpl->serviceType = serviceType; + pImpl->resolver = this; +} + +MulticastServiceResolver::~MulticastServiceResolver() noexcept { + Stop(); +} + +bool MulticastServiceResolver::HasImplementation() const { + return pImpl->table.IsValid(); +} + +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, + void* userdata) { + MulticastServiceResolver::Impl* impl = + reinterpret_cast(userdata); + + if (event == AVAHI_RESOLVER_FOUND) { + if (address->proto == AVAHI_PROTO_INET) { + AvahiStringList* strLst = txt; + MulticastServiceResolver::ServiceData data; + while (strLst != nullptr) { + std::string_view value{reinterpret_cast(strLst->text), + strLst->size}; + strLst = strLst->next; + size_t splitIndex = value.find('='); + if (splitIndex == value.npos) { + // Todo make this just do key + continue; + } + std::string_view key = value.substr(0, splitIndex); + value = value.substr(splitIndex + 1, value.size() - splitIndex - 1); + data.txt.emplace_back(std::pair{key, value}); + } + wpi::SmallString<256> outputHostName; + char label[256]; + do { + impl->table.unescape_label(&host_name, label, sizeof(label)); + if (label[0] == '\0') { + break; + } + outputHostName.append(label); + outputHostName.append("."); + } while (true); + + data.ipv4Address = address->data.ipv4.address; + data.port = port; + data.serviceName = name; + data.hostName = outputHostName.string(); + + impl->onFound(std::move(data)); + } + } + + impl->table.service_resolver_free(r); +} + +static void BrowseCallback(AvahiServiceBrowser* b, AvahiIfIndex interface, + AvahiProtocol protocol, AvahiBrowserEvent event, + const char* name, const char* type, + const char* domain, AvahiLookupResultFlags flags, + void* userdata) { + MulticastServiceResolver::Impl* impl = + reinterpret_cast(userdata); + + if (event == AVAHI_BROWSER_NEW) { + impl->table.service_resolver_new( + impl->table.service_browser_get_client(b), interface, protocol, name, + type, domain, AVAHI_PROTO_UNSPEC, AVAHI_LOOKUP_USE_MULTICAST, + ResolveCallback, userdata); + } +} + +static void ClientCallback(AvahiClient* client, AvahiClientState state, + void* userdata) { + MulticastServiceResolver::Impl* impl = + reinterpret_cast(userdata); + + if (state == AVAHI_CLIENT_S_RUNNING) { + impl->browser = impl->table.service_browser_new( + client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, impl->serviceType.c_str(), + "local", AvahiLookupFlags::AVAHI_LOOKUP_USE_MULTICAST, BrowseCallback, + userdata); + } +} + +void MulticastServiceResolver::Start() { + if (!pImpl->table.IsValid()) { + return; + } + std::scoped_lock lock{*pImpl->thread}; + if (pImpl->client) { + return; + } + + pImpl->client = + pImpl->table.client_new(pImpl->thread->GetPoll(), AVAHI_CLIENT_NO_FAIL, + ClientCallback, pImpl.get(), nullptr); +} + +void MulticastServiceResolver::Stop() { + if (!pImpl->table.IsValid()) { + return; + } + std::scoped_lock lock{*pImpl->thread}; + if (pImpl->client) { + if (pImpl->browser) { + pImpl->table.service_browser_free(pImpl->browser); + pImpl->browser = nullptr; + } + pImpl->table.client_free(pImpl->client); + pImpl->client = nullptr; + } +} diff --git a/wpiutil/src/main/native/macOS/MulticastServiceAnnouncer.cpp b/wpiutil/src/main/native/macOS/MulticastServiceAnnouncer.cpp new file mode 100644 index 0000000000..ad2ff48473 --- /dev/null +++ b/wpiutil/src/main/native/macOS/MulticastServiceAnnouncer.cpp @@ -0,0 +1,86 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/MulticastServiceAnnouncer.h" + +#include + +#include "dns_sd.h" + +using namespace wpi; + +struct MulticastServiceAnnouncer::Impl { + std::string serviceName; + std::string serviceType; + int port; + DNSServiceRef serviceRef{nullptr}; + TXTRecordRef txtRecord; + + Impl() { TXTRecordCreate(&txtRecord, 0, nullptr); } + + ~Impl() noexcept { TXTRecordDeallocate(&txtRecord); } +}; + +MulticastServiceAnnouncer::MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt) { + pImpl = std::make_unique(); + pImpl->serviceName = serviceName; + pImpl->serviceType = serviceType; + pImpl->port = port; + + for (auto&& i : txt) { + TXTRecordSetValue(&pImpl->txtRecord, i.first.c_str(), i.second.length(), + i.second.c_str()); + } +} + +MulticastServiceAnnouncer::MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt) { + pImpl = std::make_unique(); + pImpl->serviceName = serviceName; + pImpl->serviceType = serviceType; + pImpl->port = port; + + wpi::SmallString<64> key; + + for (auto&& i : txt) { + key.clear(); + key.append(i.first); + key.emplace_back('\0'); + + TXTRecordSetValue(&pImpl->txtRecord, key.data(), i.second.length(), + i.second.data()); + } +} + +MulticastServiceAnnouncer::~MulticastServiceAnnouncer() noexcept { + Stop(); +} + +bool MulticastServiceAnnouncer::HasImplementation() const { + return true; +} + +void MulticastServiceAnnouncer::Start() { + if (pImpl->serviceRef) { + return; + } + + uint16_t len = TXTRecordGetLength(&pImpl->txtRecord); + const void* ptr = TXTRecordGetBytesPtr(&pImpl->txtRecord); + + (void)DNSServiceRegister(&pImpl->serviceRef, 0, 0, pImpl->serviceName.c_str(), + pImpl->serviceType.c_str(), "local", nullptr, + htons(pImpl->port), len, ptr, nullptr, nullptr); +} + +void MulticastServiceAnnouncer::Stop() { + if (!pImpl->serviceRef) { + return; + } + DNSServiceRefDeallocate(pImpl->serviceRef); + pImpl->serviceRef = nullptr; +} diff --git a/wpiutil/src/main/native/macOS/MulticastServiceResolver.cpp b/wpiutil/src/main/native/macOS/MulticastServiceResolver.cpp new file mode 100644 index 0000000000..f8a3cfabf9 --- /dev/null +++ b/wpiutil/src/main/native/macOS/MulticastServiceResolver.cpp @@ -0,0 +1,214 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "wpi/MulticastServiceResolver.h" + +#include +#include + +#include +#include +#include + +#include "ResolverThread.h" +#include "dns_sd.h" +#include "wpi/SmallVector.h" + +using namespace wpi; + +struct DnsResolveState { + DnsResolveState(MulticastServiceResolver::Impl* impl, + std::string_view serviceNameView) + : pImpl{impl} { + data.serviceName = serviceNameView; + } + + DNSServiceRef ResolveRef = nullptr; + MulticastServiceResolver::Impl* pImpl; + + MulticastServiceResolver::ServiceData data; +}; + +struct MulticastServiceResolver::Impl { + std::string serviceType; + MulticastServiceResolver* resolver; + std::shared_ptr thread = ResolverThread::Get(); + std::vector> ResolveStates; + DNSServiceRef serviceRef = nullptr; + + void onFound(ServiceData&& data) { + resolver->eventQueue.push(std::move(data)); + resolver->event.Set(); + } +}; + +MulticastServiceResolver::MulticastServiceResolver( + std::string_view serviceType) { + pImpl = std::make_unique(); + pImpl->serviceType = serviceType; + pImpl->resolver = this; +} + +MulticastServiceResolver::~MulticastServiceResolver() noexcept { + Stop(); +} + +void ServiceGetAddrInfoReply(DNSServiceRef sdRef, DNSServiceFlags flags, + uint32_t interfaceIndex, + DNSServiceErrorType errorCode, + const char* hostname, + const struct sockaddr* address, uint32_t ttl, + void* context) { + if (errorCode != kDNSServiceErr_NoError) { + return; + } + + DnsResolveState* resolveState = static_cast(context); + + resolveState->data.hostName = hostname; + resolveState->data.ipv4Address = + reinterpret_cast(address)->sin_addr.s_addr; + + resolveState->pImpl->onFound(std::move(resolveState->data)); + + resolveState->pImpl->thread->RemoveServiceRefInThread( + resolveState->ResolveRef); + + resolveState->pImpl->ResolveStates.erase(std::find_if( + resolveState->pImpl->ResolveStates.begin(), + resolveState->pImpl->ResolveStates.end(), + [resolveState](auto& a) { return a.get() == resolveState; })); +} + +void ServiceResolveReply(DNSServiceRef sdRef, DNSServiceFlags flags, + uint32_t interfaceIndex, DNSServiceErrorType errorCode, + const char* fullname, const char* hosttarget, + uint16_t port, /* In network byte order */ + uint16_t txtLen, const unsigned char* txtRecord, + void* context) { + if (errorCode != kDNSServiceErr_NoError) { + return; + } + + DnsResolveState* resolveState = static_cast(context); + resolveState->pImpl->thread->RemoveServiceRefInThread( + resolveState->ResolveRef); + DNSServiceRefDeallocate(resolveState->ResolveRef); + resolveState->ResolveRef = nullptr; + resolveState->data.port = ntohs(port); + + int txtCount = TXTRecordGetCount(txtLen, txtRecord); + char keyBuf[256]; + uint8_t valueLen; + const void* value; + + for (int i = 0; i < txtCount; i++) { + errorCode = TXTRecordGetItemAtIndex(txtLen, txtRecord, i, sizeof(keyBuf), + keyBuf, &valueLen, &value); + if (errorCode == kDNSServiceErr_NoError) { + if (valueLen == 0) { + // No value + resolveState->data.txt.emplace_back( + std::pair{std::string{keyBuf}, {}}); + } else { + resolveState->data.txt.emplace_back(std::pair{ + std::string{keyBuf}, + std::string{reinterpret_cast(value), valueLen}}); + } + } + } + + errorCode = DNSServiceGetAddrInfo( + &resolveState->ResolveRef, flags, interfaceIndex, + kDNSServiceProtocol_IPv4, hosttarget, ServiceGetAddrInfoReply, context); + + if (errorCode == kDNSServiceErr_NoError) { + dnssd_sock_t socket = DNSServiceRefSockFD(resolveState->ResolveRef); + resolveState->pImpl->thread->AddServiceRef(resolveState->ResolveRef, + socket); + } else { + resolveState->pImpl->thread->RemoveServiceRefInThread( + resolveState->ResolveRef); + resolveState->pImpl->ResolveStates.erase(std::find_if( + resolveState->pImpl->ResolveStates.begin(), + resolveState->pImpl->ResolveStates.end(), + [resolveState](auto& a) { return a.get() == resolveState; })); + } +} + +static void DnsCompletion(DNSServiceRef sdRef, DNSServiceFlags flags, + uint32_t interfaceIndex, + DNSServiceErrorType errorCode, + const char* serviceName, const char* regtype, + const char* replyDomain, void* context) { + if (errorCode != kDNSServiceErr_NoError) { + return; + } + if (!(flags & kDNSServiceFlagsAdd)) { + return; + } + + MulticastServiceResolver::Impl* impl = + static_cast(context); + auto& resolveState = impl->ResolveStates.emplace_back( + std::make_unique(impl, serviceName)); + + errorCode = DNSServiceResolve(&resolveState->ResolveRef, 0, interfaceIndex, + serviceName, regtype, replyDomain, + ServiceResolveReply, resolveState.get()); + + if (errorCode == kDNSServiceErr_NoError) { + dnssd_sock_t socket = DNSServiceRefSockFD(resolveState->ResolveRef); + resolveState->pImpl->thread->AddServiceRef(resolveState->ResolveRef, + socket); + } else { + resolveState->pImpl->ResolveStates.erase(std::find_if( + resolveState->pImpl->ResolveStates.begin(), + resolveState->pImpl->ResolveStates.end(), + [r = resolveState.get()](auto& a) { return a.get() == r; })); + } +} + +bool MulticastServiceResolver::HasImplementation() const { + return true; +} + +void MulticastServiceResolver::Start() { + if (pImpl->serviceRef) { + return; + } + + DNSServiceErrorType status = + DNSServiceBrowse(&pImpl->serviceRef, 0, 0, pImpl->serviceType.c_str(), + "local", DnsCompletion, pImpl.get()); + if (status == kDNSServiceErr_NoError) { + dnssd_sock_t socket = DNSServiceRefSockFD(pImpl->serviceRef); + pImpl->thread->AddServiceRef(pImpl->serviceRef, socket); + } +} + +void MulticastServiceResolver::Stop() { + if (!pImpl->serviceRef) { + return; + } + wpi::SmallVector cleanupEvents; + for (auto&& i : pImpl->ResolveStates) { + cleanupEvents.push_back( + pImpl->thread->RemoveServiceRefOutsideThread(i->ResolveRef)); + } + cleanupEvents.push_back( + pImpl->thread->RemoveServiceRefOutsideThread(pImpl->serviceRef)); + wpi::SmallVector signaledBuf; + signaledBuf.resize(cleanupEvents.size()); + while (!cleanupEvents.empty()) { + auto signaled = wpi::WaitForObjects(cleanupEvents, signaledBuf); + for (auto&& s : signaled) { + cleanupEvents.erase( + std::find(cleanupEvents.begin(), cleanupEvents.end(), s)); + } + } + + pImpl->ResolveStates.clear(); + pImpl->serviceRef = nullptr; +} diff --git a/wpiutil/src/main/native/macOS/ResolverThread.cpp b/wpiutil/src/main/native/macOS/ResolverThread.cpp new file mode 100644 index 0000000000..b91d6b69fc --- /dev/null +++ b/wpiutil/src/main/native/macOS/ResolverThread.cpp @@ -0,0 +1,109 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "ResolverThread.h" + +#include "wpi/mutex.h" + +using namespace wpi; + +ResolverThread::ResolverThread(const private_init&) {} + +ResolverThread::~ResolverThread() noexcept { + running = false; + if (thread.joinable()) { + thread.join(); + } +} + +void ResolverThread::AddServiceRef(DNSServiceRef serviceRef, + dnssd_sock_t socket) { + std::scoped_lock lock{serviceRefMutex}; + serviceRefs.emplace_back( + std::pair{serviceRef, socket}); + if (serviceRefs.size() == 1) { + running = false; + if (thread.joinable()) { + thread.join(); + } + running = true; + thread = std::thread([=] { ThreadMain(); }); + } +} + +void ResolverThread::RemoveServiceRefInThread(DNSServiceRef serviceRef) { + std::scoped_lock lock{serviceRefMutex}; + serviceRefs.erase( + std::find_if(serviceRefs.begin(), serviceRefs.end(), + [=](auto& a) { return a.first == serviceRef; })); + DNSServiceRefDeallocate(serviceRef); +} + +WPI_EventHandle ResolverThread::RemoveServiceRefOutsideThread( + DNSServiceRef serviceRef) { + std::scoped_lock lock{serviceRefMutex}; + WPI_EventHandle handle = CreateEvent(true); + serviceRefsToRemove.push_back({serviceRef, handle}); + return handle; +} + +bool ResolverThread::CleanupRefs() { + std::scoped_lock lock{serviceRefMutex}; + for (auto&& r : serviceRefsToRemove) { + serviceRefs.erase( + std::find_if(serviceRefs.begin(), serviceRefs.end(), + [=](auto& a) { return a.first == r.first; })); + DNSServiceRefDeallocate(r.first); + wpi::SetEvent(r.second); + } + serviceRefsToRemove.clear(); + return serviceRefs.empty(); +} + +void ResolverThread::ThreadMain() { + std::vector readSockets; + std::vector serviceRefs; + + while (running) { + readSockets.clear(); + serviceRefs.clear(); + + for (auto&& i : this->serviceRefs) { + readSockets.emplace_back(pollfd{i.second, POLLIN, 0}); + serviceRefs.emplace_back(i.first); + } + + int res = poll(readSockets.begin().base(), readSockets.size(), 100); + + if (res > 0) { + for (size_t i = 0; i < readSockets.size(); i++) { + if (readSockets[i].revents == POLLIN) { + DNSServiceProcessResult(serviceRefs[i]); + } + } + } else if (res == 0) { + if (!running) { + CleanupRefs(); + break; + } + } + + if (CleanupRefs()) { + break; + } + } +} + +static wpi::mutex ThreadLoopLock; +static std::weak_ptr ThreadLoop; + +std::shared_ptr ResolverThread::Get() { + std::scoped_lock lock{ThreadLoopLock}; + auto locked = ThreadLoop.lock(); + if (!locked) { + locked = std::make_unique(private_init{}); + ThreadLoop = locked; + } + return locked; +} diff --git a/wpiutil/src/main/native/macOS/ResolverThread.h b/wpiutil/src/main/native/macOS/ResolverThread.h new file mode 100644 index 0000000000..0162de5a67 --- /dev/null +++ b/wpiutil/src/main/native/macOS/ResolverThread.h @@ -0,0 +1,45 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include "dns_sd.h" +#include "wpi/Synchronization.h" +#include "wpi/mutex.h" + +namespace wpi { +class ResolverThread { + private: + struct private_init {}; + + public: + explicit ResolverThread(const private_init&); + ~ResolverThread() noexcept; + + void AddServiceRef(DNSServiceRef serviceRef, dnssd_sock_t socket); + void RemoveServiceRefInThread(DNSServiceRef serviceRef); + WPI_EventHandle RemoveServiceRefOutsideThread(DNSServiceRef serviceRef); + + static std::shared_ptr Get(); + + private: + void ThreadMain(); + bool CleanupRefs(); + + wpi::mutex serviceRefMutex; + std::vector> serviceRefsToRemove; + std::vector> serviceRefs; + std::thread thread; + std::atomic_bool running; +}; +} // namespace wpi diff --git a/wpiutil/src/main/native/windows/DynamicDns.cpp b/wpiutil/src/main/native/windows/DynamicDns.cpp new file mode 100644 index 0000000000..c59d554566 --- /dev/null +++ b/wpiutil/src/main/native/windows/DynamicDns.cpp @@ -0,0 +1,41 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "DynamicDns.h" + +using namespace wpi; + +DynamicDns& DynamicDns::GetDynamicDns() { + static DynamicDns dns; + return dns; +} + +DynamicDns::DynamicDns() { + HMODULE library = LoadLibraryW(L"dnsapi"); + + if (library == nullptr) { + return; + } + + DnsServiceFreeInstancePtr = (DnsServiceFreeInstanceFunc)GetProcAddress( + library, "DnsServiceFreeInstance"); + DnsServiceConstructInstancePtr = + (DnsServiceConstructInstanceFunc)GetProcAddress( + library, "DnsServiceConstructInstance"); + DnsServiceRegisterPtr = + (DnsServiceRegisterFunc)GetProcAddress(library, "DnsServiceRegister"); + DnsServiceDeRegisterPtr = + (DnsServiceDeRegisterFunc)GetProcAddress(library, "DnsServiceDeRegister"); + + CanDnsAnnounce = DnsServiceFreeInstancePtr && + DnsServiceConstructInstancePtr && DnsServiceRegisterPtr && + DnsServiceDeRegisterPtr; + + DnsServiceBrowsePtr = + (DnsServiceBrowseFunc)GetProcAddress(library, "DnsServiceBrowse"); + DnsServiceBrowseCancelPtr = (DnsServiceBrowseCancelFunc)GetProcAddress( + library, "DnsServiceBrowseCancel"); + + CanDnsResolve = DnsServiceBrowsePtr && DnsServiceBrowseCancelPtr; +} diff --git a/wpiutil/src/main/native/windows/DynamicDns.h b/wpiutil/src/main/native/windows/DynamicDns.h new file mode 100644 index 0000000000..c8bee04a5a --- /dev/null +++ b/wpiutil/src/main/native/windows/DynamicDns.h @@ -0,0 +1,59 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#ifndef UNICODE +#define UNICODE +#endif + +#include + +#include + +namespace wpi { +class DynamicDns { + public: + using DnsServiceFreeInstanceFunc = + VOID(WINAPI*)(_In_ PDNS_SERVICE_INSTANCE pInstance); + + using DnsServiceConstructInstanceFunc = PDNS_SERVICE_INSTANCE(WINAPI*)( + _In_ PCWSTR pServiceName, _In_ PCWSTR pHostName, + _In_opt_ PIP4_ADDRESS pIp4, _In_opt_ PIP6_ADDRESS pIp6, _In_ WORD wPort, + _In_ WORD wPriority, _In_ WORD wWeight, _In_ DWORD dwPropertiesCount, + _In_reads_(dwPropertiesCount) PCWSTR* keys, + _In_reads_(dwPropertiesCount) PCWSTR* values); + + using DnsServiceRegisterFunc = + DWORD(WINAPI*)(_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, + _Inout_opt_ PDNS_SERVICE_CANCEL pCancel); + + using DnsServiceDeRegisterFunc = + DWORD(WINAPI*)(_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, + _Inout_opt_ PDNS_SERVICE_CANCEL pCancel); + + using DnsServiceBrowseFunc = + DNS_STATUS(WINAPI*)(_In_ PDNS_SERVICE_BROWSE_REQUEST pRequest, + _Inout_ PDNS_SERVICE_CANCEL pCancel); + + using DnsServiceBrowseCancelFunc = + DNS_STATUS(WINAPI*)(_In_ PDNS_SERVICE_CANCEL pCancelHandle); + + DnsServiceBrowseFunc DnsServiceBrowsePtr{nullptr}; + DnsServiceBrowseCancelFunc DnsServiceBrowseCancelPtr{nullptr}; + + DnsServiceFreeInstanceFunc DnsServiceFreeInstancePtr{nullptr}; + DnsServiceConstructInstanceFunc DnsServiceConstructInstancePtr{nullptr}; + DnsServiceRegisterFunc DnsServiceRegisterPtr{nullptr}; + DnsServiceDeRegisterFunc DnsServiceDeRegisterPtr{nullptr}; + + bool CanDnsAnnounce{false}; + bool CanDnsResolve{false}; + + static DynamicDns& GetDynamicDns(); + + private: + DynamicDns(); +}; +} // namespace wpi diff --git a/wpiutil/src/main/native/windows/MulticastServiceAnnouncer.cpp b/wpiutil/src/main/native/windows/MulticastServiceAnnouncer.cpp new file mode 100644 index 0000000000..b24c9b7028 --- /dev/null +++ b/wpiutil/src/main/native/windows/MulticastServiceAnnouncer.cpp @@ -0,0 +1,267 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#ifndef UNICODE +#define UNICODE +#endif + +#include "wpi/MulticastServiceAnnouncer.h" + +#include +#include + +#include "DynamicDns.h" +#include "wpi/ConvertUTF.h" +#include "wpi/SmallString.h" +#include "wpi/SmallVector.h" +#include "wpi/StringExtras.h" +#include "wpi/hostname.h" + +using namespace wpi; + +struct ImplBase { + wpi::DynamicDns& dynamicDns = wpi::DynamicDns::GetDynamicDns(); + PDNS_SERVICE_INSTANCE serviceInstance = nullptr; + HANDLE event = nullptr; +}; + +struct MulticastServiceAnnouncer::Impl : ImplBase { + std::wstring serviceType; + std::wstring serviceInstanceName; + std::wstring hostName; + int port; + std::vector keys; + std::vector keyPtrs; + std::vector values; + std::vector valuePtrs; +}; + +MulticastServiceAnnouncer::MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt) { + pImpl = std::make_unique(); + + if (!pImpl->dynamicDns.CanDnsAnnounce) { + return; + } + + pImpl->port = port; + + wpi::SmallVector wideStorage; + std::string hostName = wpi::GetHostname() + ".local"; + + for (auto&& i : txt) { + wideStorage.clear(); + wpi::convertUTF8ToUTF16String(i.first, wideStorage); + pImpl->keys.emplace_back( + std::wstring{reinterpret_cast(wideStorage.data()), + wideStorage.size()}); + wideStorage.clear(); + wpi::convertUTF8ToUTF16String(i.second, wideStorage); + pImpl->values.emplace_back( + std::wstring{reinterpret_cast(wideStorage.data()), + wideStorage.size()}); + } + + for (size_t i = 0; i < pImpl->keys.size(); i++) { + pImpl->keyPtrs.emplace_back(pImpl->keys[i].c_str()); + pImpl->valuePtrs.emplace_back(pImpl->values[i].c_str()); + } + + wpi::SmallString<128> storage; + + wideStorage.clear(); + wpi::convertUTF8ToUTF16String(hostName, wideStorage); + + pImpl->hostName = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; + + wideStorage.clear(); + if (wpi::ends_with_lower(serviceType, ".local")) { + wpi::convertUTF8ToUTF16String(serviceType, wideStorage); + } else { + storage.clear(); + storage.append(serviceType); + storage.append(".local"); + wpi::convertUTF8ToUTF16String(storage.str(), wideStorage); + } + pImpl->serviceType = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; + + wideStorage.clear(); + storage.clear(); + storage.append(serviceName); + storage.append("."); + storage.append(serviceType); + if (!wpi::ends_with_lower(serviceType, ".local")) { + storage.append(".local"); + } + + wpi::convertUTF8ToUTF16String(storage.str(), wideStorage); + pImpl->serviceInstanceName = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; +} + +MulticastServiceAnnouncer::MulticastServiceAnnouncer( + std::string_view serviceName, std::string_view serviceType, int port, + wpi::span> txt) { + pImpl = std::make_unique(); + + if (!pImpl->dynamicDns.CanDnsAnnounce) { + return; + } + + pImpl->port = port; + + wpi::SmallVector wideStorage; + std::string hostName = wpi::GetHostname() + ".local"; + + for (auto&& i : txt) { + wideStorage.clear(); + wpi::convertUTF8ToUTF16String(i.first, wideStorage); + pImpl->keys.emplace_back( + std::wstring{reinterpret_cast(wideStorage.data()), + wideStorage.size()}); + wideStorage.clear(); + wpi::convertUTF8ToUTF16String(i.second, wideStorage); + pImpl->values.emplace_back( + std::wstring{reinterpret_cast(wideStorage.data()), + wideStorage.size()}); + } + + for (size_t i = 0; i < pImpl->keys.size(); i++) { + pImpl->keyPtrs.emplace_back(pImpl->keys[i].c_str()); + pImpl->valuePtrs.emplace_back(pImpl->values[i].c_str()); + } + + wpi::SmallString<128> storage; + + wideStorage.clear(); + wpi::convertUTF8ToUTF16String(hostName, wideStorage); + + pImpl->hostName = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; + + wideStorage.clear(); + if (wpi::ends_with_lower(serviceType, ".local")) { + wpi::convertUTF8ToUTF16String(serviceType, wideStorage); + } else { + storage.clear(); + storage.append(serviceType); + storage.append(".local"); + wpi::convertUTF8ToUTF16String(storage.str(), wideStorage); + } + pImpl->serviceType = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; + + wideStorage.clear(); + storage.clear(); + storage.append(serviceName); + storage.append("."); + storage.append(serviceType); + if (!wpi::ends_with_lower(serviceType, ".local")) { + storage.append(".local"); + } + + wpi::convertUTF8ToUTF16String(storage.str(), wideStorage); + pImpl->serviceInstanceName = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; +} + +MulticastServiceAnnouncer::~MulticastServiceAnnouncer() noexcept { + Stop(); +} + +bool MulticastServiceAnnouncer::HasImplementation() const { + return pImpl->dynamicDns.CanDnsAnnounce; +} + +static void WINAPI DnsServiceRegisterCallback(DWORD /*Status*/, + PVOID pQueryContext, + PDNS_SERVICE_INSTANCE pInstance) { + ImplBase* impl = reinterpret_cast(pQueryContext); + + impl->serviceInstance = pInstance; + + SetEvent(impl->event); +} + +void MulticastServiceAnnouncer::Start() { + if (pImpl->serviceInstance) { + return; + } + + if (!pImpl->dynamicDns.CanDnsAnnounce) { + return; + } + + PDNS_SERVICE_INSTANCE serviceInst = + pImpl->dynamicDns.DnsServiceConstructInstancePtr( + pImpl->serviceInstanceName.c_str(), pImpl->hostName.c_str(), nullptr, + nullptr, pImpl->port, 0, 0, static_cast(pImpl->keyPtrs.size()), + pImpl->keyPtrs.data(), pImpl->valuePtrs.data()); + if (serviceInst == nullptr) { + return; + } + + DNS_SERVICE_REGISTER_REQUEST registerRequest = {}; + registerRequest.pQueryContext = static_cast(pImpl.get()); + registerRequest.pRegisterCompletionCallback = DnsServiceRegisterCallback; + registerRequest.Version = DNS_QUERY_REQUEST_VERSION1; + registerRequest.unicastEnabled = false; + registerRequest.pServiceInstance = serviceInst; + registerRequest.InterfaceIndex = 0; + + pImpl->event = CreateEvent(NULL, true, false, NULL); + + if (pImpl->dynamicDns.DnsServiceRegisterPtr(®isterRequest, nullptr) == + DNS_REQUEST_PENDING) { + WaitForSingleObject(pImpl->event, INFINITE); + } + + pImpl->dynamicDns.DnsServiceFreeInstancePtr(serviceInst); + CloseHandle(pImpl->event); + pImpl->event = nullptr; +} + +static void WINAPI DnsServiceDeRegisterCallback( + DWORD /*Status*/, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { + ImplBase* impl = reinterpret_cast(pQueryContext); + + if (pInstance != nullptr) { + impl->dynamicDns.DnsServiceFreeInstancePtr(pInstance); + pInstance = nullptr; + } + + SetEvent(impl->event); +} + +void MulticastServiceAnnouncer::Stop() { + if (!pImpl->dynamicDns.CanDnsAnnounce) { + return; + } + + if (pImpl->serviceInstance == nullptr) { + return; + } + + pImpl->event = CreateEvent(NULL, true, false, NULL); + DNS_SERVICE_REGISTER_REQUEST registerRequest = {}; + registerRequest.pQueryContext = static_cast(pImpl.get()); + registerRequest.pRegisterCompletionCallback = DnsServiceDeRegisterCallback; + registerRequest.Version = DNS_QUERY_REQUEST_VERSION1; + registerRequest.unicastEnabled = false; + registerRequest.pServiceInstance = pImpl->serviceInstance; + registerRequest.InterfaceIndex = 0; + + if (pImpl->dynamicDns.DnsServiceDeRegisterPtr(®isterRequest, nullptr) == + DNS_REQUEST_PENDING) { + WaitForSingleObject(pImpl->event, INFINITE); + } + + pImpl->dynamicDns.DnsServiceFreeInstancePtr(pImpl->serviceInstance); + pImpl->serviceInstance = nullptr; + CloseHandle(pImpl->event); + pImpl->event = nullptr; +} diff --git a/wpiutil/src/main/native/windows/MulticastServiceResolver.cpp b/wpiutil/src/main/native/windows/MulticastServiceResolver.cpp new file mode 100644 index 0000000000..40f9b897a4 --- /dev/null +++ b/wpiutil/src/main/native/windows/MulticastServiceResolver.cpp @@ -0,0 +1,207 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#ifndef UNICODE +#define UNICODE +#endif + +#include "wpi/MulticastServiceResolver.h" + +#include + +#include "DynamicDns.h" +#include "wpi/ConvertUTF.h" +#include "wpi/SmallString.h" +#include "wpi/SmallVector.h" +#include "wpi/StringExtras.h" + +#pragma comment(lib, "dnsapi") + +using namespace wpi; + +struct MulticastServiceResolver::Impl { + wpi::DynamicDns& dynamicDns = wpi::DynamicDns::GetDynamicDns(); + std::wstring serviceType; + DNS_SERVICE_CANCEL serviceCancel{nullptr}; + + MulticastServiceResolver* resolver; + + void onFound(ServiceData&& data) { + resolver->eventQueue.push(std::move(data)); + resolver->event.Set(); + } +}; + +MulticastServiceResolver::MulticastServiceResolver( + std::string_view serviceType) { + pImpl = std::make_unique(); + pImpl->resolver = this; + + if (!pImpl->dynamicDns.CanDnsResolve) { + return; + } + + wpi::SmallVector wideStorage; + + if (wpi::ends_with_lower(serviceType, ".local")) { + wpi::convertUTF8ToUTF16String(serviceType, wideStorage); + } else { + wpi::SmallString<128> storage; + storage.append(serviceType); + storage.append(".local"); + wpi::convertUTF8ToUTF16String(storage.str(), wideStorage); + } + pImpl->serviceType = std::wstring{ + reinterpret_cast(wideStorage.data()), wideStorage.size()}; +} + +MulticastServiceResolver::~MulticastServiceResolver() noexcept { + Stop(); +} + +bool MulticastServiceResolver::HasImplementation() const { + return pImpl->dynamicDns.CanDnsResolve; +} + +static _Function_class_(DNS_QUERY_COMPLETION_ROUTINE) VOID WINAPI + DnsCompletion(_In_ PVOID pQueryContext, + _Inout_ PDNS_QUERY_RESULT pQueryResults) { + MulticastServiceResolver::Impl* impl = + reinterpret_cast(pQueryContext); + + wpi::SmallVector PtrRecords; + wpi::SmallVector SrvRecords; + wpi::SmallVector TxtRecords; + wpi::SmallVector ARecords; + + { + DNS_RECORDW* current = pQueryResults->pQueryRecords; + while (current != nullptr) { + switch (current->wType) { + case DNS_TYPE_PTR: + PtrRecords.push_back(current); + break; + case DNS_TYPE_SRV: + SrvRecords.push_back(current); + break; + case DNS_TYPE_TEXT: + TxtRecords.push_back(current); + break; + case DNS_TYPE_A: + ARecords.push_back(current); + break; + } + current = current->pNext; + } + } + + for (DNS_RECORDW* Ptr : PtrRecords) { + if (std::wstring_view{Ptr->pName} != impl->serviceType) { + continue; + } + + std::wstring_view nameHost = Ptr->Data.Ptr.pNameHost; + DNS_RECORDW* foundSrv = nullptr; + for (DNS_RECORDW* Srv : SrvRecords) { + if (std::wstring_view{Srv->pName} == nameHost) { + foundSrv = Srv; + break; + } + } + + if (!foundSrv) { + continue; + } + + for (DNS_RECORDW* A : ARecords) { + if (std::wstring_view{A->pName} == + std::wstring_view{foundSrv->Data.Srv.pNameTarget}) { + MulticastServiceResolver::ServiceData data; + wpi::SmallString<128> storage; + for (DNS_RECORDW* Txt : TxtRecords) { + if (std::wstring_view{Txt->pName} == nameHost) { + for (DWORD i = 0; i < Txt->Data.Txt.dwStringCount; i++) { + std::wstring_view wideView = Txt->Data.TXT.pStringArray[i]; + size_t splitIndex = wideView.find(L'='); + if (splitIndex == wideView.npos) { + // Todo make this just do key + continue; + } + storage.clear(); + wpi::span wideStr{ + reinterpret_cast(wideView.data()), + splitIndex}; + wpi::convertUTF16ToUTF8String(wideStr, storage); + auto& pair = data.txt.emplace_back( + std::pair{storage.string(), {}}); + storage.clear(); + wideStr = wpi::span{ + reinterpret_cast(wideView.data() + + splitIndex + 1), + wideView.size() - splitIndex - 1}; + wpi::convertUTF16ToUTF8String(wideStr, storage); + pair.second = storage.string(); + } + } + } + + storage.clear(); + wpi::span wideHostName{ + reinterpret_cast(A->pName), wcslen(A->pName)}; + wpi::convertUTF16ToUTF8String(wideHostName, storage); + storage.append("."); + + data.hostName = storage.string(); + storage.clear(); + + int len = nameHost.find(impl->serviceType.c_str()); + wpi::span wideServiceName{ + reinterpret_cast(nameHost.data()), + nameHost.size()}; + if (len != nameHost.npos) { + wideServiceName = wideServiceName.subspan(0, len - 1); + } + wpi::convertUTF16ToUTF8String(wideServiceName, storage); + + data.serviceName = storage.string(); + data.port = foundSrv->Data.Srv.wPort; + data.ipv4Address = A->Data.A.IpAddress; + + impl->onFound(std::move(data)); + } + } + } + DnsFree(pQueryResults->pQueryRecords, DNS_FREE_TYPE::DnsFreeRecordList); +} + +void MulticastServiceResolver::Start() { + if (pImpl->serviceCancel.reserved != nullptr) { + return; + } + + if (!pImpl->dynamicDns.CanDnsResolve) { + return; + } + + DNS_SERVICE_BROWSE_REQUEST request = {}; + request.InterfaceIndex = 0; + request.pQueryContext = pImpl.get(); + request.QueryName = pImpl->serviceType.c_str(); + request.Version = 2; + request.pBrowseCallbackV2 = DnsCompletion; + pImpl->dynamicDns.DnsServiceBrowsePtr(&request, &pImpl->serviceCancel); +} + +void MulticastServiceResolver::Stop() { + if (!pImpl->dynamicDns.CanDnsResolve) { + return; + } + + if (pImpl->serviceCancel.reserved == nullptr) { + return; + } + + pImpl->dynamicDns.DnsServiceBrowseCancelPtr(&pImpl->serviceCancel); + pImpl->serviceCancel.reserved = nullptr; +}