org.openhab.binding.network.internal.PresenceDetection.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.network.internal.PresenceDetection.java

Source

/**
 * Copyright (c) 2010-2017 by the respective copyright holders.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.network.internal;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import org.apache.commons.lang.StringUtils;
import org.openhab.binding.network.internal.dhcp.DHCPListenService;
import org.openhab.binding.network.internal.dhcp.IPRequestReceivedCallback;
import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheAsync;
import org.openhab.binding.network.internal.utils.NetworkUtils;
import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum;
import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link PresenceDetection} handles the connection to the Device
 *
 * @author David Grff, 2017 - Rewritten
 * @author Marc Mettke - Initial contribution
 */
public class PresenceDetection implements IPRequestReceivedCallback {
    public static final double NOT_REACHABLE = -1;
    NetworkUtils networkUtils = new NetworkUtils();
    private Logger logger = LoggerFactory.getLogger(PresenceDetection.class);

    /// Configuration variables
    private boolean useDHCPsniffing = false;
    private ArpPingUtilEnum arpPingMethod = null;
    private String arpPingUtilPath = "arping";
    private IpPingMethodEnum pingMethod = null;
    private boolean iosDevice;
    private Set<Integer> tcpPorts = new HashSet<Integer>();

    private long refreshIntervalInMS = 60000;
    private int timeoutInMS = 5000;
    private long lastSeenInMS;

    private String hostname;

    /// State variables (cannot be final because of test dependency injections)
    ExpiringCacheAsync<PresenceDetectionValue> cache;
    private final PresenceDetectionListener updateListener;
    private ScheduledFuture<?> refreshJob;
    private InetAddress destination;
    ExecutorService executorService;
    private String dhcpState = "off";
    Integer currentCheck = 0;
    int detectionChecks;

    public PresenceDetection(final PresenceDetectionListener updateListener, int cacheDeviceStateTimeInMS)
            throws IllegalArgumentException {
        this.updateListener = updateListener;
        cache = new ExpiringCacheAsync<PresenceDetectionValue>(cacheDeviceStateTimeInMS, () -> {
            performPresenceDetection(false);
        });
    }

    public String getHostname() {
        return hostname;
    }

    public Set<Integer> getServicePorts() {
        return tcpPorts;
    }

    public long getRefreshInterval() {
        return refreshIntervalInMS;
    }

    public int getTimeout() {
        return timeoutInMS;
    }

    public void setHostname(String hostname) throws UnknownHostException {
        this.hostname = hostname;
        this.destination = InetAddress.getByName(hostname);
        if (arpPingMethod != null) {
            if (destination instanceof Inet4Address) {
                setUseArpPing(true, arpPingUtilPath);
            } else {
                arpPingMethod = null;
            }
        }
    }

    public void setServicePorts(Set<Integer> ports) {
        this.tcpPorts = ports;
    }

    public void setUseDhcpSniffing(boolean enable) {
        this.useDHCPsniffing = enable;
    }

    public void setRefreshInterval(long refreshInterval) {
        this.refreshIntervalInMS = refreshInterval;
    }

    public void setTimeout(int timeout) {
        this.timeoutInMS = timeout;
    }

    /**
     * Sets the ping method. This method will perform a feature test. If SYSTEM_PING
     * does not work on this system, JAVA_PING will be used instead.
     *
     * @param useSystemPing Set to true to use a system ping method, false to use java ping and null to disable ICMP
     *            pings.
     */
    public void setUseIcmpPing(Boolean useSystemPing) {
        if (useSystemPing == null) {
            pingMethod = null;
        } else if (useSystemPing) {
            pingMethod = networkUtils.determinePingMethod();
        } else {
            pingMethod = IpPingMethodEnum.JAVA_PING;
        }
    }

    /**
     * Enables or disables ARP pings. Will be automatically disabled if the destination
     * is not an IPv4 address. If the feature test for the native arping utility fails,
     * it will be disabled as well.
     *
     * @param enable Enable or disable ARP ping
     * @param arpPingUtilPath The file path to the utility
     */
    public void setUseArpPing(boolean enable, String arpPingUtilPath) {
        this.arpPingUtilPath = arpPingUtilPath;
        if (!enable || StringUtils.isBlank(arpPingUtilPath)) {
            arpPingMethod = null;
            return;
        } else if (destination == null || !(destination instanceof Inet4Address)) {
            arpPingMethod = null;
            return;
        }
        arpPingMethod = networkUtils.determineNativeARPpingMethod(arpPingUtilPath);
    }

    public ArpPingUtilEnum arpPingMethod() {
        return arpPingMethod;
    }

    public IpPingMethodEnum getPingMethod() {
        return pingMethod;
    }

    public String getDhcpState() {
        return dhcpState;
    }

    /**
     * Return true if the device presence detection is performed for an iOS device
     * like iPhone or iPads. An additional port knock is performed before a ping.
     */
    public boolean isIOSdevice() {
        return iosDevice;
    }

    /**
     * Set to true if the device presence detection should be performed for an iOS device
     * like iPhone or iPads. An additional port knock is performed before a ping.
     */
    public void setIOSDevice(boolean value) {
        iosDevice = value;
    }

    /**
     * Return the last seen value in milliseconds based on {@link System.currentTimeMillis()} or 0 if not seen yet.
     */
    public long getLastSeen() {
        return lastSeenInMS;
    }

    /**
     * Return asynchronously the value of the presence detection as a PresenceDetectionValue.
     *
     * @param callback A callback with the PresenceDetectionValue. The callback may
     *            not happen immediately if the cached value expired, but as soon as a new
     *            discovery took place.
     */
    public void getValue(Consumer<PresenceDetectionValue> callback) {
        cache.getValue(callback);
    }

    public ExecutorService getThreadsFor(int threadCount) {
        return Executors.newFixedThreadPool(threadCount);
    }

    /**
     * Perform a presence detection with ICMP-, ARP ping and
     * TCP connection attempts simultaneously. A fixed thread pool will be created with as many
     * thread as necessary to perform all tests at once.
     *
     * This is a NO-OP, if there is already an ongoing detection or if the cached value
     * is not expired yet.
     *
     * Please be aware of the following restrictions:
     * - ARP pings are only executed on IPv4 addresses.
     * - Non system / Java pings are not recommended at all
     * (not interruptible, useless TCP echo service fall back)
     *
     * @param waitForDetectionToFinish If you want to synchronously wait for the result, set this to true
     * @return Return true if a presence detection is performed and false otherwise.
     */
    public boolean performPresenceDetection(boolean waitForDetectionToFinish) {
        if (executorService != null) {
            logger.debug(
                    "There is already an ongoing presence discovery for {} and a new one was issued by the scheduler! TCP Port {}",
                    hostname, tcpPorts);
            return false;
        }

        if (!cache.isExpired()) {
            return false;
        }

        Set<String> interfaceNames = null;

        currentCheck = 0;
        detectionChecks = tcpPorts.size();
        if (pingMethod != null) {
            detectionChecks += 1;
        }
        if (arpPingMethod != null) {
            interfaceNames = networkUtils.getInterfaceNames();
            detectionChecks += interfaceNames.size();
        }

        if (detectionChecks == 0) {
            return false;
        }

        executorService = getThreadsFor(detectionChecks);

        for (Integer tcpPort : tcpPorts) {
            executorService.execute(() -> {
                Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + String.valueOf(tcpPort));
                performServicePing(tcpPort);
                checkIfFinished();
            });
        }

        // ARP ping for IPv4 addresses. Use an own executor for each network interface
        if (interfaceNames != null) {
            for (final String interfaceName : interfaceNames) {
                executorService.execute(() -> {
                    Thread.currentThread().setName("presenceDetectionARP_" + hostname + " " + interfaceName);
                    performARPping(interfaceName);
                    checkIfFinished();
                });
            }
        }

        // ICMP ping
        if (pingMethod != null) {
            executorService.execute(() -> {
                if (pingMethod != IpPingMethodEnum.JAVA_PING) {
                    Thread.currentThread().setName("presenceDetectionICMP_" + hostname);
                    performSystemPing();
                } else {
                    performJavaPing();
                }
                checkIfFinished();
            });
        }

        if (waitForDetectionToFinish) {
            waitForPresenceDetection();
        }

