@@ -256,6 +256,7 @@ def __init__(
256256 # Automatic identify coordination
257257 self ._identify_inflight : set [ID ] = set ()
258258 self ._identified_peers : set [ID ] = set ()
259+ self ._identify_scopes : dict [ID , trio .CancelScope ] = {}
259260 self ._network .register_notifee (_IdentifyNotifee (self ))
260261
261262 def get_id (self ) -> ID :
@@ -842,6 +843,31 @@ async def disconnect(self, peer_id: ID) -> None:
842843 await self ._network .close_peer (peer_id )
843844
844845 async def close (self ) -> None :
846+ """
847+ Close the host and its underlying network service.
848+ """
849+ # Stop background services
850+ if self .mDNS is not None :
851+ self .mDNS .stop ()
852+
853+ if self .bootstrap is not None :
854+ self .bootstrap .stop ()
855+
856+ # Cleanup UPnP mappings if active
857+ if self .upnp and self .upnp .get_external_ip ():
858+ try :
859+ logger .debug ("Removing UPnP port mappings due to host closure" )
860+ for addr in self .get_addrs ():
861+ if port := addr .value_for_protocol ("tcp" ):
862+ await self .upnp .remove_port_mapping (int (port ), "TCP" )
863+ except Exception as e :
864+ logger .warning (f"Error removing UPnP mappings during close: { e } " )
865+
866+ # Cancel inflight identify tasks
867+ for scope in list (self ._identify_scopes .values ()):
868+ scope .cancel ()
869+
870+ # Close network
845871 await self ._network .close ()
846872
847873 def _schedule_identify (self , peer_id : ID , * , reason : str ) -> None :
@@ -857,14 +883,28 @@ def _schedule_identify(self, peer_id: ID, *, reason: str) -> None:
857883 return
858884 if not self ._should_identify_peer (peer_id ):
859885 return
886+
860887 self ._identify_inflight .add (peer_id )
861- trio .lowlevel .spawn_system_task (self ._identify_task_entry , peer_id , reason )
888+
889+ # Create a new cancel scope for this identify task
890+ cancel_scope = trio .CancelScope ()
891+ self ._identify_scopes [peer_id ] = cancel_scope
892+
893+ trio .lowlevel .spawn_system_task (
894+ self ._identify_task_entry , peer_id , reason , cancel_scope
895+ )
862896
863- async def _identify_task_entry (self , peer_id : ID , reason : str ) -> None :
897+ async def _identify_task_entry (
898+ self , peer_id : ID , reason : str , cancel_scope : trio .CancelScope
899+ ) -> None :
864900 try :
865- await self ._identify_peer (peer_id , reason = reason )
901+ with cancel_scope :
902+ await self ._identify_peer (peer_id , reason = reason )
866903 finally :
867904 self ._identify_inflight .discard (peer_id )
905+ # Remove scope from tracking if it matches (to avoid race conditions)
906+ if self ._identify_scopes .get (peer_id ) is cancel_scope :
907+ self ._identify_scopes .pop (peer_id , None )
868908
869909 def _has_cached_protocols (self , peer_id : ID ) -> bool :
870910 """
0 commit comments