[adb client] Add "mdns services" command.

This command list all discovered mdns services, so we
can connect via service name later on.

Bug: 152521166

Test: 'adb mdns services'
Test: test_adb.py
Change-Id: I23d42a7933e67a65bd0c9924afd6abe5915c0a11
diff --git a/adb/adb.cpp b/adb/adb.cpp
index aced079..dcec0ba 100644
--- a/adb/adb.cpp
+++ b/adb/adb.cpp
@@ -1086,6 +1086,11 @@
         SendOkay(reply_fd, check);
         return true;
     }
+    if (service == "services") {
+        std::string services_list = mdns_list_discovered_services();
+        SendOkay(reply_fd, services_list);
+        return true;
+    }
 
     return false;
 }
diff --git a/adb/adb_wifi.h b/adb/adb_wifi.h
index 49d3a91..3a6b0b1 100644
--- a/adb/adb_wifi.h
+++ b/adb/adb_wifi.h
@@ -28,6 +28,7 @@
 bool adb_wifi_is_known_host(const std::string& host);
 
 std::string mdns_check();
+std::string mdns_list_discovered_services();
 
 #else  // !ADB_HOST
 
diff --git a/adb/client/adb_client.h b/adb/client/adb_client.h
index 27be28f..caf4e86 100644
--- a/adb/client/adb_client.h
+++ b/adb/client/adb_client.h
@@ -90,8 +90,9 @@
 
 // ADB Secure DNS service interface. Used to query what ADB Secure DNS services have been
 // resolved, and to run some kind of callback for each one.
-using adb_secure_foreach_service_callback = std::function<void(
-        const char* _Nonnull service_name, const char* _Nonnull ip_address, uint16_t port)>;
+using adb_secure_foreach_service_callback =
+        std::function<void(const char* _Nonnull service_name, const char* _Nonnull reg_type,
+                           const char* _Nonnull ip_address, uint16_t port)>;
 
 // Queries pairing/connect services that have been discovered and resolved.
 // If |host_name| is not null, run |cb| only for services
diff --git a/adb/client/commandline.cpp b/adb/client/commandline.cpp
index 7b8b2d7..d565e01 100644
--- a/adb/client/commandline.cpp
+++ b/adb/client/commandline.cpp
@@ -128,6 +128,7 @@
         " reverse --remove REMOTE  remove specific reverse socket connection\n"
         " reverse --remove-all     remove all reverse socket connections from device\n"
         " mdns check               check if mdns discovery is available\n"
+        " mdns services            list all discovered services\n"
         "\n"
         "file transfer:\n"
         " push [--sync] [-z ALGORITHM] [-Z] LOCAL... REMOTE\n"
@@ -1925,6 +1926,10 @@
         if (!strcmp(argv[0], "check")) {
             if (argc != 1) error_exit("mdns %s doesn't take any arguments", argv[0]);
             query += "check";
+        } else if (!strcmp(argv[0], "services")) {
+            if (argc != 1) error_exit("mdns %s doesn't take any arguments", argv[0]);
+            query += "services";
+            printf("List of discovered mdns services\n");
         } else {
             error_exit("unknown mdns command [%s]", argv[0]);
         }
diff --git a/adb/client/transport_mdns.cpp b/adb/client/transport_mdns.cpp
index 38a760f..2bf062f 100644
--- a/adb/client/transport_mdns.cpp
+++ b/adb/client/transport_mdns.cpp
@@ -216,6 +216,9 @@
 
         int adbSecureServiceType = serviceIndex();
         switch (adbSecureServiceType) {
+            case kADBTransportServiceRefIndex:
+                sAdbTransportServices->push_back(this);
+                break;
             case kADBSecurePairingServiceRefIndex:
                 sAdbSecurePairingServices->push_back(this);
                 break;
@@ -233,16 +236,21 @@
 
     std::string serviceName() const { return serviceName_; }
 
+    std::string regType() const { return regType_; }
+
     std::string ipAddress() const { return ip_addr_; }
 
     uint16_t port() const { return port_; }
 
     using ServiceRegistry = std::vector<ResolvedService*>;
 
+    // unencrypted tcp connections
+    static ServiceRegistry* sAdbTransportServices;
+
     static ServiceRegistry* sAdbSecurePairingServices;
     static ServiceRegistry* sAdbSecureConnectServices;
 
-    static void initAdbSecure();
+    static void initAdbServiceRegistries();
 
     static void forEachService(const ServiceRegistry& services, const std::string& hostname,
                                adb_secure_foreach_service_callback cb);
@@ -264,13 +272,19 @@
 };
 
 // static