        return true;
    }

    /**
     * Calls updateListener.finalDetectionResult() with a final result value.
     * Safe to be called from different threads. After a call to this method,
     * the presence detection process is finished and all threads are forcefully
     * shut down.
     */
    private synchronized void submitFinalResult() {
        // Do nothing if we are not in a detection process
        if (executorService == null) {
            return;
        }
        // Finish the detection process
        executorService.shutdownNow();
        executorService = null;
        detectionChecks = 0;

        PresenceDetectionValue v;

        // The cache will be expired by now if cache_time < timeoutInMS. But the device might be actually reachable.
        // Therefore use lastSeenInMS here and not cache.isExpired() to determine if we got a ping response.
        if (lastSeenInMS + timeoutInMS + 100 < System.currentTimeMillis()) {
            // We haven't seen the device in the detection process
            v = new PresenceDetectionValue(destination.getHostAddress(), -1);
        } else {
            // Make the cache valid again and submit the value.
            v = cache.getExpiredValue();
        }
        cache.setValue(v);
        updateListener.finalDetectionResult(v);
    }

    /**
     * This method is called after each individual check and increases a check counter.
     * If the counter equals the total checks,the final result is submitted. This will
     * happen way before the "timeoutInMS", if all checks were successful.
     * Thread safe.
     */
    private synchronized void checkIfFinished() {
        currentCheck += 1;
        if (currentCheck < detectionChecks) {
            return;
        }
        submitFinalResult();
    }

    /**
     * Waits for the presence detection threads to finish. Returns immediately
     * if no presence detection is performed right now.
     */
    public void waitForPresenceDetection() {
        if (executorService == null) {
            return;
        }
        try {
            // We may get interrupted here by cancelRefreshJob().
            executorService.awaitTermination(timeoutInMS + 100, TimeUnit.MILLISECONDS);
            submitFinalResult();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Reset interrupt flag
            executorService.shutdownNow();
            executorService = null;
        }
    }

    /**
     * If the cached PresenceDetectionValue has not expired yet, the cached version
     * is returned otherwise a new reachable PresenceDetectionValue is created with
     * a latency of 0.
     *
     * It is safe to call this method from multiple threads. The returned PresenceDetectionValue
     * might be still be altered in other threads though.
     *
     * @param type The detection type
     * @return The non expired or a new instance of PresenceDetectionValue.
     */
    synchronized PresenceDetectionValue updateReachableValue(PresenceDetectionType type, double latency) {
        lastSeenInMS = System.currentTimeMillis();
        PresenceDetectionValue v;
        if (cache.isExpired()) {
            v = new PresenceDetectionValue(destination.getHostAddress(), 0);
        } else {
            v = cache.getExpiredValue();
        }
        v.updateLatency(latency);
        v.addType(type);
        cache.setValue(v);
        return v;
    }

    protected void performServicePing(int tcpPort) {
        logger.trace("Perform TCP presence detection for {} on port", hostname, tcpPorts);
        try {
            double pingTime = System.nanoTime();
            if (networkUtils.servicePing(destination.getHostAddress(), tcpPort, timeoutInMS)) {
                final double latency = Math.round((System.nanoTime() - pingTime) / 1000000.0f);
                PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.TCP_CONNECTION, latency);
                v.addReachableTcpService(tcpPort);
                updateListener.partialDetectionResult(v);
            }
        } catch (IOException e) {
            // This should not happen and might be a user configuration issue, we log a warning message therefore.
            logger.warn("Could not create a socket connection", e);
        }
    }

    /**
     * Performs an "ARP ping" (ARP request) on the given interface.
     * If it is an iOS device, the {@see NetworkUtils.wakeUpIOS()} method is
     * called before performing the ARP ping.
     *
     * @param interfaceName The interface name. You can request a list of interface names
     *            from {@see NetworkUtils.getInterfaceNames()} for example.
     */
    protected void performARPping(String interfaceName) {
        try {
            logger.trace("Perform ARP ping presence detection for {} on interface", hostname, interfaceName);
            if (iosDevice) {
                networkUtils.wakeUpIOS(destination);
                Thread.sleep(50);
            }
            double pingTime = System.nanoTime();
            if (networkUtils.nativeARPPing(arpPingMethod, arpPingUtilPath, interfaceName,
                    destination.getHostAddress(), timeoutInMS)) {
                final double latency = Math.round((System.nanoTime() - pingTime) / 1000000.0f);
                PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ARP_PING, latency);
                updateListener.partialDetectionResult(v);
            }
        } catch (IOException e) {
            logger.trace("Failed to execute an arp ping for ip {}", hostname, e);
        } catch (InterruptedException ignored) {
            // This can be ignored, the thread will end anyway
        }
    }

    /**
     * Performs a java ping. It is not recommended to use this, as it is not interruptible,
     * and will not work on windows systems reliably and will fall back from ICMP pings to
     * the TCP echo service on port 7 which barely no device or server supports nowadays.
     * (http://docs.oracle.com/javase/7/docs/api/java/net/InetAddress.html#isReachable%28int%29)
     */
    protected void performJavaPing() {
        try {
            logger.trace("Perform java ping presence detection for {}", hostname);
            double pingTime = System.nanoTime();
            if (destination.isReachable(timeoutInMS)) {
                final double latency = Math.round((System.nanoTime() - pingTime) / 1000000.0f);
                PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING, latency);
                updateListener.partialDetectionResult(v);
            }
        } catch (IOException e) {
            logger.trace("Failed to execute a java ping for ip {}", hostname, e);
        }
    }

    protected void performSystemPing() {
        try {
            logger.trace("Perform native ping presence detection for {}", hostname);
            double pingTime = System.nanoTime();
            if (networkUtils.nativePing(pingMethod, destination.getHostAddress(), timeoutInMS)) {
                final double latency = Math.round((System.nanoTime() - pingTime) / 1000000.0f);
                PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING, latency);
                updateListener.partialDetectionResult(v);
            }
        } catch (IOException e) {
            logger.trace("Failed to execute a native ping for ip {}", hostname, e);
        } catch (InterruptedException e) {
            // This can be ignored, the thread will end anyway
        }
    }

    @Override
    public void dhcpRequestReceived(String ipAddress) {
        PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.DHCP_REQUEST, 0);
        updateListener.partialDetectionResult(v);
    }

    /**
     * Start/Restart a fixed scheduled runner to update the devices reach-ability state.
     *
     * @param scheduledExecutorService A scheduler to run pings periodically.
     */
    public void startAutomaticRefresh(ScheduledExecutorService scheduledExecutorService) {
        if (refreshJob != null && !refreshJob.isDone()) {
            refreshJob.cancel(true);
        }
        refreshJob = scheduledExecutorService.scheduleWithFixedDelay(() -> performPresenceDetection(true), 0,
                refreshIntervalInMS, TimeUnit.MILLISECONDS);

        enableDHCPListen(useDHCPsniffing);
    }

    /**
     * Return true if automatic refreshing is enabled.
     */
    public boolean isAutomaticRefreshing() {
        return refreshJob != null;
    }

    /**
     * Stop automatic refreshing.
     */
    public void stopAutomaticRefresh() {
        if (refreshJob != null && !refreshJob.isDone()) {
            refreshJob.cancel(true);
            refreshJob = null;
        }
        enableDHCPListen(false);
    }

    /**
     * Enables/Disables listing for dhcp packets to figure out if devices have entered the network. This does not work
     * for iOS devices. The hostname of this network service object will be registered to the dhcp request packet
     * listener if enabled and unregistered otherwise.
     *
     * @param enabled Enable/Disable the dhcp listen service for this hostname.
     */
    private void enableDHCPListen(boolean enabled) {
        if (enabled) {
            try {
                if (DHCPListenService.register(destination.getHostAddress(), this).isUseUnprevilegedPort()) {
                    dhcpState = "No access right for port 67. Bound to port 6767 instead. Port forwarding necessary!";
                } else {
                    dhcpState = "Running normally";
                }
            } catch (SocketException e) {
                logger.warn("Cannot use DHCP sniffing.", e);
                useDHCPsniffing = false;
                dhcpState = "Cannot use DHCP sniffing: " + e.getLocalizedMessage();
            }
        } else {
            DHCPListenService.unregister(destination.getHostAddress());
            dhcpState = "off";
        }
    }
}