discoverer: Rework how discoveries are done

This implements a backoff timer that waits an additional second every time an
announcement is done. This should allow us to discover all the devices on a
local segment fully.

Change-Id: I0443e44fd80cf494a5bb010b260fce5537f770b4
diff --git a/mdt/command.py b/mdt/command.py
index 7f4b110..73f76f5 100644
--- a/mdt/command.py
+++ b/mdt/command.py
@@ -56,16 +56,12 @@
             self.address = self.device
 
         if not self.address:
-            self.discoverer.start()
             if self.device:
                 print('Waiting for device {0}...'.format(self.device))
             else:
                 print('Waiting for a device...')
+            self.discoverer.discover()
 
-            while not self.address:
-                sleep(0.1)
-
-        self.discoverer.stop()
         client = None
         try:
             print('Connecting to {0} at {1}'.format(self.device, self.address))
diff --git a/mdt/devices.py b/mdt/devices.py
index 89f9421..a74fdca 100644
--- a/mdt/devices.py
+++ b/mdt/devices.py
@@ -40,8 +40,7 @@
         self.device = Config().preferredDevice()
 
     def run(self, args):
-        self.discoverer.start()
-        sleep(1)
+        self.discoverer.discover()
         discoveries = self.discoverer.discoveries
         for host, address in discoveries.items():
             if self.device and host == self.device:
@@ -53,16 +52,7 @@
 class DevicesWaitCommand:
     '''Usage: mdt wait-for-device
 
-Waits for either the first device found, or your preferred device to be
-discovered on the local network segment.
-
-Variables used:
-   preferred-device: contains the device name you want as your default
-                     Can be set to an IPv4 address to bypass the mDNS lookup.
-
-Note: if preferred-device is cleared, then this will return on the first
-available device found. Also, MDT uses a python implementation of mDNS
-ZeroConf for discovery, so it does not require a running Avahi daemon.
+Waits until a device is found.
 '''
 
     def __init__(self):
@@ -76,7 +66,9 @@
 
     def run(self, args):
         print('Waiting for device...')
-        self.discoverer.start()
+
         while not self.found_devices:
-            sleep(0.1)
-        print('Device found: {0} ({1})'.format(self.hostname, self.address))
+            self.discoverer.discover()
+
+        print('Found {0} devices.'.format(len(self.discoverer.discoveries)))
+        return 0
diff --git a/mdt/discoverer.py b/mdt/discoverer.py
index 3be13e1..3550c6a 100644
--- a/mdt/discoverer.py
+++ b/mdt/discoverer.py
@@ -19,36 +19,58 @@
 from zeroconf import ServiceBrowser, Zeroconf
 
 import socket
+import time
 
 
 class Discoverer:
+    ANNOUNCE_PERIOD_SECS = 1
+    MAXIMUM_WAIT_CYCLES = 10
+    SERVICE_TYPE = "_googlemdt._tcp.local."
+
     def __init__(self, listener=None):
-        self.zeroconf = Zeroconf()
         self.discoveries = {}
         self.listener = listener
-        self.browser = None
+        self.zeroconf = None
 
-    def start(self):
-        self.browser = ServiceBrowser(self.zeroconf, "_googlemdt._tcp.local.", self)
+    def discover(self):
+        self.zeroconf = Zeroconf()
+        self.browser = ServiceBrowser(self.zeroconf, Discoverer.SERVICE_TYPE, self)
+        self._heard_announcement = True
+        cycle_count = 0
+
+        # Keep waiting until we stop hearing announcements for a full second, or until we've waited 10 seconds
+        while self._heard_announcement and cycle_count < Discoverer.MAXIMUM_WAIT_CYCLES:
+            cycle_count += 1
+            self._heard_announcement = False
+            time.sleep(Discoverer.ANNOUNCE_PERIOD_SECS)
+
+        self.browser.cancel()
+        self.browser = None
+        self.zeroconf = None
 
     def add_service(self, zeroconf, type, name):
         info = self.zeroconf.get_service_info(type, name)
+
         if info:
             hostname = info.server.split('.')[0]
             address = socket.inet_ntoa(cast(bytes, info.address))
+
+            # Prevent duplicate announcements from extending the discovery delay
+            if hostname not in self.discoveries:
+                self._heard_announcement = True
+
             self.discoveries[hostname] = address
+
             if self.listener and hasattr(self.listener, "add_device"):
                 self.listener.add_device(hostname, address)
 
     def remove_service(self, zeroconf, type, name):
         info = self.zeroconf.get_service_info(type, name)
+        self._heard_announcement = True
+
         if self.listener and hasattr(self.listener, "remove_device"):
             self.listener.remove_device(info.server,
                                         self.discoveries[info.server])
+
         if info.server in self.discoveries:
             del(self.discoveries[info.server])
-
-    def stop(self):
-        if self.browser:
-            self.browser.cancel()
-            self.browser = None