+std::vector<ResolvedService*>* ResolvedService::sAdbTransportServices = NULL;
+
+// static
 std::vector<ResolvedService*>* ResolvedService::sAdbSecurePairingServices = NULL;
 
 // static
 std::vector<ResolvedService*>* ResolvedService::sAdbSecureConnectServices = NULL;
 
 // static
-void ResolvedService::initAdbSecure() {
+void ResolvedService::initAdbServiceRegistries() {
+    if (!sAdbTransportServices) {
+        sAdbTransportServices = new ServiceRegistry;
+    }
     if (!sAdbSecurePairingServices) {
         sAdbSecurePairingServices = new ServiceRegistry;
     }
@@ -283,17 +297,18 @@
 void ResolvedService::forEachService(const ServiceRegistry& services,
                                      const std::string& wanted_service_name,
                                      adb_secure_foreach_service_callback cb) {
-    initAdbSecure();
+    initAdbServiceRegistries();
 
     for (auto service : services) {
         auto service_name = service->serviceName();
+        auto reg_type = service->regType();
         auto ip = service->ipAddress();
         auto port = service->port();
 
         if (wanted_service_name == "") {
-            cb(service_name.c_str(), ip.c_str(), port);
+            cb(service_name.c_str(), reg_type.c_str(), ip.c_str(), port);
         } else if (service_name == wanted_service_name) {
-            cb(service_name.c_str(), ip.c_str(), port);
+            cb(service_name.c_str(), reg_type.c_str(), ip.c_str(), port);
         }
     }
 }
@@ -301,7 +316,7 @@
 // static
 bool ResolvedService::connectByServiceName(const ServiceRegistry& services,
                                            const std::string& service_name) {
-    initAdbSecure();
+    initAdbServiceRegistries();
     for (auto service : services) {
         if (service_name == service->serviceName()) {
             D("Got service_name match [%s]", service->serviceName().c_str());
@@ -398,6 +413,9 @@
     int index = adb_DNSServiceIndexByName(regType);
     ResolvedService::ServiceRegistry* services;
     switch (index) {
+        case kADBTransportServiceRefIndex:
+            services = ResolvedService::sAdbTransportServices;
+            break;
         case kADBSecurePairingServiceRefIndex:
             services = ResolvedService::sAdbSecurePairingServices;
             break;
@@ -542,7 +560,7 @@
 }
 
 void init_mdns_transport_discovery(void) {
-    ResolvedService::initAdbSecure();
+    ResolvedService::initAdbServiceRegistries();
     std::thread(init_mdns_transport_discovery_thread).detach();
 }
 
@@ -559,3 +577,17 @@
     result = android::base::StringPrintf("mdns daemon version [%u]", daemon_version);
     return result;
 }
+
+std::string mdns_list_discovered_services() {
+    std::string result;
+    auto cb = [&](const char* service_name, const char* reg_type, const char* ip_addr,
+                  uint16_t port) {
+        result += android::base::StringPrintf("%s\t%s\t%s:%u\n", service_name, reg_type, ip_addr,
+                                              port);
+    };
+
+    ResolvedService::forEachService(*ResolvedService::sAdbTransportServices, "", cb);
+    ResolvedService::forEachService(*ResolvedService::sAdbSecureConnectServices, "", cb);
+    ResolvedService::forEachService(*ResolvedService::sAdbSecurePairingServices, "", cb);
+    return result;
+}
diff --git a/adb/test_adb.py b/adb/test_adb.py
index 6989e3b..03bdcbd 100755
--- a/adb/test_adb.py
+++ b/adb/test_adb.py
@@ -32,6 +32,8 @@
 import time
 import unittest
 import warnings
+from importlib import util
+from parameterized import parameterized_class
 
 def find_open_port():
     # Find an open port.
@@ -583,10 +585,91 @@
                                           "mdns", "check"]).strip()
         return output.startswith(b"mdns daemon version")
 
+"""Check if we have zeroconf python library installed"""
+def is_zeroconf_installed():
+    zeroconf_spec = util.find_spec("zeroconf")
+    return zeroconf_spec is not None
+
+@contextlib.contextmanager
+def zeroconf_context(ipversion):
+    from zeroconf import Zeroconf
+    """Context manager for a zeroconf instance
+
+    This creates a zeroconf instance and returns it.
+    """
+
+    try:
+        zeroconf = Zeroconf(ip_version=ipversion)
+        yield zeroconf
+    finally:
+        zeroconf.close()
+
+@contextlib.contextmanager
+def zeroconf_register_service(zeroconf_ctx, info):
+    """Context manager for a zeroconf service
+
+    Registers a service and unregisters it on cleanup. Returns the ServiceInfo
+    supplied.
+    """
+
+    try:
+        zeroconf_ctx.register_service(info)
+        yield info
+    finally:
+        zeroconf_ctx.unregister_service(info)
+
+"""Should match the service names listed in adb_mdns.h"""
+@parameterized_class(('service_name',), [
+    ("adb",),
+    ("adb-tls-connect",),
+    ("adb-tls-pairing",),
+])
 @unittest.skipIf(not is_adb_mdns_available(), "mdns feature not available")
 class MdnsTest(unittest.TestCase):
     """Tests for adb mdns."""
-    pass
+
+    @unittest.skipIf(not is_zeroconf_installed(), "zeroconf library not installed")
+    def test_mdns_services_register_unregister(self):
+        """Ensure that `adb mdns services` correctly adds and removes a service
+        """
+        from zeroconf import IPVersion, ServiceInfo
+ 
+        def _mdns_services(port):
+            output = subprocess.check_output(["adb", "-P", str(port), "mdns", "services"])
+            return [x.split("\t") for x in output.decode("utf8").strip().splitlines()[1:]]
+
+        with adb_server() as server_port:
+            output = subprocess.check_output(["adb", "-P", str(server_port),
+                                              "mdns", "services"]).strip()
+            self.assertTrue(output.startswith(b"List of discovered mdns services"))
+            print(f"services={_mdns_services(server_port)}")
+
+            """TODO(joshuaduong): Add ipv6 tests once we have it working in adb"""
+            """Register/Unregister a service"""
+            with zeroconf_context(IPVersion.V4Only) as zc:
+                serv_instance = "my_fake_test_service"
+                serv_type = "_" + self.service_name + "._tcp."
+                serv_ipaddr = socket.inet_aton("1.2.3.4")
+                serv_port = 12345
+                service_info = ServiceInfo(
+                        serv_type + "local.",
+                        name=serv_instance + "." + serv_type + "local.",
+                        addresses=[serv_ipaddr],
+                        port=serv_port)
+                print(f"Registering {serv_instance}.{serv_type} ...")
+                with zeroconf_register_service(zc, service_info) as info:
+                    """Give adb some time to register the service"""
+                    time.sleep(0.25)
+                    print(f"services={_mdns_services(server_port)}")
+                    self.assertTrue(any((serv_instance in line and serv_type in line)
+                        for line in _mdns_services(server_port)))
+
+                """Give adb some time to unregister the service"""
+                print("Unregistering mdns service...")
+                time.sleep(0.25)
+                print(f"services={_mdns_services(server_port)}")
+                self.assertFalse(any((serv_instance in line and serv_type in line)
+                    for line in _mdns_services(server_port)))
 
 def main():
     """Main entrypoint."""