← 输入框|github.com|刷新
Skip to content

Getting service info when a single server is removed #1235

Open

Description

@kiplingw

Hey everyone,

I note that the listener's add_service() callback has available to it the coordinates for the custom service that I am tracking. Whenever it comes online this callback can use get_service_info() to get the host and port.

I note that the listener's remove_service() callback cannot try to obtain the host and port of the service that just went offline by querying get_service_info(). The latter will return None.

My application would like to track a particular service and update a GUI accordingly. When the service comes online it's easy to add the host and port to some kind of GUI list, but I cannot figure out how to determine which server just went down when remove_service() callback is invoked.

How can I get this information?

Maybe I'm going about this all the wrong way, but presumably this is a common issue others have had in the past.

Activity

  1. bdraco commented on Sep 3, 2023

    @bdraco
    Member

    You need to watch for Removed callbacks. Check the examples

  2. kiplingw commented on Sep 3, 2023

    @kiplingw
    Author

    Hey @bdraco. Thanks for responding. As I mentioned, as far as I understand it, the remove_service() callback doesn't have enough information available to it to obtain the host and port of the service that just went down when it queries get_service_info(). The latter will return None.

    I looked at the examples, but perhaps I'm missing something?

  3. bdraco commented on Sep 3, 2023

    @bdraco
    Member

    Once a service has been removed it’s info is gone and there is no responder on the network anymore to answer questions to fill ServiceInfo, you’ll have to save it when it’s added if you need it later

  4. kiplingw commented on Sep 3, 2023

    @kiplingw
    Author

    No I get that, but even if you keep a list as each service is added, you don't know which one of them was removed when you get the notification - only that one of them has been.

  5. bdraco commented on Sep 3, 2023

    @bdraco
    Member

    The name is in the remove callback so you can keep a dict of the service info by name when it's added

  6. kiplingw commented on Sep 3, 2023

    @kiplingw
    Author

    Yes, but as I mentioned I'm only tracking one type of custom service. So all of the add and remove notifications are for the same type of service, but not necessarily the same server.

  7. bdraco commented on Sep 3, 2023

    @bdraco
    Member

    You probably need to install your own RecordUpdateListener and watch records as they come in and are removed.

  8. kiplingw commented on Sep 3, 2023

    @kiplingw
    Author

    Thanks @bdraco, but I'm still struggling. I looked up what I could find on RecordUpdateListener and I'm not clear if I need to change the following to derive from that class?

    # Zeroconf local network service listener with callbacks to discover MyService
    #  server...
    class LocalNetworkServiceListener:
    
        # Constructor...
        def __init__(self, logging=True, add_callback=None, remove_callback=None):
            self._logging           = logging
            self._add_callback      = add_callback
            self._remove_callback   = remove_callback
            self.found_event        = threading.Event()
            self._servers           = []
            self._name_regex        = r'^MyService(\s\#\d*)?\._http(s)?\._tcp\.local\.$'
            self._service_tls_regex = r'^MyService(\s\#\d*)?\._https\._tcp\.local\.$'
    
        # Service online callback...
        def add_service(self, zeroconf, type, name):
    
            # Which service?
            info = zeroconf.get_service_info(type, name)
    
            # Found server...
            if re.match(self._name_regex, name) is not None:
    
                # No network service information available...
                if info is None:
    
                    # Log, if requested...
                    if self._logging:
                        print(_(F"MyService server {colored(_('online'), 'green')} (no service information available)"))
    
                    # Nothing more to do...
                    return
    
                # Extract host and port...
                host = info.parsed_addresses()[0]
                port = info.port
                tls  = bool(re.match(self._service_tls_regex, name) is not None)
    
                # If not already known...
                if not self._servers.count((host,port,tls)):
    
                    # Add to available list...
                    self._servers.append((host,port,tls))
    
                    # Invoke user callback, if provided...
                    if self._add_callback is not None:
                        self._add_callback(host, port, tls)
    
                # Prepare message to notify user...
                if re.match(self._service_tls_regex, name) is not None:
                    message = _(F"MyService server {colored(_('online'), 'green')} at {host}:{port} ({colored(_('TLS'), 'green')})")
                else:
                    message = _(F"MyService server {colored(_('online'), 'green')} at {host}:{port} ({colored(_('TLS disabled'), 'red')})")
    
                # Log, if requested...
                if self._logging:
                    print(message)
    
                # Alert any waiting threads at least one server is found...
                self.found_event.set()
    
        # Service went offline callback...
        def remove_service(self, zeroconf, type, name):
    
            # Which service?
            info = zeroconf.get_service_info(type, name)
    
            # Not a MyService server...
            if re.match(self._name_regex, name) is None:
                return
    
            # No network service information available...
            if info is None:
    
                # Log, if requested...
                if self._logging:
                    print(_(F"MyService server {colored(_('offline'), 'yellow')} (no service information available)"))
    
                # Invoke user's callback, if requested...
    #            if self._remove_callback is not None:
    #                self._remove_callback(host, port)
    
                # Done...
                return
    
            # Extract host and port...
            host = info.parsed_addresses()[0]
            port = info.port
    
            # Remove from available list...
            for server in self._servers:
    
                # If it was already there, remove from available list...
                if (server[0] is host) and (server[1] is port):
                    self._servers.remove(server)
    
            # If nothing is available, clear event flag...
            if len(self._servers) == 0:
                self.found_event.clear()
    
        # Get server list of all servers found...
        def get_found(self):
            return self._servers
    
    
  9. kiplingw commented on Sep 3, 2023

    @kiplingw
    Author

    I'm sorry but it doesn't. It's not clear what you're directing me to. This looks like a method of a class derived from zeroconf.RecordUpdateListener? It's not clear to me what async_update_records is doing and how that relates to my problem.

  10. kiplingw commented on Mar 25, 2024

    @kiplingw
    Author

    👋🏽

  11. bdraco commented on Apr 1, 2024

    @bdraco
    Member

    You'll have to cache the data when the service is added or updated, as once it's removed, the device is off-line and will not respond to requests

  12. kiplingw commented on Apr 1, 2024

    @kiplingw
    Author

    Right, but cache which data?

  13. bdraco commented on Apr 13, 2024

    @bdraco
    Member

    From your opening text, I expect you need to keep a map of service name to host/port and look it up in the dict when it goes offline

  14. kiplingw commented on Apr 13, 2024

    @kiplingw
    Author

    @bdraco, as I mentioned above the remove notification doesn't tell you which host went offline. So even if you keep a cache of service, host, and port as they come online, you don't know which one of them went offline when they do. You're only given the name of the service.

  15. 1 remaining item

  16. kiplingw commented on Apr 13, 2024

    @kiplingw
    Author

    That still doesn't address the problem. As I said above I have multiple machines advertising the same service on the network. Each machine has a different host and port. When the service is announced a listener sees the name of the service, its host, and port. When the service goes offline, the listener only knows that an instance of a service name is gone, but not which host and port.

  17. bdraco commented on Apr 13, 2024

    @bdraco
    Member

    Its a bit unconventional that you would have multiple different hosts published for the same service name that change frequently, but in that case you'll have to go with the RecordUpdateListener I originally suggested in #1235 (comment) and make an async_update_records which watches for a RecordUpdate to know when records to go away when they expire. You can check if the record is expired with is_expired

    def async_add_listener(

    def async_update_records(self, zc: 'Zeroconf', now: float_, records: List[RecordUpdate]) -> None:

  18. kiplingw commented on Apr 14, 2024

    @kiplingw
    Author

    Thanks @bdraco. I'll give it a go when I have time and report back.

  19. sdbbs commented on Apr 19, 2024

    @sdbbs

    I just ran into this problem, I will try to illustrate.

    Its a bit unconventional that you would have multiple different hosts published for the same service name that change frequently

    Well, I have a work setup, where I have (let's say) 10 Raspberry Pis, each of which runs an OLA server. And also, let's say I want to keep track of all of them - whether their OLA servers are online - in a table.

    So, I run my class MyListener(ServiceListener) script, and have a corresponding printout print(f"add_service {name=} {type_=} {info=} {zc=}") or print(f"remove_service {name=} {type_=} {info=} {zc=}") in their respective handlers (and of course, a info = zc.get_service_info(type_, name) before the printouts.

    So first I get this printout (formatting mine)

    add_service name='OLA Server._http._tcp.local.' 
      type_='_http._tcp.local.' 
      info=ServiceInfo(
        type='_http._tcp.local.', 
        name='OLA Server._http._tcp.local.', 
        addresses=[b'\xc0\xa8\x01\x0f'], 
        port=9090, 
        weight=0, 
        priority=0, 
        server='my-rpi-03.local.', 
        properties={b'path': b'/'}, 
        interface_index=None) 
    zc=<zeroconf._core.Zeroconf object at 0x000002772cf16d10>
    

    Great - so I have an OLA Server running on device with hostname my-rpi-03, with IP address list(b'\xc0\xa8\x01\x0f') == [192, 168, 1, 15], listening on port 9090. So I can add this entry to my table:

    Hostname IP address OLA port
    my-rpi-03.local. 192.168.1.15 9090

    Soon afterwards, I get this printout:

    add_service name='OLA Server._http._tcp.local.' 
      type_='_http._tcp.local.' 
      info=ServiceInfo(
        type='_http._tcp.local.', 
        name='OLA Server._http._tcp.local.', 
        addresses=[b'\xc0\xa8\x01\x1b'], 
        port=9090, 
        weight=0, 
        priority=0, 
        server='my-rpi-04.local.', 
        properties={b'path': b'/'}, 
        interface_index=None) 
    zc=<zeroconf._core.Zeroconf object at 0x000002772cf16d10>
    

    Great again - so I also have an OLA Server running on device with hostname my-rpi-04, with IP address list(b'\xc0\xa8\x01\x1b') == [192, 168, 1, 27], listening again on port 9090. So I can add also this entry to my table:

    Hostname IP address OLA port
    my-rpi-03.local. 192.168.1.15 9090
    my-rpi-04.local. 192.168.1.27 9090

    Excellent - exactly what I want: a list of devices with their hostnames and IP addresses, offering the service I'm interested on, on a given port.

    OK, now just for fun, I log in to my-rpi-04 over ssh, and shut down the OLA server with:

    sudo service olad stop
    

    Soon thereafter, I receive the remove_service printout:

    remove_service name='OLA Server._http._tcp.local.' 
      type_='_http._tcp.local.' 
      info=None 
    zc=<zeroconf._core.Zeroconf object at 0x000002772cf16d10>
    

    Great - now I know that an OLA Server went offline - but which one? 192.168.1.15 or 192.168.1.27? Which entry do I remove from the table?

    Looking at this:

    you'll have to go with the RecordUpdateListener I originally suggested in #1235 (comment) and make an async_update_records which watches for a RecordUpdate to know when records to go away when they expire. You can check if the record is expired with is_expired

    ... I still cannot tell whether this RecordUpdate will have some sort of a unique identifier, relating it to a given hostname + IP address + port combination? Because if there isn't such an identifier, it is useless for the kind of "table tracking" I've just described above. But I guess, I'll have to take a look ...

    In any case - if worse comes to worse, - I guess I could live with the following workaround: remove_service event comes in, we don't know from whom; so we just delete the entire table of online servers, and then somehow restart the entire Zeroconf discovery process, and populate the table anew - at least, when this process is done, the table should be accurate. But is there a command to restart the Zeroconf discovery process gracefully (at least, more gracefully than e.g. deleting the whole MyListener object, and re-instantiating it again? or exiting the app and restarting it?)

  20. kiplingw commented on Apr 19, 2024

    @kiplingw
    Author

    Excellent illustration of the problem, @sdbbs. Thank you.

  21. sdbbs commented on Apr 19, 2024

    @sdbbs

    OK, I found one workaround: given that in the context of this problem, we actually start by managing an online servers table, obviously, at first, we will get this table populated with entries advertising the service of interest (SOI) via add_service.

    Then, let's say remove_service hits - as stated before, at this point, we do not know which device server went offlne; however, thanks to our initial goal to manage a table -- at this point, we do know which servers had previously been online, and we have a concrete, known, number of them in a list/table.

    So then, the solution is: when remove_service hits, loop through all entries of the (previously) online servers table, and check if the specified port at the specified IP address is open; when we arrive at a device in the online servers list that does not respond at the port any more, then it is this device('s server) that went offline, and is the source of remove_service Zeroconf event; and so, it is this device entry that should be removed from the online servers list.

    Here is an example I cooked up from the README example:

    #!/usr/bin/env python3
    
    from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
    import socket
    
    SOI = "OLA Server._http._tcp.local." # Service Of Interest
    online_servers_list = []
    
    def socket_check_port(ip_address, port):
      """ inspired from https://stackoverflow.com/q/19196105 """
      s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      s.settimeout(0.5) # Timeout in case of port not open
      try:
        s.connect((ip_address, port))
        return True
      except:
        return False
    
    
    class MyListener(ServiceListener):
    
      def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
        info = zc.get_service_info(type_, name)
        #print(f"update_service {name=} {type_=} {info=} {zc=}")
    
      def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
        info = zc.get_service_info(type_, name)
        #print(f"remove_service {name=} {type_=} {info=} {zc=}")
        if name == SOI:
          print("")
          print(f"remove_service {info=} - so iterate through previously live entries in online_servers_list, and check open ports on previously found servers")
          idx_to_remove = -1
          for idx, idict in enumerate(online_servers_list):
            ip_address, port = idict["ip_address"], idict["port"]
            is_port_open = socket_check_port(ip_address, port)
            print(f"remove_service list {idx=} : {ip_address=} {port=} {is_port_open=}")
            if not(is_port_open):
              print(f"NOTE: previously live server with list {idx=} has gone offline!")
              idx_to_remove = idx
              break
          if idx_to_remove>-1:
            # remove entry
            online_servers_list.pop(idx_to_remove)
          print(f"\nremove_service: {online_servers_list=}\n")
      def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
        info = zc.get_service_info(type_, name)
        #print(f"add_service {name=} {type_=} {info=} {zc=}")
        if name == SOI:
          ip_address = ".".join(map(str, list(info.addresses[0])))
          is_port_open = socket_check_port(ip_address, info.port)
          print(f"add_service {ip_address=} {info.port=} {is_port_open=}")
          tdict = {"hostname": info.server, "ip_address": ip_address, "port": info.port}
          entry_found = False
          for idict in online_servers_list:
            is_same_device = ( (tdict["hostname"] == idict["hostname"]) and (tdict["ip_address"] == idict["ip_address"]) )
            if is_same_device:
              entry_found = True
              break
          if not entry_found:
            online_servers_list.append(tdict)
          print(f"\nadd_service: {online_servers_list=}\n")
    
    
    zeroconf = Zeroconf()
    listener = MyListener()
    browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)
    try:
      input("Press enter to exit...\n\n")
    finally:
      zeroconf.close()
    

    So I could only test with one RPi with OLA on the local network; but when I start the script with RPI OLA server online on local network, I get printouts:

    add_service ip_address='192.168.1.15' info.port=9090 is_port_open=True
    
    add_service: online_servers_list=[{'hostname': 'my-rpi-03.local.', 'ip_address': '192.168.1.15', 'port': 9090}]
    

    Then I log in to the RPi, and shut down OLA server with

    sudo service olad stop
    

    ... and soon afterwards I get this printout:

    remove_service info=None - so iterate through previously live entries in online_servers_list, and check open p
    orts on previously found servers
    remove_service list idx=0 : ip_address='192.168.1.15' port=9090 is_port_open=False
    NOTE: previously live server with list idx=0 has gone offline!
    
    remove_service: online_servers_list=[]
    

    For the kind of context I need this for (servers running on Raspberry Pis on local network, unencrypted) this seems to work - though I'm not sure if socket checking for open ports will work for HTTPS servers or similar .... so some more complicated "check open port" or "establish connection" or "ping" methods might be required there for this concept to work ...

    This has not been extensively tested so YMMV, but at least with one device, - in principle - it seems to work.

  22. kiplingw commented on Apr 19, 2024

    @kiplingw
    Author
  23. added
    multi-srv-record-supportThis library currently lacks support for handling multiple SRV records for the same name
    on Feb 14, 2025
  24. changed the title [-]Getting service info on unregistration[/-] [+]Getting service info when a server is renamed[/+] on Feb 14, 2025
  25. changed the title [-]Getting service info when a server is renamed[/-] [+]Getting service info when a server is removed[/+] on Feb 14, 2025
  26. changed the title [-]Getting service info when a server is removed[/-] [+]Getting service info when a single server is removed[/+] on Feb 14, 2025
  27. bdraco commented on Feb 15, 2025

    @bdraco
    Member

    I think what you are asking for is a new type of browser aka ServerBrowser

    Assuming the following

    • you have multiple SRV records for the same name
    • you need to know when they are added and remove
    • you need a background task to rescues the records since SRV records have a short TTL while the browser is running
    • you need to know when the associated A/AAAA records change

    This could probably be implemented as a new browser object that in addition a to the service changes (PTR records) you could get a separate callback for when a server is added,remove,changed (including the A/AAAA) records

    Assuming all the above, this is a bit of a large project that should take about 2-3 weeks to properly build and test.

  28. kiplingw commented on Feb 15, 2025

    @kiplingw
    Author

    Thanks @bdraco. I'd love to contribute, but I'm not an expert in the library. However, I'd be happy to test a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions