From 46578d144cb3deefb50ac4af05f552e10afa6b41 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Wed, 13 Dec 2023 00:33:56 +0100 Subject: [PATCH] exception handling, delayed init Signed-off-by: Holger Friedrich --- .../discovery/addon/ip/IpAddonFinder.java | 221 ++++++++++++------ .../main/resources/OH-INF/config/addons.xml | 2 +- .../resources/OH-INF/i18n/addons.properties | 2 +- 3 files changed, 151 insertions(+), 74 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index ab6175074c9..9648876d08b 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -28,6 +28,7 @@ import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; +import java.text.ParseException; import java.util.HashSet; import java.util.HexFormat; import java.util.Iterator; @@ -36,23 +37,30 @@ import java.util.Objects; import java.util.Set; import java.util.StringTokenizer; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.addon.AddonDiscoveryMethod; import org.openhab.core.addon.AddonInfo; +import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.config.discovery.addon.AddonFinder; import org.openhab.core.config.discovery.addon.BaseAddonFinder; import org.openhab.core.net.NetUtil; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a {@link IpAddonFinder} for finding suggested add-ons by sending IP packets to the * network and collecting responses. + * + * @implNote On activation, a thread is spawned which handles the detection. Scan runs once, + * no continuous background scanning. * * @author Holger Friedrich - Initial contribution */ @@ -64,112 +72,181 @@ public class IpAddonFinder extends BaseAddonFinder { public static final String SERVICE_NAME = SERVICE_NAME_IP; private final Logger logger = LoggerFactory.getLogger(IpAddonFinder.class); + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(SERVICE_NAME); + private @Nullable Future scanJob = null; + Set suggestions = new HashSet<>(); @Activate public IpAddonFinder() { logger.warn("IpAddonFinder::IpAddonFinder"); } - Set scan() { - Set result = new HashSet<>(); + @Deactivate + public void deactivate() { + logger.warn("IpAddonFinder::deactivate"); + stopScan(); + } + + void startScan() { + scanJob = scheduler.schedule(this::scan, 1000, null); + } + + void stopScan() { + Future tmpScanJob = scanJob; + if (tmpScanJob != null) { + if (!tmpScanJob.isDone()) { + logger.trace("Trying to cancel IP scan"); + tmpScanJob.cancel(true); + try { + Thread.sleep(1000); + } catch (InterruptedException ignore) { + } + } + scanJob = null; + } + } + + void scan() { + logger.warn("IpAddonFinder::scan"); for (AddonInfo candidate : addonCandidates) { for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream() .filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) { + logger.trace("Checking candidate: {}", candidate.getUID()); + Map matchProperties = method.getMatchProperties().stream() .collect(Collectors.toMap(property -> property.getName(), property -> property.getRegex())); - String type = matchProperties.get("type"); - String request = matchProperties.get("request"); - int timeoutMs = Integer.parseInt(Objects.toString(matchProperties.get("timeout_ms"))); + // parse standard set op parameters: + String type = Objects.toString(matchProperties.get("type"), ""); + String request = Objects.toString(matchProperties.get("request"), ""); + String response = Objects.toString(matchProperties.get("response"), ""); + int timeoutMs = 0; + try { + timeoutMs = Integer.parseInt(Objects.toString(matchProperties.get("timeout_ms"))); + } catch (NumberFormatException e) { + logger.info("{}: match-property timeout_ms cannot be parsed", candidate.getUID()); + continue; + } @Nullable InetAddress destIp = null; try { destIp = InetAddress.getByName(matchProperties.get("dest_ip")); } catch (UnknownHostException e) { - // TODO Auto-generated catch block - + logger.info("{}: match-property dest_ip cannot be parsed", candidate.getUID()); + continue; } - int destPort = Integer.parseInt(Objects.toString(matchProperties.get("dest_port"))); - - if ("ip_multicast".equals(type)) { - - List ipAddresses = NetUtil.getAllInterfaceAddresses().stream() - .filter(a -> a.getAddress() instanceof Inet4Address) - .map(a -> a.getAddress().getHostAddress()).toList(); - - for (String localIp : ipAddresses) { - try { - - DatagramChannel channel = (DatagramChannel) DatagramChannel - .open(StandardProtocolFamily.INET) - .setOption(StandardSocketOptions.SO_REUSEADDR, true) - .bind(new InetSocketAddress(localIp, 0)) - .setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64).configureBlocking(false); - InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress(); - - ByteArrayOutputStream requestFrame = new ByteArrayOutputStream(); - StringTokenizer parts = new StringTokenizer(request); - - while (parts.hasMoreTokens()) { - String token = parts.nextToken(); - if (token.startsWith("$")) { - switch (token) { - case "$src_ip": - byte[] adr = sock.getAddress().getAddress(); - requestFrame.write(adr); - break; - case "$src_port": - int dPort = sock.getPort(); - requestFrame.write((byte) ((dPort >> 8) & 0xff)); - requestFrame.write((byte) (dPort & 0xff)); + int destPort = 0; + try { + destPort = Integer.parseInt(Objects.toString(matchProperties.get("dest_port"))); + } catch (NumberFormatException e) { + logger.info("{}: match-property dest_port cannot be parsed", candidate.getUID()); + continue; + } + + // + // handle known types + // + try { + switch (Objects.toString(type)) { + case "ip_multicast": + List ipAddresses = NetUtil.getAllInterfaceAddresses().stream() + .filter(a -> a.getAddress() instanceof Inet4Address) + .map(a -> a.getAddress().getHostAddress()).toList(); + + for (String localIp : ipAddresses) { + try { + DatagramChannel channel = (DatagramChannel) DatagramChannel + .open(StandardProtocolFamily.INET) + .setOption(StandardSocketOptions.SO_REUSEADDR, true) + .bind(new InetSocketAddress(localIp, 0)) + .setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64) + .configureBlocking(false); + + byte[] requestArray = buildRequestArray(channel, Objects.toString(request)); + logger.trace("{}: {}", candidate.getUID(), + HexFormat.of().withDelimiter(" ").formatHex(requestArray)); + + channel.send(ByteBuffer.wrap(requestArray), + new InetSocketAddress(destIp, destPort)); + + // listen to responses + Selector selector = Selector.open(); + ByteBuffer buffer = ByteBuffer.wrap(new byte[50]); + channel.register(selector, SelectionKey.OP_READ); + selector.select(timeoutMs); + Iterator it = selector.selectedKeys().iterator(); + + switch (Objects.toString(response)) { + case "any": + if (it.hasNext()) { + final SocketAddress source = ((DatagramChannel) it.next().channel()) + .receive(buffer); + logger.debug("Received return frame from {}", + ((InetSocketAddress) source).getAddress().getHostAddress()); + suggestions.add(candidate); + logger.debug("Suggested add-on found: {}", candidate.getUID()); + } else { + logger.trace("{}: no response", candidate.getUID()); + } break; default: - logger.warn("unknown token"); + logger.info("{}: match-property response \"{}\" is unknown", + candidate.getUID(), type); + break; // end loop } - } else { - int i = Integer.decode(token); - requestFrame.write((byte) i); + + } catch (IOException e) { + logger.debug("{}: network error", candidate.getUID(), e); } } - logger.info("{}", HexFormat.of().withDelimiter(" ").formatHex(requestFrame.toByteArray())); - - channel.send(ByteBuffer.wrap(requestFrame.toByteArray()), - new InetSocketAddress(destIp, destPort)); - - // listen to responses - Selector selector = Selector.open(); - ByteBuffer buffer = ByteBuffer.wrap(new byte[50]); - channel.register(selector, SelectionKey.OP_READ); - selector.select(timeoutMs); - Iterator it = selector.selectedKeys().iterator(); - if (it.hasNext()) { - final SocketAddress source = ((DatagramChannel) it.next().channel()).receive(buffer); - logger.debug("Received return frame from {}", - ((InetSocketAddress) source).getAddress().getHostAddress()); - result.add(candidate); - } else { - logger.debug("no response"); - } + break; - } catch (IOException e) { - logger.trace("KNXnet/IP discovery failed on {}", localIp, e); - } + default: + logger.info("{}: match-property type \"{}\" is unknown", candidate.getUID(), type); } + } catch (ParseException | NumberFormatException none) { + continue; } } } - return result; + } + + byte[] buildRequestArray(DatagramChannel channel, String request) throws java.io.IOException, ParseException { + InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress(); + + ByteArrayOutputStream requestFrame = new ByteArrayOutputStream(); + StringTokenizer parts = new StringTokenizer(request); + + while (parts.hasMoreTokens()) { + String token = parts.nextToken(); + if (token.startsWith("$")) { + switch (token) { + case "$src_ip": + byte[] adr = sock.getAddress().getAddress(); + requestFrame.write(adr); + break; + case "$src_port": + int dPort = sock.getPort(); + requestFrame.write((byte) ((dPort >> 8) & 0xff)); + requestFrame.write((byte) (dPort & 0xff)); + break; + default: + logger.info("Unknown token in request frame \"{}\"", token); + throw new ParseException(token, 0); + } + } else { + int i = Integer.decode(token); + requestFrame.write((byte) i); + } + } + return requestFrame.toByteArray(); } @Override public Set getSuggestedAddons() { logger.trace("IpAddonFinder::getSuggestedAddons"); - Set result = new HashSet<>(); - - result = scan(); - - return result; + return suggestions; } @Override diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml b/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml index f175329fd41..3b42760d1e6 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml @@ -32,7 +32,7 @@ true - Use IP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. + Use IP network scan to suggest add-ons. true diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties index f4fde0f03d5..c0b1862a19a 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties @@ -3,7 +3,7 @@ system.config.addons.includeIncompatible.description = Some add-on services may system.config.addons.remote.label = Access Remote Repository system.config.addons.remote.description = Defines whether openHAB should access the remote repository for add-on installation. system.config.addons.suggestionFinderIp.label = IP-based Suggestion Finder -system.config.addons.suggestionFinderIp.description = Use IP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. +system.config.addons.suggestionFinderIp.description = Use IP network scan to suggest add-ons. system.config.addons.suggestionFinderMdns.label = mDNS Suggestion Finder system.config.addons.suggestionFinderMdns.description = Use mDNS network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderUpnp.label = UPnP Suggestion Finder